Example #1
0
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)
Example #3
0
    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)
Example #4
0
    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)
Example #5
0
    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
Example #6
0
    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)
Example #7
0
    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)
Example #8
0
    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
Example #9
0
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
Example #10
0
def stream(processor):
    from prompt_toolkit.terminal.vt100_input import InputStream
    return InputStream(processor.feed_key)
Example #11
0
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
Example #12
0
    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
Example #13
0
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
Example #14
0
    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
Example #15
0
    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
Example #16
0
    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)
Example #19
0
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