|
| 1 | +import code |
| 2 | +import signal |
| 3 | +import sys |
| 4 | +import greenlet |
| 5 | +import logging |
| 6 | + |
| 7 | +class SigintHappened(object): |
| 8 | + pass |
| 9 | + |
| 10 | +class SystemExitFromCodeThread(SystemExit): |
| 11 | + pass |
| 12 | + |
| 13 | +class CodeRunner(object): |
| 14 | + """Runs user code in an interpreter, taking care of stdout/in/err""" |
| 15 | + def __init__(self, interp=None, stuff_a_refresh_request=lambda:None): |
| 16 | + self.interp = interp or code.InteractiveInterpreter() |
| 17 | + self.source = None |
| 18 | + self.main_greenlet = greenlet.getcurrent() |
| 19 | + self.code_greenlet = None |
| 20 | + self.stuff_a_refresh_request = stuff_a_refresh_request |
| 21 | + self.code_is_waiting = False |
| 22 | + self.sigint_happened = False |
| 23 | + self.orig_sigint_handler = None |
| 24 | + |
| 25 | + @property |
| 26 | + def running(self): |
| 27 | + return self.source and self.code_greenlet |
| 28 | + |
| 29 | + def load_code(self, source): |
| 30 | + """Prep code to be run""" |
| 31 | + self.source = source |
| 32 | + self.code_greenlet = None |
| 33 | + |
| 34 | + def _unload_code(self): |
| 35 | + """Called when done running code""" |
| 36 | + self.source = None |
| 37 | + self.code_greenlet = None |
| 38 | + self.code_is_waiting = False |
| 39 | + |
| 40 | + def run_code(self, for_code=None): |
| 41 | + """Returns Truthy values if code finishes, False otherwise |
| 42 | +
|
| 43 | + if for_code is provided, send that value to the code greenlet |
| 44 | + if source code is complete, returns "done" |
| 45 | + if source code is incomplete, returns "unfinished" |
| 46 | + """ |
| 47 | + if self.code_greenlet is None: |
| 48 | + assert self.source is not None |
| 49 | + self.code_greenlet = greenlet.greenlet(self._blocking_run_code) |
| 50 | + self.orig_sigint_handler = signal.getsignal(signal.SIGINT) |
| 51 | + signal.signal(signal.SIGINT, self.sigint_handler) |
| 52 | + request = self.code_greenlet.switch() |
| 53 | + else: |
| 54 | + assert self.code_is_waiting |
| 55 | + self.code_is_waiting = False |
| 56 | + signal.signal(signal.SIGINT, self.sigint_handler) |
| 57 | + if self.sigint_happened: |
| 58 | + self.sigint_happened = False |
| 59 | + request = self.code_greenlet.switch(SigintHappened) |
| 60 | + else: |
| 61 | + request = self.code_greenlet.switch(for_code) |
| 62 | + |
| 63 | + if request in ['wait', 'refresh']: |
| 64 | + self.code_is_waiting = True |
| 65 | + if request == 'refresh': |
| 66 | + self.stuff_a_refresh_request() |
| 67 | + return False |
| 68 | + elif request in ['done', 'unfinished']: |
| 69 | + self._unload_code() |
| 70 | + signal.signal(signal.SIGINT, self.orig_sigint_handler) |
| 71 | + self.orig_sigint_handler = None |
| 72 | + return request |
| 73 | + elif request in ['SystemExit']: #use the object? |
| 74 | + self._unload_code() |
| 75 | + raise SystemExitFromCodeThread() |
| 76 | + else: |
| 77 | + raise ValueError("Not a valid value from code greenlet: %r" % request) |
| 78 | + |
| 79 | + def sigint_handler(self, *args): |
| 80 | + if greenlet.getcurrent() is self.code_greenlet: |
| 81 | + logging.debug('sigint while running user code!') |
| 82 | + raise KeyboardInterrupt() |
| 83 | + else: |
| 84 | + logging.debug('sigint while fufilling code request sigint handler running!') |
| 85 | + self.sigint_happened = True |
| 86 | + |
| 87 | + def _blocking_run_code(self): |
| 88 | + try: |
| 89 | + unfinished = self.interp.runsource(self.source) |
| 90 | + except SystemExit: |
| 91 | + return 'SystemExit' |
| 92 | + return 'unfinished' if unfinished else 'done' |
| 93 | + |
| 94 | + def wait_and_get_value(self): |
| 95 | + """Return the argument passed in to .run_code(for_code) |
| 96 | +
|
| 97 | + Nothing means calls to run_code must be... |
| 98 | + """ |
| 99 | + value = self.main_greenlet.switch('wait') |
| 100 | + if value is SigintHappened: |
| 101 | + raise KeyboardInterrupt() |
| 102 | + return value |
| 103 | + |
| 104 | + def refresh_and_get_value(self): |
| 105 | + """Returns the argument passed in to .run_code(for_code) """ |
| 106 | + value = self.main_greenlet.switch('refresh') |
| 107 | + if value is SigintHappened: |
| 108 | + raise KeyboardInterrupt() |
| 109 | + return value |
| 110 | + |
| 111 | +class FakeOutput(object): |
| 112 | + def __init__(self, coderunner, please): |
| 113 | + self.coderunner = coderunner |
| 114 | + self.please = please |
| 115 | + def write(self, *args, **kwargs): |
| 116 | + self.please(*args, **kwargs) |
| 117 | + return self.coderunner.refresh_and_get_value() |
| 118 | + |
| 119 | +def test_simple(): |
| 120 | + orig_stdout = sys.stdout |
| 121 | + orig_stderr = sys.stderr |
| 122 | + c = CodeRunner(stuff_a_refresh_request=lambda: orig_stdout.flush() or orig_stderr.flush()) |
| 123 | + stdout = FakeOutput(c, orig_stdout.write) |
| 124 | + sys.stdout = stdout |
| 125 | + c.load_code('1 + 1') |
| 126 | + c.run_code() |
| 127 | + c.run_code() |
| 128 | + c.run_code() |
| 129 | + |
| 130 | +def test_exception(): |
| 131 | + orig_stdout = sys.stdout |
| 132 | + orig_stderr = sys.stderr |
| 133 | + c = CodeRunner(stuff_a_refresh_request=lambda: orig_stdout.flush() or orig_stderr.flush()) |
| 134 | + def ctrlc(): |
| 135 | + raise KeyboardInterrupt() |
| 136 | + stdout = FakeOutput(c, lambda x: ctrlc()) |
| 137 | + sys.stdout = stdout |
| 138 | + c.load_code('1 + 1') |
| 139 | + c.run_code() |
| 140 | + |
| 141 | +if __name__ == '__main__': |
| 142 | + test_simple() |
| 143 | + |
0 commit comments