def main(): stream = InputStream(callback) with raw_mode(sys.stdin.fileno()): while True: c = sys.stdin.read(1) stream.feed(c)
def setUp(self): class _ProcessorMock(object): def __init__(self): self.keys = [] def feed_key(self, key_press): self.keys.append(key_press) self.processor = _ProcessorMock() self.stream = InputStream(self.processor.feed_key)
def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None self._inputstream = InputStream( lambda key: self.cli.input_processor.feed_key(key)) pymux.eventloop.add_reader(connection.fileno(), self._recv)
def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None self._inputstream = InputStream( lambda key: self.cli.input_processor.feed_key(key)) pymux.eventloop.add_reader( connection.fileno(), self._recv)
def run(self, stdin, callbacks): inputstream = InputStream(callbacks.feed_key) stdin_reader = PosixStdinReader(stdin.fileno()) self._callbacks = callbacks if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() select_timeout = INPUT_TIMEOUT with ctx: while self._running: r, _, _ = select.select([stdin.fileno(),self._schedule_pipe_read], [], [],select_timeout) if stdin.fileno() in r: select_timeout = INPUT_TIMEOUT data = stdin_reader.read() inputstream.feed(data) if stdin_reader.closed: break elif self._schedule_pipe_read in r: os.read(self._schedule_pipe_read,8192) while True: try: task = self._calls_from_executor.pop(0) except IndexError: break else: task() else: # timeout inputstream.flush() callbacks.input_timeout() select_timeout = None self._callbacks = None
def set_application(self, app, callback=None): """ Set ``CommandLineInterface`` instance for this connection. (This can be replaced any time.) :param cli: CommandLineInterface instance. :param callback: Callable that takes the result of the CLI. """ assert isinstance(app, Application) assert callback is None or callable(callback) self.cli = CommandLineInterface(application=app, eventloop=self.eventloop, output=self.vt100_output) self.callback = callback # Create a parser, and parser callbacks. cb = self.cli.create_eventloop_callbacks() inputstream = InputStream(cb.feed_key) # Input decoder for stdin. (Required when working with multibyte # characters, like chinese input.) stdin_decoder_cls = getincrementaldecoder(self.encoding) stdin_decoder = [stdin_decoder_cls()] # nonlocal # Tell the CLI that it's running. We don't start it through the run() # call, but will still want _redraw() to work. self.cli._is_running = True def data_received(data): """ TelnetProtocolParser 'data_received' callback """ assert isinstance(data, binary_type) try: result = stdin_decoder[0].decode(data) inputstream.feed(result) except UnicodeDecodeError: stdin_decoder[0] = stdin_decoder_cls() return '' def size_received(rows, columns): """ TelnetProtocolParser 'size_received' callback """ self.size = Size(rows=rows, columns=columns) cb.terminal_size_changed() self.parser = TelnetProtocolParser(data_received, size_received)
def set_cli(self, cli, callback=None): """ Set ``CommandLineInterface`` instance for this connection. (This can be replaced any time.) :param cli: CommandLineInterface instance. :param callback: Callable that takes the result of the CLI. """ assert isinstance(cli, CommandLineInterface) assert callback is None or callable(callback) self.cli = cli self.callback = callback # Replace the eventloop and renderer to integrate with the # telnet server. cli.eventloop = self.eventloop cli.renderer = Renderer(output=self.vt100_output) # Create a parser, and parser callbacks. cb = self.cli.create_eventloop_callbacks() inputstream = InputStream(cb.feed_key) # Input decoder for stdin. (Required when working with multibyte # characters, like chinese input.) stdin_decoder_cls = getincrementaldecoder(self.encoding) stdin_decoder = [stdin_decoder_cls()] # nonlocal def data_received(data): """ TelnetProtocolParser 'data_received' callback """ assert isinstance(data, binary_type) try: result = stdin_decoder[0].decode(data) inputstream.feed(result) except UnicodeDecodeError: stdin_decoder[0] = stdin_decoder_cls() return '' def size_received(rows, columns): """ TelnetProtocolParser 'size_received' callback """ self.size = Size(rows=rows, columns=columns) cb.terminal_size_changed() self.parser = TelnetProtocolParser(data_received, size_received)
def run(self, stdin, callbacks): inputstream = InputStream(callbacks.feed_key) stdin_reader = PosixStdinReader(stdin.fileno()) self._callbacks = callbacks if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() select_timeout = INPUT_TIMEOUT with ctx: while self._running: r, _, _ = select.select( [stdin.fileno(), self._schedule_pipe_read], [], [], select_timeout) if stdin.fileno() in r: select_timeout = INPUT_TIMEOUT data = stdin_reader.read() inputstream.feed(data) if stdin_reader.closed: break elif self._schedule_pipe_read in r: os.read(self._schedule_pipe_read, 8192) while True: try: task = self._calls_from_executor.pop(0) except IndexError: break else: task() else: # timeout inputstream.flush() callbacks.input_timeout() select_timeout = None self._callbacks = None
class ServerConnection(object): """ For each client that connects, we have one instance of this class. """ def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None self._inputstream = InputStream( lambda key: self.cli.input_processor.feed_key(key)) pymux.eventloop.add_reader( connection.fileno(), self._recv) def _recv(self): """ Data received from the client. (Parse it.) """ # Read next chunk. data = self.connection.recv(1024) if data == b'': # End of file. Close connection. self.detach_and_close() else: # Receive and process packets. self._recv_buffer += data while b'\0' in self._recv_buffer: # Zero indicates end of packet. pos = self._recv_buffer.index(b'\0') self._process(self._recv_buffer[:pos]) self._recv_buffer = self._recv_buffer[pos + 1:] def _process(self, data): """ Process packet received from client. """ packet = json.loads(data.decode('utf-8')) # Handle commands. if packet['cmd'] == 'run-command': self._run_command(packet) # Handle stdin. elif packet['cmd'] == 'in': self._inputstream.feed(packet['data']) elif packet['cmd'] == 'flush-input': self._inputstream.flush() # Flush escape key. # Set size. (The client reports the size.) elif packet['cmd'] == 'size': data = packet['data'] self.size = Size(rows=data[0], columns=data[1]) self.pymux.invalidate() # Start GUI. (Create CommandLineInterface front-end for pymux.) elif packet['cmd'] == 'start-gui': detach_other_clients = bool(packet['detach-others']) true_color = bool(packet['true-color']) if detach_other_clients: for c in self.pymux.connections: c.detach_and_close() self._create_cli(true_color=true_color) def _send_packet(self, data): """ Send packet to client. """ try: self.connection.send(json.dumps(data).encode('utf-8') + b'\0') except socket.error: if not self._closed: self.detach_and_close() def _run_command(self, packet): """ Execute a run command from the client. """ create_temp_cli = self.cli is None if create_temp_cli: # If this client doesn't have a CLI. Create a Fake CLI where the # window containing this pane, is the active one. (The CLI instance # will be removed before the render function is called, so it doesn't # hurt too much and makes the code easier.) pane_id = int(packet['pane_id']) self._create_cli() self.pymux.arrangement.set_active_window_from_pane_id(self.cli, pane_id) try: self.pymux.handle_command(self.cli, packet['data']) finally: self._close_cli() def _create_cli(self, true_color=False): """ Create CommandLineInterface for this client. Called when the client wants to attach the UI to the server. """ output = Vt100_Output(_SocketStdout(self._send_packet), lambda: self.size, true_color=true_color) input = _ClientInput(self._send_packet) self.cli = self.pymux.create_cli(self, output, input) def _close_cli(self): if self in self.pymux.clis: del self.pymux.clis[self] self.cli = None def suspend_client_to_background(self): """ Ask the client to suspend itself. (Like, when Ctrl-Z is pressed.) """ if self.cli: def suspend(): self._send_packet({'cmd': 'suspend'}) self.cli.run_in_terminal(suspend) def detach_and_close(self): # Remove from Pymux. self.pymux.connections.remove(self) # Remove from eventloop. self.pymux.eventloop.remove_reader(self.connection.fileno()) self.connection.close() self._closed = True
def stream(processor): from prompt_toolkit.terminal.vt100_input import InputStream return InputStream(processor.feed_key)
class ServerConnection(object): """ For each client that connects, we have one instance of this class. """ def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None self._inputstream = InputStream( lambda key: self.cli.input_processor.feed_key(key)) pymux.eventloop.add_reader(connection.fileno(), self._recv) def _recv(self): """ Data received from the client. (Parse it.) """ # Read next chunk. data = self.connection.recv(1024) if data == b'': # End of file. Close connection. self.detach_and_close() else: # Receive and process packets. self._recv_buffer += data while b'\0' in self._recv_buffer: # Zero indicates end of packet. pos = self._recv_buffer.index(b'\0') self._process(self._recv_buffer[:pos]) self._recv_buffer = self._recv_buffer[pos + 1:] def _process(self, data): """ Process packet received from client. """ packet = json.loads(data.decode('utf-8')) # Handle commands. if packet['cmd'] == 'run-command': self._run_command(packet) # Handle stdin. elif packet['cmd'] == 'in': self._inputstream.feed(packet['data']) elif packet['cmd'] == 'flush-input': self._inputstream.flush() # Flush escape key. # Set size. (The client reports the size.) elif packet['cmd'] == 'size': data = packet['data'] self.size = Size(rows=data[0], columns=data[1]) self.pymux.invalidate() # Start GUI. (Create CommandLineInterface front-end for pymux.) elif packet['cmd'] == 'start-gui': detach_other_clients = bool(packet['detach-others']) true_color = bool(packet['true-color']) if detach_other_clients: for c in self.pymux.connections: c.detach_and_close() self._create_cli(true_color=true_color) def _send_packet(self, data): """ Send packet to client. """ try: self.connection.send(json.dumps(data).encode('utf-8') + b'\0') except socket.error: if not self._closed: self.detach_and_close() def _run_command(self, packet): """ Execute a run command from the client. """ create_temp_cli = self.cli is None if create_temp_cli: # If this client doesn't have a CLI. Create a Fake CLI where the # window containing this pane, is the active one. (The CLI instance # will be removed before the render function is called, so it doesn't # hurt too much and makes the code easier.) pane_id = int(packet['pane_id']) self._create_cli() self.pymux.arrangement.set_active_window_from_pane_id( self.cli, pane_id) try: self.pymux.handle_command(self.cli, packet['data']) finally: self._close_cli() def _create_cli(self, true_color=False): """ Create CommandLineInterface for this client. Called when the client wants to attach the UI to the server. """ output = Vt100_Output(_SocketStdout(self._send_packet), lambda: self.size, true_color=true_color) input = _ClientInput(self._send_packet) self.cli = self.pymux.create_cli(self, output, input) def _close_cli(self): if self in self.pymux.clis: del self.pymux.clis[self] self.cli = None def suspend_client_to_background(self): """ Ask the client to suspend itself. (Like, when Ctrl-Z is pressed.) """ if self.cli: def suspend(): self._send_packet({'cmd': 'suspend'}) self.cli.run_in_terminal(suspend) def detach_and_close(self): # Remove from Pymux. self.pymux.connections.remove(self) # Remove from eventloop. self.pymux.eventloop.remove_reader(self.connection.fileno()) self.connection.close() self._closed = True
def run(self, stdin, callbacks): """ The input 'event loop'. """ assert isinstance(stdin, Input) assert isinstance(callbacks, EventLoopCallbacks) assert not self._running if self.closed: raise Exception('Event loop already closed.') self._running = True self._callbacks = callbacks inputstream = InputStream(callbacks.feed_key) current_timeout = [INPUT_TIMEOUT] # Nonlocal # Create reader class. stdin_reader = PosixStdinReader(stdin.fileno()) # Only attach SIGWINCH signal handler in main thread. # (It's not possible to attach signal handlers in other threads. In # that case we should rely on a the main thread to call this manually # instead.) if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() def read_from_stdin(): " Read user input. " # Feed input text. data = stdin_reader.read() inputstream.feed(data) # Set timeout again. current_timeout[0] = INPUT_TIMEOUT # Quit when the input stream was closed. if stdin_reader.closed: self.stop() self.add_reader(stdin, read_from_stdin) self.add_reader(self._schedule_pipe[0], None) with ctx: while self._running: # Call inputhook. with TimeIt() as inputhook_timer: if self._inputhook_context: def ready(wait): " True when there is input ready. The inputhook should return control. " return self._ready_for_reading(current_timeout[0] if wait else 0) != [] self._inputhook_context.call_inputhook(ready) # Calculate remaining timeout. (The inputhook consumed some of the time.) if current_timeout[0] is None: remaining_timeout = None else: remaining_timeout = max(0, current_timeout[0] - inputhook_timer.duration) # Wait until input is ready. fds = self._ready_for_reading(remaining_timeout) # When any of the FDs are ready. Call the appropriate callback. if fds: # Create lists of high/low priority tasks. The main reason # for this is to allow painting the UI to happen as soon as # possible, but when there are many events happening, we # don't want to call the UI renderer 1000x per second. If # the eventloop is completely saturated with many CPU # intensive tasks (like processing input/output), we say # that drawing the UI can be postponed a little, to make # CPU available. This will be a low priority task in that # case. tasks = [] low_priority_tasks = [] now = _now() for fd in fds: # For the 'call_from_executor' fd, put each pending # item on either the high or low priority queue. if fd == self._schedule_pipe[0]: for c, max_postpone_until in self._calls_from_executor: if max_postpone_until is None or max_postpone_until < now: tasks.append(c) else: low_priority_tasks.append((c, max_postpone_until)) self._calls_from_executor = [] # Flush all the pipe content. os.read(self._schedule_pipe[0], 1024) else: handler = self._read_fds.get(fd) if handler: tasks.append(handler) # Handle everything in random order. (To avoid starvation.) random.shuffle(tasks) random.shuffle(low_priority_tasks) # When there are high priority tasks, run all these. # Schedule low priority tasks for the next iteration. if tasks: for t in tasks: t() # Postpone low priority tasks. for t, max_postpone_until in low_priority_tasks: self.call_from_executor(t, _max_postpone_until=max_postpone_until) else: # Currently there are only low priority tasks -> run them right now. for t, _ in low_priority_tasks: t() else: # Flush all pending keys on a timeout. (This is most # important to flush the vt100 'Escape' key early when # nothing else follows.) inputstream.flush() # Fire input timeout event. callbacks.input_timeout() current_timeout[0] = None self.remove_reader(stdin) self.remove_reader(self._schedule_pipe[0]) self._callbacks = None
class ServerConnection(object): """ For each client that connects, we have one instance of this class. """ def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None def feed_key(key): self.cli.input_processor.feed(key) self.cli.input_processor.process_keys() self._inputstream = InputStream(feed_key) pymux.eventloop.add_reader(connection.fileno(), self._recv) def _recv(self): """ Data received from the client. (Parse it.) """ # Read next chunk. try: data = self.connection.recv(1024) except OSError as e: # On OSX, when we try to create a new window by typing "pymux # new-window" in a centain pane, very often we get the following # error: "OSError: [Errno 9] Bad file descriptor." # This doesn't seem very harmful, and we can just try again. logger.warning( 'Got OSError while reading data from client: %s. ' 'Trying again.', e) return if data == b'': # End of file. Close connection. self.detach_and_close() else: # Receive and process packets. self._recv_buffer += data while b'\0' in self._recv_buffer: # Zero indicates end of packet. pos = self._recv_buffer.index(b'\0') self._process(self._recv_buffer[:pos]) self._recv_buffer = self._recv_buffer[pos + 1:] def _process(self, data): """ Process packet received from client. """ try: packet = json.loads(data.decode('utf-8')) except ValueError: # So far, this never happened. But it would be good to have some # protection. logger.warning('Received invalid JSON from client. Ignoring.') return # Handle commands. if packet['cmd'] == 'run-command': self._run_command(packet) # Handle stdin. elif packet['cmd'] == 'in': self._inputstream.feed(packet['data']) elif packet['cmd'] == 'flush-input': self._inputstream.flush() # Flush escape key. # Set size. (The client reports the size.) elif packet['cmd'] == 'size': data = packet['data'] self.size = Size(rows=data[0], columns=data[1]) self.pymux.invalidate() # Start GUI. (Create CommandLineInterface front-end for pymux.) elif packet['cmd'] == 'start-gui': detach_other_clients = bool(packet['detach-others']) true_color = bool(packet['true-color']) ansi_colors_only = bool(packet['ansi-colors-only']) term = packet['term'] if detach_other_clients: for c in self.pymux.connections: c.detach_and_close() self._create_cli(true_color=true_color, ansi_colors_only=ansi_colors_only, term=term) def _send_packet(self, data): """ Send packet to client. """ try: self.connection.send(json.dumps(data).encode('utf-8') + b'\0') except socket.error: if not self._closed: self.detach_and_close() def _run_command(self, packet): """ Execute a run command from the client. """ create_temp_cli = self.cli is None if create_temp_cli: # If this client doesn't have a CLI. Create a Fake CLI where the # window containing this pane, is the active one. (The CLI instance # will be removed before the render function is called, so it doesn't # hurt too much and makes the code easier.) pane_id = int(packet['pane_id']) self._create_cli() self.pymux.arrangement.set_active_window_from_pane_id( self.cli, pane_id) try: self.pymux.handle_command(self.cli, packet['data']) finally: self._close_cli() def _create_cli(self, true_color=False, ansi_colors_only=False, term='xterm'): """ Create CommandLineInterface for this client. Called when the client wants to attach the UI to the server. """ output = Vt100_Output(_SocketStdout(self._send_packet), lambda: self.size, true_color=true_color, ansi_colors_only=ansi_colors_only, term=term, write_binary=False) input = _ClientInput(self._send_packet) self.cli = self.pymux.create_cli(self, output, input) def _close_cli(self): if self in self.pymux.clis: # This is important. If we would forget this, the server will # render CLI output for clients that aren't connected anymore. del self.pymux.clis[self] self.cli = None def suspend_client_to_background(self): """ Ask the client to suspend itself. (Like, when Ctrl-Z is pressed.) """ if self.cli: def suspend(): self._send_packet({'cmd': 'suspend'}) self.cli.run_in_terminal(suspend) def detach_and_close(self): # Remove from Pymux. self.pymux.connections.remove(self) self._close_cli() # Remove from eventloop. self.pymux.eventloop.remove_reader(self.connection.fileno()) self.connection.close() self._closed = True
def run(self, stdin, callbacks): """ The input 'event loop'. """ assert isinstance(stdin, Input) assert isinstance(callbacks, EventLoopCallbacks) assert not self._running if self.closed: raise Exception('Event loop already closed.') self._running = True self._callbacks = callbacks inputstream = InputStream(callbacks.feed_key) current_timeout = [INPUT_TIMEOUT] # Nonlocal # Create reader class. stdin_reader = PosixStdinReader(stdin.fileno()) # Only attach SIGWINCH signal handler in main thread. # (It's not possible to attach signal handlers in other threads. In # that case we should rely on a the main thread to call this manually # instead.) if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() def read_from_stdin(): " Read user input. " # Feed input text. data = stdin_reader.read() inputstream.feed(data) # Set timeout again. current_timeout[0] = INPUT_TIMEOUT # Quit when the input stream was closed. if stdin_reader.closed: self.stop() self.add_reader(stdin, read_from_stdin) self.add_reader(self._schedule_pipe[0], None) with ctx: while self._running: # Call inputhook. with TimeIt() as inputhook_timer: if self._inputhook_context: def ready(wait): " True when there is input ready. The inputhook should return control. " return self._ready_for_reading( current_timeout[0] if wait else 0) != [] self._inputhook_context.call_inputhook(ready) # Calculate remaining timeout. (The inputhook consumed some of the time.) if current_timeout[0] is None: remaining_timeout = None else: remaining_timeout = max( 0, current_timeout[0] - inputhook_timer.duration) # Wait until input is ready. fds = self._ready_for_reading(remaining_timeout) # When any of the FDs are ready. Call the appropriate callback. if fds: # Create lists of high/low priority tasks. The main reason # for this is to allow painting the UI to happen as soon as # possible, but when there are many events happening, we # don't want to call the UI renderer 1000x per second. If # the eventloop is completely saturated with many CPU # intensive tasks (like processing input/output), we say # that drawing the UI can be postponed a little, to make # CPU available. This will be a low priority task in that # case. tasks = [] low_priority_tasks = [] now = _now() for fd in fds: # For the 'call_from_executor' fd, put each pending # item on either the high or low priority queue. if fd == self._schedule_pipe[0]: for c, max_postpone_until in self._calls_from_executor: if max_postpone_until is None or max_postpone_until < now: tasks.append(c) else: low_priority_tasks.append( (c, max_postpone_until)) self._calls_from_executor = [] # Flush all the pipe content. os.read(self._schedule_pipe[0], 1024) else: handler = self._read_fds.get(fd) if handler: tasks.append(handler) # Handle everything in random order. (To avoid starvation.) random.shuffle(tasks) random.shuffle(low_priority_tasks) # When there are high priority tasks, run all these. # Schedule low priority tasks for the next iteration. if tasks: for t in tasks: t() # Postpone low priority tasks. for t, max_postpone_until in low_priority_tasks: self.call_from_executor( t, _max_postpone_until=max_postpone_until) else: # Currently there are only low priority tasks -> run them right now. for t, _ in low_priority_tasks: t() else: # Flush all pending keys on a timeout. (This is most # important to flush the vt100 'Escape' key early when # nothing else follows.) inputstream.flush() # Fire input timeout event. callbacks.input_timeout() current_timeout[0] = None self.remove_reader(stdin) self.remove_reader(self._schedule_pipe[0]) self._callbacks = None
def run(self, stdin, callbacks): """ The input 'event loop'. """ assert isinstance(callbacks, EventLoopCallbacks) assert not self._running if self.closed: raise Exception('Event loop already closed.') self._running = True self._callbacks = callbacks inputstream = InputStream(callbacks.feed_key) current_timeout = INPUT_TIMEOUT # Create reader class. stdin_reader = PosixStdinReader(stdin) # Only attach SIGWINCH signal handler in main thread. # (It's not possible to attach signal handlers in other threads. In # that case we should rely on a the main thread to call this manually # instead.) if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() with ctx: while self._running: # Call inputhook. with TimeIt() as inputhook_timer: if self._inputhook_context: def ready(wait): " True when there is input ready. The inputhook should return control. " return self._ready_for_reading(stdin, current_timeout if wait else 0) != [] self._inputhook_context.call_inputhook(ready) # Calculate remaining timeout. (The inputhook consumed some of the time.) if current_timeout is None: remaining_timeout = None else: remaining_timeout = max(0, current_timeout - inputhook_timer.duration) # Wait until input is ready. r = self._ready_for_reading(stdin, remaining_timeout) # If we got a character, feed it to the input stream. If we got # none, it means we got a repaint request. if stdin in r: # Feed input text. data = stdin_reader.read() inputstream.feed(data) callbacks.redraw() # Set timeout again. current_timeout = INPUT_TIMEOUT # If we receive something on our "call_from_executor" pipe, process # these callbacks in a thread safe way. elif self._schedule_pipe[0] in r: # Flush all the pipe content. os.read(self._schedule_pipe[0], 1024) # Process calls from executor. calls_from_executor, self._calls_from_executor = self._calls_from_executor, [] for c in calls_from_executor: c() else: # Flush all pending keys on a timeout and redraw. (This is # most important to flush the vt100 escape key early when # nothing else follows.) inputstream.flush() callbacks.redraw() # Fire input timeout event. callbacks.input_timeout() current_timeout = None self._callbacks = None
def run(self, stdin, callbacks): """ The input 'event loop'. """ assert isinstance(stdin, Input) assert isinstance(callbacks, EventLoopCallbacks) assert not self._running if self.closed: raise Exception('Event loop already closed.') self._running = True self._callbacks = callbacks inputstream = InputStream(callbacks.feed_key) current_timeout = INPUT_TIMEOUT # Create reader class. stdin_reader = PosixStdinReader(stdin.fileno()) # Only attach SIGWINCH signal handler in main thread. # (It's not possible to attach signal handlers in other threads. In # that case we should rely on a the main thread to call this manually # instead.) if in_main_thread(): ctx = call_on_sigwinch(self.received_winch) else: ctx = DummyContext() with ctx: while self._running: # Call inputhook. with TimeIt() as inputhook_timer: if self._inputhook_context: def ready(wait): " True when there is input ready. The inputhook should return control. " return self._ready_for_reading( stdin, current_timeout if wait else 0) != [] self._inputhook_context.call_inputhook(ready) # Calculate remaining timeout. (The inputhook consumed some of the time.) if current_timeout is None: remaining_timeout = None else: remaining_timeout = max( 0, current_timeout - inputhook_timer.duration) # Wait until input is ready. r = self._ready_for_reading(stdin, remaining_timeout) # If we got a character, feed it to the input stream. If we got # none, it means we got a repaint request. if stdin in r: # Feed input text. data = stdin_reader.read() inputstream.feed(data) # Set timeout again. current_timeout = INPUT_TIMEOUT # When any of the registered read pipes are ready. Call the # appropriate callback. elif set(r) & set(self._read_fds): for fd in r: handler = self._read_fds.get(fd) if handler: handler() # If we receive something on our "call_from_executor" pipe, process # these callbacks in a thread safe way. elif self._schedule_pipe[0] in r: # Flush all the pipe content. os.read(self._schedule_pipe[0], 1024) # Process calls from executor. calls_from_executor, self._calls_from_executor = self._calls_from_executor, [] for c in calls_from_executor: c() else: # Flush all pending keys on a timeout. (This is most # important to flush the vt100 'Escape' key early when # nothing else follows.) inputstream.flush() # Fire input timeout event. callbacks.input_timeout() current_timeout = None self._callbacks = None
def stream(processor): return InputStream(processor.feed_key)
class InputStreamTest(unittest.TestCase): def setUp(self): class _ProcessorMock(object): def __init__(self): self.keys = [] def feed_key(self, key_press): self.keys.append(key_press) self.processor = _ProcessorMock() self.stream = InputStream(self.processor.feed_key) def test_control_keys(self): self.stream.feed('\x01\x02\x10') self.assertEqual(len(self.processor.keys), 3) self.assertEqual(self.processor.keys[0].key, Keys.ControlA) self.assertEqual(self.processor.keys[1].key, Keys.ControlB) self.assertEqual(self.processor.keys[2].key, Keys.ControlP) self.assertEqual(self.processor.keys[0].data, '\x01') self.assertEqual(self.processor.keys[1].data, '\x02') self.assertEqual(self.processor.keys[2].data, '\x10') def test_arrows(self): self.stream.feed('\x1b[A\x1b[B\x1b[C\x1b[D') self.assertEqual(len(self.processor.keys), 4) self.assertEqual(self.processor.keys[0].key, Keys.Up) self.assertEqual(self.processor.keys[1].key, Keys.Down) self.assertEqual(self.processor.keys[2].key, Keys.Right) self.assertEqual(self.processor.keys[3].key, Keys.Left) self.assertEqual(self.processor.keys[0].data, '\x1b[A') self.assertEqual(self.processor.keys[1].data, '\x1b[B') self.assertEqual(self.processor.keys[2].data, '\x1b[C') self.assertEqual(self.processor.keys[3].data, '\x1b[D') def test_escape(self): self.stream.feed('\x1bhello') self.assertEqual(len(self.processor.keys), 1 + len('hello')) self.assertEqual(self.processor.keys[0].key, Keys.Escape) self.assertEqual(self.processor.keys[1].key, 'h') self.assertEqual(self.processor.keys[0].data, '\x1b') self.assertEqual(self.processor.keys[1].data, 'h') def test_special_double_keys(self): self.stream.feed('\x1b[1;3D') # Should both send escape and left. self.assertEqual(len(self.processor.keys), 2) self.assertEqual(self.processor.keys[0].key, Keys.Escape) self.assertEqual(self.processor.keys[1].key, Keys.Left) self.assertEqual(self.processor.keys[0].data, '\x1b[1;3D') self.assertEqual(self.processor.keys[1].data, '\x1b[1;3D') def test_flush_1(self): # Send left key in two parts without flush. self.stream.feed('\x1b') self.stream.feed('[D') self.assertEqual(len(self.processor.keys), 1) self.assertEqual(self.processor.keys[0].key, Keys.Left) self.assertEqual(self.processor.keys[0].data, '\x1b[D') def test_flush_2(self): # Send left key with a 'Flush' in between. # The flush should make sure that we process evenything before as-is, # with makes the first part just an escape character instead. self.stream.feed('\x1b') self.stream.flush() self.stream.feed('[D') self.assertEqual(len(self.processor.keys), 3) self.assertEqual(self.processor.keys[0].key, Keys.Escape) self.assertEqual(self.processor.keys[1].key, '[') self.assertEqual(self.processor.keys[2].key, 'D') self.assertEqual(self.processor.keys[0].data, '\x1b') self.assertEqual(self.processor.keys[1].data, '[') self.assertEqual(self.processor.keys[2].data, 'D') def test_meta_arrows(self): self.stream.feed('\x1b\x1b[D') self.assertEqual(len(self.processor.keys), 2) self.assertEqual(self.processor.keys[0].key, Keys.Escape) self.assertEqual(self.processor.keys[1].key, Keys.Left) def test_control_square_close(self): self.stream.feed('\x1dC') self.assertEqual(len(self.processor.keys), 2) self.assertEqual(self.processor.keys[0].key, Keys.ControlSquareClose) self.assertEqual(self.processor.keys[1].key, 'C') def test_invalid(self): # Invalid sequence that has at two characters in common with other # sequences. self.stream.feed('\x1b[*') self.assertEqual(len(self.processor.keys), 3) self.assertEqual(self.processor.keys[0].key, Keys.Escape) self.assertEqual(self.processor.keys[1].key, '[') self.assertEqual(self.processor.keys[2].key, '*') def test_cpr_response(self): self.stream.feed('a\x1b[40;10Rb') self.assertEqual(len(self.processor.keys), 3) self.assertEqual(self.processor.keys[0].key, 'a') self.assertEqual(self.processor.keys[1].key, Keys.CPRResponse) self.assertEqual(self.processor.keys[2].key, 'b') def test_cpr_response_2(self): # Make sure that the newline is not included in the CPR response. self.stream.feed('\x1b[40;1R\n') self.assertEqual(len(self.processor.keys), 2) self.assertEqual(self.processor.keys[0].key, Keys.CPRResponse) self.assertEqual(self.processor.keys[1].key, Keys.ControlJ)
class ServerConnection(object): """ For each client that connects, we have one instance of this class. """ def __init__(self, pymux, connection, client_address): self.pymux = pymux self.connection = connection self.client_address = client_address self.size = Size(rows=20, columns=80) self._closed = False self._recv_buffer = b'' self.cli = None def feed_key(key): self.cli.input_processor.feed(key) self.cli.input_processor.process_keys() self._inputstream = InputStream(feed_key) pymux.eventloop.add_reader( connection.fileno(), self._recv) def _recv(self): """ Data received from the client. (Parse it.) """ # Read next chunk. try: data = self.connection.recv(1024) except OSError as e: # On OSX, when we try to create a new window by typing "pymux # new-window" in a centain pane, very often we get the following # error: "OSError: [Errno 9] Bad file descriptor." # This doesn't seem very harmful, and we can just try again. logger.warning('Got OSError while reading data from client: %s. ' 'Trying again.', e) return if data == b'': # End of file. Close connection. self.detach_and_close() else: # Receive and process packets. self._recv_buffer += data while b'\0' in self._recv_buffer: # Zero indicates end of packet. pos = self._recv_buffer.index(b'\0') self._process(self._recv_buffer[:pos]) self._recv_buffer = self._recv_buffer[pos + 1:] def _process(self, data): """ Process packet received from client. """ try: packet = json.loads(data.decode('utf-8')) except ValueError: # So far, this never happened. But it would be good to have some # protection. logger.warning('Received invalid JSON from client. Ignoring.') return # Handle commands. if packet['cmd'] == 'run-command': self._run_command(packet) # Handle stdin. elif packet['cmd'] == 'in': self._inputstream.feed(packet['data']) elif packet['cmd'] == 'flush-input': self._inputstream.flush() # Flush escape key. # Set size. (The client reports the size.) elif packet['cmd'] == 'size': data = packet['data'] self.size = Size(rows=data[0], columns=data[1]) self.pymux.invalidate() # Start GUI. (Create CommandLineInterface front-end for pymux.) elif packet['cmd'] == 'start-gui': detach_other_clients = bool(packet['detach-others']) true_color = bool(packet['true-color']) ansi_colors_only = bool(packet['ansi-colors-only']) term = packet['term'] if detach_other_clients: for c in self.pymux.connections: c.detach_and_close() self._create_cli(true_color=true_color, ansi_colors_only=ansi_colors_only, term=term) def _send_packet(self, data): """ Send packet to client. """ try: self.connection.send(json.dumps(data).encode('utf-8') + b'\0') except socket.error: if not self._closed: self.detach_and_close() def _run_command(self, packet): """ Execute a run command from the client. """ create_temp_cli = self.cli is None if create_temp_cli: # If this client doesn't have a CLI. Create a Fake CLI where the # window containing this pane, is the active one. (The CLI instance # will be removed before the render function is called, so it doesn't # hurt too much and makes the code easier.) pane_id = int(packet['pane_id']) self._create_cli() self.pymux.arrangement.set_active_window_from_pane_id(self.cli, pane_id) try: self.pymux.handle_command(self.cli, packet['data']) finally: self._close_cli() def _create_cli(self, true_color=False, ansi_colors_only=False, term='xterm'): """ Create CommandLineInterface for this client. Called when the client wants to attach the UI to the server. """ output = Vt100_Output(_SocketStdout(self._send_packet), lambda: self.size, true_color=true_color, ansi_colors_only=ansi_colors_only, term=term, write_binary=False) input = _ClientInput(self._send_packet) self.cli = self.pymux.create_cli(self, output, input) def _close_cli(self): if self in self.pymux.clis: # This is important. If we would forget this, the server will # render CLI output for clients that aren't connected anymore. del self.pymux.clis[self] self.cli = None def suspend_client_to_background(self): """ Ask the client to suspend itself. (Like, when Ctrl-Z is pressed.) """ if self.cli: def suspend(): self._send_packet({'cmd': 'suspend'}) self.cli.run_in_terminal(suspend) def detach_and_close(self): # Remove from Pymux. self.pymux.connections.remove(self) self._close_cli() # Remove from eventloop. self.pymux.eventloop.remove_reader(self.connection.fileno()) self.connection.close() self._closed = True