Esempio n. 1
0
    def __init__(self, eventloop, invalidate, exec_func, bell_func=None,
                 done_callback=None, has_priority=None):
        assert isinstance(eventloop, EventLoop)
        assert callable(invalidate)
        assert callable(exec_func)
        assert bell_func is None or callable(bell_func)
        assert done_callback is None or callable(done_callback)
        assert has_priority is None or callable(has_priority)

        self.eventloop = eventloop
        self.invalidate = invalidate
        self.exec_func = exec_func
        self.done_callback = done_callback
        self.has_priority = has_priority or (lambda: True)

        self.pid = None
        self.is_terminated = False
        self.suspended = False

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master)

        # Create output stream and attach to screen
        self.sx = 0
        self.sy = 0

        self.screen = BetterScreen(self.sx, self.sy,
                                   write_process_input=self.write_input,
                                   bell_func=bell_func)

        self.stream = BetterStream(self.screen)
        self.stream.attach(self.screen)
Esempio n. 2
0
    def __init__(self,
                 eventloop,
                 invalidate,
                 exec_func,
                 bell_func=None,
                 done_callback=None,
                 has_priority=None):
        assert isinstance(eventloop, EventLoop)
        assert callable(invalidate)
        assert callable(exec_func)
        assert bell_func is None or callable(bell_func)
        assert done_callback is None or callable(done_callback)
        assert has_priority is None or callable(has_priority)

        self.eventloop = eventloop
        self.invalidate = invalidate
        self.exec_func = exec_func
        self.done_callback = done_callback
        self.has_priority = has_priority or (lambda: True)

        self.pid = None
        self.is_terminated = False
        self.suspended = False
        self._reader_connected = False

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master, errors='replace')

        # Create output stream and attach to screen
        self.sx = 0
        self.sy = 0

        self.screen = BetterScreen(self.sx,
                                   self.sy,
                                   write_process_input=self.write_input,
                                   bell_func=bell_func)

        self.stream = BetterStream(self.screen)
        self.stream.attach(self.screen)
Esempio n. 3
0
    def __init__(self, socket_name):
        self.socket_name = socket_name
        self._mode_context_managers = []

        # Connect to socket.
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.socket.connect(socket_name)
        self.socket.setblocking(1)

        # Input reader.
        #     Some terminals, like lxterminal send non UTF-8 input sequences,
        #     even when the input encoding is supposed to be UTF-8. This
        #     happens in the case of mouse clicks in the right area of a wide
        #     terminal. Apparently, these are some binary blobs in between the
        #     UTF-8 input.)
        #     We should not replace these, because this would break the
        #     decoding otherwise. (Also don't pass errors='ignore', because
        #     that doesn't work for parsing mouse input escape sequences, which
        #     consist of a fixed number of bytes.)
        self._stdin_reader = PosixStdinReader(sys.stdin.fileno(),
                                              errors='replace')
Esempio n. 4
0
    def __init__(self, fileno, lexer=None, name='<stdin>'):
        assert isinstance(fileno, int)
        assert lexer is None or isinstance(lexer, Lexer)
        assert isinstance(name, six.text_type)

        self.fileno = fileno
        self.lexer = lexer
        self.name = name

        self._line_tokens = []
        self._eof = False

        # Default style attributes.
        self._attrs = Attrs(
            color=None, bgcolor=None, bold=False, underline=False,
            italic=False, blink=False, reverse=False)

        # Start input parser.
        self._parser = self._parse_corot()
        next(self._parser)
        self._stdin_reader = PosixStdinReader(fileno)
Esempio n. 5
0
    def __init__(self,
                 eventloop,
                 invalidate,
                 exec_func,
                 bell_func=None,
                 done_callback=None):
        assert isinstance(eventloop, EventLoop)
        assert callable(invalidate)
        assert callable(exec_func)
        assert bell_func is None or callable(bell_func)
        assert done_callback is None or callable(done_callback)

        self.eventloop = eventloop
        self.invalidate = invalidate
        self.exec_func = exec_func
        self.done_callback = done_callback
        self.pid = None
        self.is_terminated = False
        self.suspended = False
        self.slow_motion = False  # For debugging

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master)

        # Create output stream and attach to screen
        self.sx = 120
        self.sy = 24

        self.screen = BetterScreen(self.sx,
                                   self.sy,
                                   write_process_input=self.write_input,
                                   bell_func=bell_func)

        self.stream = BetterStream(self.screen)
        self.stream.attach(self.screen)
Esempio n. 6
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
Esempio n. 7
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
Esempio n. 8
0
    def __init__(self, socket_name):
        self.socket_name = socket_name
        self._mode_context_managers = []

        # Connect to socket.
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.socket.connect(socket_name)
        self.socket.setblocking(1)

        # Input reader.
        #     Some terminals, like lxterminal send non UTF-8 input sequences,
        #     even when the input encoding is supposed to be UTF-8. This
        #     happens in the case of mouse clicks in the right area of a wide
        #     terminal. Apparently, these are some binary blobs in between the
        #     UTF-8 input.)
        #     We should not replace these, because this would break the
        #     decoding otherwise. (Also don't pass errors='ignore', because
        #     that doesn't work for parsing mouse input escape sequences, which
        #     consist of a fixed number of bytes.)
        self._stdin_reader = PosixStdinReader(sys.stdin.fileno(), errors='replace')
Esempio n. 9
0
class Process(object):
    """
    Child process.
    Functionality for parsing the vt100 output (the Pyte screen and stream), as
    well as sending input to the process.

    Usage:

        p = Process(eventloop, ...):
        p.start()

    :param eventloop: Prompt_toolkit eventloop. Used for executing blocking
        stuff in an executor, as well as adding additional readers to the
        eventloop.
    :param invalidate: When the screen content changes, and the renderer needs
        to redraw the output, this callback is called.
    :param exec_func: Callable that is called in the child process. (Usualy,
        this calls execv.)
    :param bell_func: Called when the process does a `bell`.
    :param done_callback: Called when the process terminates.
    :param has_priority: Callable that returns True when this Process should
        get priority in the event loop. (When this pane has the focus.)
        Otherwise output can be delayed.
    """
    def __init__(self,
                 eventloop,
                 invalidate,
                 exec_func,
                 bell_func=None,
                 done_callback=None,
                 has_priority=None):
        assert isinstance(eventloop, EventLoop)
        assert callable(invalidate)
        assert callable(exec_func)
        assert bell_func is None or callable(bell_func)
        assert done_callback is None or callable(done_callback)
        assert has_priority is None or callable(has_priority)

        self.eventloop = eventloop
        self.invalidate = invalidate
        self.exec_func = exec_func
        self.done_callback = done_callback
        self.has_priority = has_priority or (lambda: True)

        self.pid = None
        self.is_terminated = False
        self.suspended = False

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master)

        # Create output stream and attach to screen
        self.sx = 0
        self.sy = 0

        self.screen = BetterScreen(self.sx,
                                   self.sy,
                                   write_process_input=self.write_input,
                                   bell_func=bell_func)

        self.stream = BetterStream(self.screen)
        self.stream.attach(self.screen)

    def start(self):
        """
        Start the process: fork child.
        """
        self.set_size(120, 24)
        self._start()
        self._connect_reader()
        self._waitpid()

    @classmethod
    def from_command(cls,
                     eventloop,
                     invalidate,
                     command,
                     done_callback,
                     bell_func=None,
                     before_exec_func=None,
                     has_priority=None):
        """
        Create Process from command,
        e.g. command=['python', '-c', 'print("test")']

        :param before_exec_func: Function that is called before `exec` in the process fork.
        """
        assert isinstance(command, list)

        def execv():
            if before_exec_func:
                before_exec_func()

            for p in os.environ['PATH'].split(':'):
                path = os.path.join(p, command[0])
                if os.path.exists(path) and os.access(path, os.X_OK):
                    os.execv(path, command)

        return cls(eventloop,
                   invalidate,
                   execv,
                   bell_func=bell_func,
                   done_callback=done_callback,
                   has_priority=has_priority)

    def _start(self):
        """
        Create fork and start the child process.
        """
        pid = os.fork()

        if pid == 0:
            self._in_child()
        elif pid > 0:
            # In parent.
            os.close(self.slave)
            self.slave = None

            # We wait a very short while, to be sure the child had the time to
            # call _exec. (Otherwise, we are still sharing signal handlers and
            # FDs.) Resizing the pty, when the child is still in our Python
            # code and has the signal handler from prompt_toolkit, but closed
            # the 'fd' for 'call_from_executor', will cause OSError.
            time.sleep(0.1)

            self.pid = pid

    def _waitpid(self):
        """
        Create an executor that waits and handles process termination.
        """
        def wait_for_finished():
            " Wait for PID in executor. "
            os.waitpid(self.pid, 0)
            self.eventloop.call_from_executor(done)

        def done():
            " PID received. Back in the main thread. "
            # Close pty and remove reader.
            os.close(self.master)
            self.eventloop.remove_reader(self.master)
            self.master = None

            # Callback.
            self.is_terminated = True
            self.done_callback()

        self.eventloop.run_in_executor(wait_for_finished)

    def set_size(self, width, height):
        """
        Set terminal size.
        """
        assert isinstance(width, int)
        assert isinstance(height, int)

        if self.master is not None:
            if (self.sx, self.sy) != (width, height):
                set_terminal_size(self.master, height, width)
        self.screen.resize(lines=height, columns=width)

        self.screen.lines = height
        self.screen.columns = width

        self.sx = width
        self.sy = height

    def _in_child(self):
        " Will be executed in the forked child. "
        os.close(self.master)

        # Remove signal handler for SIGWINCH as early as possible.
        # (We don't want this to be triggered when execv has not been called
        # yet.)
        signal.signal(signal.SIGWINCH, 0)

        pty_make_controlling_tty(self.slave)

        # In the fork, set the stdin/out/err to our slave pty.
        os.dup2(self.slave, 0)
        os.dup2(self.slave, 1)
        os.dup2(self.slave, 2)

        # Execute in child.
        try:
            self._close_file_descriptors()
            self.exec_func()
        except Exception:
            traceback.print_exc()
            time.sleep(5)

            os._exit(1)
        os._exit(0)

    def _close_file_descriptors(self):
        # Do not allow child to inherit open file descriptors from parent.
        # (In case that we keep running Python code. We shouldn't close them.
        # because the garbage collector is still active, and he will close them
        # eventually.)
        max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[-1]

        try:
            os.closerange(3, max_fd)
        except OverflowError:
            # On OS X, max_fd can return very big values, than closerange
            # doesn't understand, e.g. 9223372036854775807. In this case, just
            # use 4096. This is what Linux systems report, and should be
            # sufficient. (I hope...)
            os.closerange(3, 4096)

    def write_input(self, data, paste=False):
        """
        Write user key strokes to the input.

        :param data: (text, not bytes.) The input.
        :param paste: When True, and the process running here understands
            bracketed paste. Send as pasted text.
        """
        # send as bracketed paste?
        if paste and self.screen.bracketed_paste_enabled:
            data = '\x1b[200~' + data + '\x1b[201~'

        while self.master is not None:
            try:
                os.write(self.master, data.encode('utf-8'))
            except OSError as e:
                # This happens when the window resizes and a SIGWINCH was received.
                # We get 'Error: [Errno 4] Interrupted system call'
                if e.errno == 4:
                    continue
            return

    def write_key(self, key):
        """
        Write prompt_toolkit Key.
        """
        data = prompt_toolkit_key_to_vt100_key(
            key, application_mode=self.screen.in_application_mode)
        self.write_input(data)

    def _connect_reader(self):
        """
        Process stdout output from the process.
        """
        if self.master is not None:
            self.eventloop.add_reader(self.master, self._read)

    def _read(self):
        """
        Read callback, called by the eventloop.
        """
        d = self._reader.read(
            4096)  # Make sure not to read too much at once. (Otherwise,
        # this could block the event loop.)

        if d:

            def process():
                self.stream.feed(d)
                self.invalidate()

            # Feed directly, if this process has priority. (That is when this
            # pane has the focus in any of the clients.)
            if self.has_priority():
                process()

            # Otherwise, postpone processing until we have CPU time available.
            else:
                self.eventloop.remove_reader(self.master)

                def do_asap():
                    " Process output and reconnect to event loop. "
                    process()
                    self._connect_reader()

                # When the event loop is saturated because of CPU, we will
                # postpone this processing max 'x' seconds.

                # '1' seems like a reasonable value, because that way we say
                # that we will process max 1k/1s in case of saturation.
                # That should be enough to prevent the UI from feeling
                # unresponsive.
                timestamp = datetime.datetime.now() + datetime.timedelta(
                    seconds=1)

                self.eventloop.call_from_executor(
                    do_asap, _max_postpone_until=timestamp)
        else:
            # End of stream. Remove child.
            self.eventloop.remove_reader(self.master)

    def suspend(self):
        """
        Suspend process. Stop reading stdout. (Called when going into copy mode.)
        """
        self.suspended = True
        self.eventloop.remove_reader(self.master)

    def resume(self):
        """
        Resume from 'suspend'.
        """
        if self.suspended and self.master is not None:
            self._connect_reader()
            self.suspended = False

    def get_cwd(self):
        """
        The current working directory for this process. (Or `None` when
        unknown.)
        """
        return get_cwd_for_pid(self.pid)

    def get_name(self):
        """
        The name for this process. (Or `None` when unknown.)
        """
        # TODO: Maybe cache for short time.
        if self.master is not None:
            return get_name_for_fd(self.master)

    def send_signal(self, signal):
        " Send signal to running process. "
        assert isinstance(signal, int), type(signal)

        if self.pid and not self.is_terminated:
            os.kill(self.pid, signal)

    def create_copy_document(self):
        """
        Create a Document instance and token list that can be used in copy
        mode.
        """
        data_buffer = self.screen.pt_screen.data_buffer
        text = []
        token_list = []

        first_row = min(data_buffer.keys())
        last_row = max(data_buffer.keys())

        def token_has_no_background(token):
            try:
                # Token looks like ('C', color, bgcolor, bold, underline, ...)
                return token[2] is None
            except IndexError:
                return True

        for row_index in range(first_row, last_row + 1):
            row = data_buffer[row_index]
            max_column = max(row.keys()) if row else 0

            # Remove trailing whitespace. (If the background is transparent.)
            row_data = [row[x] for x in range(0, max_column + 1)]

            while (row_data and row_data[-1].char.isspace()
                   and token_has_no_background(row_data[-1].token)):
                row_data.pop()

            # Walk through row.
            char_iter = iter(range(len(row_data)))

            for x in char_iter:
                c = row[x]
                text.append(c.char)
                token_list.append((c.token, c.char))

                # Skip next cell when this is a double width character.
                if c.width == 2:
                    next(char_iter)

            # Add newline.
            text.append('\n')
            token_list.append((Token, '\n'))

        # Remove newlines at the end.
        while text and text[-1] == '\n':
            text.pop()
            token_list.pop()

        # Calculate cursor position.
        d = Document(text=''.join(text))

        return Document(
            text=d.text,
            cursor_position=d.translate_row_col_to_index(
                row=self.screen.pt_screen.cursor_position.y,
                col=self.screen.pt_screen.cursor_position.x)), token_list
Esempio n. 10
0
class Client(object):
    def __init__(self, socket_name):
        self.socket_name = socket_name
        self._mode_context_managers = []

        # Connect to socket.
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.socket.connect(socket_name)
        self.socket.setblocking(1)

        # Input reader.
        #     Some terminals, like lxterminal send non UTF-8 input sequences,
        #     even when the input encoding is supposed to be UTF-8. This
        #     happens in the case of mouse clicks in the right area of a wide
        #     terminal. Apparently, these are some binary blobs in between the
        #     UTF-8 input.)
        #     We should not replace these, because this would break the
        #     decoding otherwise. (Also don't pass errors='ignore', because
        #     that doesn't work for parsing mouse input escape sequences, which
        #     consist of a fixed number of bytes.)
        self._stdin_reader = PosixStdinReader(sys.stdin.fileno(),
                                              errors='replace')

    def run_command(self, command, pane_id=None):
        """
        Ask the server to run this command.

        :param pane_id: Optional identifier of the current pane.
        """
        self._send_packet({
            'cmd': 'run-command',
            'data': command,
            'pane_id': pane_id
        })

    def attach(self, detach_other_clients=False, true_color=False):
        """
        Attach client user interface.
        """
        assert isinstance(detach_other_clients, bool)
        assert isinstance(true_color, bool)

        self._send_size()
        self._send_packet({
            'cmd': 'start-gui',
            'detach-others': detach_other_clients,
            'true-color': true_color,
            'term': os.environ.get('TERM', ''),
            'data': ''
        })

        with raw_mode(sys.stdin.fileno()):
            data_buffer = b''

            stdin_fd = sys.stdin.fileno()
            socket_fd = self.socket.fileno()
            current_timeout = INPUT_TIMEOUT  # Timeout, used to flush escape sequences.

            with call_on_sigwinch(self._send_size):
                while True:
                    r, w, x = _select([stdin_fd, socket_fd], [], [],
                                      current_timeout)

                    if socket_fd in r:
                        # Received packet from server.
                        data = self.socket.recv(1024)

                        if data == b'':
                            # End of file. Connection closed.
                            # Reset terminal
                            o = Vt100_Output.from_pty(sys.stdout)
                            o.quit_alternate_screen()
                            o.disable_mouse_support()
                            o.disable_bracketed_paste()
                            o.reset_attributes()
                            o.flush()
                            return
                        else:
                            data_buffer += data

                            while b'\0' in data_buffer:
                                pos = data_buffer.index(b'\0')
                                self._process(data_buffer[:pos])
                                data_buffer = data_buffer[pos + 1:]

                    elif stdin_fd in r:
                        # Got user input.
                        self._process_stdin()
                        current_timeout = INPUT_TIMEOUT

                    else:
                        # Timeout. (Tell the server to flush the vt100 Escape.)
                        self._send_packet({'cmd': 'flush-input'})
                        current_timeout = None

    def _process(self, data_buffer):
        """
        Handle incoming packet from server.
        """
        packet = json.loads(data_buffer.decode('utf-8'))

        if packet['cmd'] == 'out':
            # Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8.
            os.write(sys.stdout.fileno(), packet['data'].encode('utf-8'))

        elif packet['cmd'] == 'suspend':
            # Suspend client process to background.
            if hasattr(signal, 'SIGTSTP'):
                os.kill(os.getpid(), signal.SIGTSTP)

        elif packet['cmd'] == 'mode':
            # Set terminal to raw/cooked.
            action = packet['data']

            if action == 'raw':
                cm = raw_mode(sys.stdin.fileno())
                cm.__enter__()
                self._mode_context_managers.append(cm)

            elif action == 'cooked':
                cm = cooked_mode(sys.stdin.fileno())
                cm.__enter__()
                self._mode_context_managers.append(cm)

            elif action == 'restore' and self._mode_context_managers:
                cm = self._mode_context_managers.pop()
                cm.__exit__()

    def _process_stdin(self):
        """
        Received data on stdin. Read and send to server.
        """
        with nonblocking(sys.stdin.fileno()):
            data = self._stdin_reader.read()

        # Send input in chunks of 4k.
        step = 4056
        for i in range(0, len(data), step):
            self._send_packet({
                'cmd': 'in',
                'data': data[i:i + step],
            })

    def _send_packet(self, data):
        " Send to server. "
        data = json.dumps(data).encode('utf-8')

        # Be sure that our socket is blocking, otherwise, the send() call could
        # raise `BlockingIOError` if the buffer is full.
        self.socket.setblocking(1)

        self.socket.send(data + b'\0')

    def _send_size(self):
        " Report terminal size to server. "
        rows, cols = _get_size(sys.stdout.fileno())
        self._send_packet({'cmd': 'size', 'data': [rows, cols]})
Esempio n. 11
0
class Process(object):
    """
    Child process.
    Functionality for parsing the vt100 output (the Pyte screen and stream), as
    well as sending input to the process.

    Usage:

        p = Process(eventloop, ...):
        p.start()

    :param eventloop: Prompt_toolkit eventloop. Used for executing blocking
        stuff in an executor, as well as adding additional readers to the
        eventloop.
    :param invalidate: When the screen content changes, and the renderer needs
        to redraw the output, this callback is called.
    :param exec_func: Callable that is called in the child process. (Usualy,
        this calls execv.)
    :param bell_func: Called when the process does a `bell`.
    :param done_callback: Called when the process terminates.
    :param has_priority: Callable that returns True when this Process should
        get priority in the event loop. (When this pane has the focus.)
        Otherwise output can be delayed.
    """

    def __init__(self,
                 eventloop,
                 invalidate,
                 exec_func,
                 bell_func=None,
                 done_callback=None,
                 has_priority=None):
        assert isinstance(eventloop, EventLoop)
        assert callable(invalidate)
        assert callable(exec_func)
        assert bell_func is None or callable(bell_func)
        assert done_callback is None or callable(done_callback)
        assert has_priority is None or callable(has_priority)

        self.eventloop = eventloop
        self.invalidate = invalidate
        self.exec_func = exec_func
        self.done_callback = done_callback
        self.has_priority = has_priority or (lambda: True)

        self.pid = None
        self.is_terminated = False
        self.suspended = False

        # Create pseudo terminal for this pane.
        self.master, self.slave = os.openpty()

        # Master side -> attached to terminal emulator.
        self._reader = PosixStdinReader(self.master, errors='replace')

        # Create output stream and attach to screen
        self.sx = 0
        self.sy = 0

        self.screen = BetterScreen(
            self.sx,
            self.sy,
            write_process_input=self.write_input,
            bell_func=bell_func)

        self.stream = BetterStream(self.screen)
        self.stream.attach(self.screen)

    def start(self):
        """
        Start the process: fork child.
        """
        self.set_size(120, 24)
        self._start()
        self._connect_reader()
        self._waitpid()

    @classmethod
    def from_command(cls,
                     eventloop,
                     invalidate,
                     command,
                     done_callback,
                     bell_func=None,
                     before_exec_func=None,
                     has_priority=None):
        """
        Create Process from command,
        e.g. command=['python', '-c', 'print("test")']

        :param before_exec_func: Function that is called before `exec` in the process fork.
        """
        assert isinstance(command, list)

        def execv():
            if before_exec_func:
                before_exec_func()

            for p in os.environ['PATH'].split(':'):
                path = os.path.join(p, command[0])
                if os.path.exists(path) and os.access(path, os.X_OK):
                    os.execv(path, command)

        return cls(
            eventloop,
            invalidate,
            execv,
            bell_func=bell_func,
            done_callback=done_callback,
            has_priority=has_priority)

    def _start(self):
        """
        Create fork and start the child process.
        """
        pid = os.fork()

        if pid == 0:
            self._in_child()
        elif pid > 0:
            # In parent.
            os.close(self.slave)
            self.slave = None

            # We wait a very short while, to be sure the child had the time to
            # call _exec. (Otherwise, we are still sharing signal handlers and
            # FDs.) Resizing the pty, when the child is still in our Python
            # code and has the signal handler from prompt_toolkit, but closed
            # the 'fd' for 'call_from_executor', will cause OSError.
            time.sleep(0.1)

            self.pid = pid

    def _waitpid(self):
        """
        Create an executor that waits and handles process termination.
        """

        def wait_for_finished():
            " Wait for PID in executor. "
            os.waitpid(self.pid, 0)
            self.eventloop.call_from_executor(done)

        def done():
            " PID received. Back in the main thread. "
            # Close pty and remove reader.
            os.close(self.master)
            self.eventloop.remove_reader(self.master)
            self.master = None

            # Callback.
            self.is_terminated = True
            self.done_callback()

        self.eventloop.run_in_executor(wait_for_finished)

    def set_size(self, width, height):
        """
        Set terminal size.
        """
        assert isinstance(width, int)
        assert isinstance(height, int)

        if self.master is not None:
            if (self.sx, self.sy) != (width, height):
                set_terminal_size(self.master, height, width)
        self.screen.resize(lines=height, columns=width)

        self.screen.lines = height
        self.screen.columns = width

        self.sx = width
        self.sy = height

    def _in_child(self):
        " Will be executed in the forked child. "
        os.close(self.master)

        # Remove signal handler for SIGWINCH as early as possible.
        # (We don't want this to be triggered when execv has not been called
        # yet.)
        signal.signal(signal.SIGWINCH, 0)

        pty_make_controlling_tty(self.slave)

        # In the fork, set the stdin/out/err to our slave pty.
        os.dup2(self.slave, 0)
        os.dup2(self.slave, 1)
        os.dup2(self.slave, 2)

        # Execute in child.
        try:
            self._close_file_descriptors()
            self.exec_func()
        except Exception:
            traceback.print_exc()
            time.sleep(5)

            os._exit(1)
        os._exit(0)

    def _close_file_descriptors(self):
        # Do not allow child to inherit open file descriptors from parent.
        # (In case that we keep running Python code. We shouldn't close them.
        # because the garbage collector is still active, and he will close them
        # eventually.)
        max_fd = resource.getrlimit(resource.RLIMIT_NOFILE)[-1]

        try:
            os.closerange(3, max_fd)
        except OverflowError:
            # On OS X, max_fd can return very big values, than closerange
            # doesn't understand, e.g. 9223372036854775807. In this case, just
            # use 4096. This is what Linux systems report, and should be
            # sufficient. (I hope...)
            os.closerange(3, 4096)

    def write_input(self, data, paste=False):
        """
        Write user key strokes to the input.

        :param data: (text, not bytes.) The input.
        :param paste: When True, and the process running here understands
            bracketed paste. Send as pasted text.
        """
        # send as bracketed paste?
        if paste and self.screen.bracketed_paste_enabled:
            data = '\x1b[200~' + data + '\x1b[201~'

        self.write_bytes(data.encode('utf-8'))

    def write_bytes(self, data):
        while self.master is not None:
            try:
                os.write(self.master, data)
            except OSError as e:
                # This happens when the window resizes and a SIGWINCH was received.
                # We get 'Error: [Errno 4] Interrupted system call'
                if e.errno == 4:
                    continue
            return

    def write_key(self, key):
        """
        Write prompt_toolkit Key.
        """
        data = prompt_toolkit_key_to_vt100_key(
            key, application_mode=self.screen.in_application_mode)
        self.write_input(data)

    def _connect_reader(self):
        """
        Process stdout output from the process.
        """
        if self.master is not None:
            self.eventloop.add_reader(self.master, self._read)

    def _read(self):
        """
        Read callback, called by the eventloop.
        """
        d = self._reader.read(
            4096)  # Make sure not to read too much at once. (Otherwise,
        # this could block the event loop.)

        if not self._reader.closed:

            def process():
                self.stream.feed(d)
                self.invalidate()

            # Feed directly, if this process has priority. (That is when this
            # pane has the focus in any of the clients.)
            if self.has_priority():
                process()

            # Otherwise, postpone processing until we have CPU time available.
            else:
                self.eventloop.remove_reader(self.master)

                def do_asap():
                    " Process output and reconnect to event loop. "
                    process()
                    self._connect_reader()

                # When the event loop is saturated because of CPU, we will
                # postpone this processing max 'x' seconds.

                # '1' seems like a reasonable value, because that way we say
                # that we will process max 1k/1s in case of saturation.
                # That should be enough to prevent the UI from feeling
                # unresponsive.
                timestamp = datetime.datetime.now() + datetime.timedelta(
                    seconds=1)

                self.eventloop.call_from_executor(
                    do_asap, _max_postpone_until=timestamp)
        else:
            # End of stream. Remove child.
            self.eventloop.remove_reader(self.master)

    def suspend(self):
        """
        Suspend process. Stop reading stdout. (Called when going into copy mode.)
        """
        self.suspended = True
        self.eventloop.remove_reader(self.master)

    def resume(self):
        """
        Resume from 'suspend'.
        """
        if self.suspended and self.master is not None:
            self._connect_reader()
            self.suspended = False

    def get_cwd(self):
        """
        The current working directory for this process. (Or `None` when
        unknown.)
        """
        return get_cwd_for_pid(self.pid)

    def get_name(self):
        """
        The name for this process. (Or `None` when unknown.)
        """
        # TODO: Maybe cache for short time.
        if self.master is not None:
            return get_name_for_fd(self.master)

    def send_signal(self, signal):
        " Send signal to running process. "
        assert isinstance(signal, int), type(signal)

        if self.pid and not self.is_terminated:
            try:
                os.kill(self.pid, signal)
            except OSError:
                pass  # [Errno 3] No such process.

    def create_copy_document(self):
        """
        Create a Document instance and token list that can be used in copy
        mode.
        """
        data_buffer = self.screen.data_buffer
        text = []
        token_lists = []

        first_row = min(data_buffer.keys())
        last_row = max(data_buffer.keys())

        def token_has_no_background(token):
            try:
                # Token looks like ('C', color, bgcolor, bold, underline, ...)
                return token[2] is None
            except IndexError:
                return True

        for lineno in range(first_row, last_row + 1):
            token_list = []

            row = data_buffer[lineno]
            max_column = max(row.keys()) if row else 0

            # Remove trailing whitespace. (If the background is transparent.)
            row_data = [row[x] for x in range(0, max_column + 1)]

            while (row_data and row_data[-1].char.isspace()
                   and token_has_no_background(row_data[-1].token)):
                row_data.pop()

            # Walk through row.
            char_iter = iter(range(len(row_data)))

            for x in char_iter:
                c = row[x]
                text.append(c.char)
                token_list.append((c.token, c.char))

                # Skip next cell when this is a double width character.
                if c.width == 2:
                    try:
                        next(char_iter)
                    except StopIteration:
                        pass

            token_lists.append(token_list)
            text.append('\n')

        def get_tokens_for_line(lineno):
            try:
                return token_lists[lineno]
            except IndexError:
                return []

        # Calculate cursor position.
        d = Document(text=''.join(text))

        return Document(
            text=d.text,
            cursor_position=d.translate_row_col_to_index(
                row=self.screen.pt_screen.cursor_position.y,
                col=self.screen.pt_screen.cursor_position.
                x)), get_tokens_for_line
Esempio n. 12
0
class Client(object):
    def __init__(self, socket_name):
        self.socket_name = socket_name
        self._mode_context_managers = []

        # Connect to socket.
        self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        self.socket.connect(socket_name)
        self.socket.setblocking(1)

        # Input reader.
        #     Some terminals, like lxterminal send non UTF-8 input sequences,
        #     even when the input encoding is supposed to be UTF-8. This
        #     happens in the case of mouse clicks in the right area of a wide
        #     terminal. Apparently, these are some binary blobs in between the
        #     UTF-8 input.)
        #     We should not replace these, because this would break the
        #     decoding otherwise. (Also don't pass errors='ignore', because
        #     that doesn't work for parsing mouse input escape sequences, which
        #     consist of a fixed number of bytes.)
        self._stdin_reader = PosixStdinReader(sys.stdin.fileno(), errors='replace')

    def run_command(self, command, pane_id=None):
        """
        Ask the server to run this command.

        :param pane_id: Optional identifier of the current pane.
        """
        self._send_packet({
            'cmd': 'run-command',
            'data': command,
            'pane_id': pane_id
        })

    def attach(self, detach_other_clients=False, true_color=False):
        """
        Attach client user interface.
        """
        assert isinstance(detach_other_clients, bool)
        assert isinstance(true_color, bool)

        self._send_size()
        self._send_packet({
            'cmd': 'start-gui',
            'detach-others': detach_other_clients,
            'true-color': true_color,
            'term': os.environ.get('TERM', ''),
            'data': ''
        })

        with raw_mode(sys.stdin.fileno()):
            data_buffer = b''

            stdin_fd = sys.stdin.fileno()
            socket_fd = self.socket.fileno()
            current_timeout = INPUT_TIMEOUT  # Timeout, used to flush escape sequences.

            with call_on_sigwinch(self._send_size):
                while True:
                    r, w, x = _select([stdin_fd, socket_fd], [], [], current_timeout)

                    if socket_fd in r:
                        # Received packet from server.
                        data = self.socket.recv(1024)

                        if data == b'':
                            # End of file. Connection closed.
                            # Reset terminal
                            o = Vt100_Output.from_pty(sys.stdout)
                            o.quit_alternate_screen()
                            o.disable_mouse_support()
                            o.disable_bracketed_paste()
                            o.reset_attributes()
                            o.flush()
                            return
                        else:
                            data_buffer += data

                            while b'\0' in data_buffer:
                                pos = data_buffer.index(b'\0')
                                self._process(data_buffer[:pos])
                                data_buffer = data_buffer[pos + 1:]

                    elif stdin_fd in r:
                        # Got user input.
                        self._process_stdin()
                        current_timeout = INPUT_TIMEOUT

                    else:
                        # Timeout. (Tell the server to flush the vt100 Escape.)
                        self._send_packet({'cmd': 'flush-input'})
                        current_timeout = None

    def _process(self, data_buffer):
        """
        Handle incoming packet from server.
        """
        packet = json.loads(data_buffer.decode('utf-8'))

        if packet['cmd'] == 'out':
            # Call os.write manually. In Python2.6, sys.stdout.write doesn't use UTF-8.
            os.write(sys.stdout.fileno(), packet['data'].encode('utf-8'))

        elif packet['cmd'] == 'suspend':
            # Suspend client process to background.
            if hasattr(signal, 'SIGTSTP'):
                os.kill(os.getpid(), signal.SIGTSTP)

        elif packet['cmd'] == 'mode':
            # Set terminal to raw/cooked.
            action = packet['data']

            if action == 'raw':
                cm = raw_mode(sys.stdin.fileno())
                cm.__enter__()
                self._mode_context_managers.append(cm)

            elif action == 'cooked':
                cm = cooked_mode(sys.stdin.fileno())
                cm.__enter__()
                self._mode_context_managers.append(cm)

            elif action == 'restore' and self._mode_context_managers:
                cm = self._mode_context_managers.pop()
                cm.__exit__()

    def _process_stdin(self):
        """
        Received data on stdin. Read and send to server.
        """
        with nonblocking(sys.stdin.fileno()):
            data = self._stdin_reader.read()

        # Send input in chunks of 4k.
        step = 4056
        for i in range(0, len(data), step):
            self._send_packet({
                'cmd': 'in',
                'data': data[i:i + step],
            })

    def _send_packet(self, data):
        " Send to server. "
        data = json.dumps(data).encode('utf-8')

        # Be sure that our socket is blocking, otherwise, the send() call could
        # raise `BlockingIOError` if the buffer is full.
        self.socket.setblocking(1)

        self.socket.send(data + b'\0')

    def _send_size(self):
        " Report terminal size to server. "
        rows, cols = _get_size(sys.stdout.fileno())
        self._send_packet({
            'cmd': 'size',
            'data': [rows, cols]
        })
Esempio n. 13
0
class PipeSource(Source):
    """
    When input is read from another process that is chained to use through a
    unix pipe.
    """
    def __init__(self, fileno, lexer=None, name='<stdin>'):
        assert isinstance(fileno, int)
        assert lexer is None or isinstance(lexer, Lexer)
        assert isinstance(name, six.text_type)

        self.fileno = fileno
        self.lexer = lexer
        self.name = name

        self._line_tokens = []
        self._eof = False

        # Default style attributes.
        self._attrs = Attrs(
            color=None, bgcolor=None, bold=False, underline=False,
            italic=False, blink=False, reverse=False)

        # Start input parser.
        self._parser = self._parse_corot()
        next(self._parser)
        self._stdin_reader = PosixStdinReader(fileno)

    def get_name(self):
        return self.name

    def get_fd(self):
        return self.fileno

    def eof(self):
        return self._eof

    def read_chunk(self):
        # Content is ready for reading on stdin.
        data = self._stdin_reader.read()

        if not data:
            self._eof = True

        # Send input data to the parser.
        for c in data:
            self._parser.send(c)

        # Return the tokens from the parser.
        # (Don't return the last token yet, because the parser should
        # be able to pop if the input starts with \b).
        if self._eof:
            tokens = self._line_tokens[:]
            del self._line_tokens[:]
        else:
            tokens = self._line_tokens[:-1]
            del self._line_tokens[:-1]

        return tokens

    def _parse_corot(self):
        """
        Coroutine that parses the pager input.
        A \b with any character before should make the next character standout.
        A \b with an underscore before should make the next character emphasized.
        """
        token = Token
        line_tokens = self._line_tokens
        replace_one_token = False

        while True:
            csi = False
            c = yield

            if c == '\b':
                # Handle \b escape codes from man pages.
                if line_tokens:
                    _, last_char = line_tokens[-1]
                    line_tokens.pop()
                    replace_one_token = True
                    if last_char == '_':
                        token = Token.Standout2
                    else:
                        token = Token.Standout
                continue

            elif c == '\x1b':
                # Start of color escape sequence.
                square_bracket = yield
                if square_bracket == '[':
                    csi = True
                else:
                    continue
            elif c == '\x9b':
                csi = True

            if csi:
                # Got a CSI sequence. Color codes are following.
                current = ''
                params = []
                while True:
                    char = yield
                    if char.isdigit():
                        current += char
                    else:
                        params.append(min(int(current or 0), 9999))
                        if char == ';':
                            current = ''
                        elif char == 'm':
                            # Set attributes and token.
                            self._select_graphic_rendition(params)
                            token = ('C', ) + self._attrs
                            break
                        else:
                            # Ignore unspported sequence.
                            break
            else:
                line_tokens.append((token, c))
                if replace_one_token:
                    token = Token

    def _select_graphic_rendition(self, attrs):
        """
        Taken a list of graphics attributes and apply changes to Attrs.
        """
        # NOTE: This function is almost literally taken from Pymux.
        #       if something is wrong, please report there as well!
        #       https://github.com/jonathanslenders/pymux
        replace = {}

        if not attrs:
            attrs = [0]
        else:
            attrs = list(attrs[::-1])

        while attrs:
            attr = attrs.pop()

            if attr in _fg_colors:
                replace["color"] = _fg_colors[attr]
            elif attr in _bg_colors:
                replace["bgcolor"] = _bg_colors[attr]
            elif attr == 1:
                replace["bold"] = True
            elif attr == 3:
                replace["italic"] = True
            elif attr == 4:
                replace["underline"] = True
            elif attr == 5:
                replace["blink"] = True
            elif attr == 6:
                replace["blink"] = True  # Fast blink.
            elif attr == 7:
                replace["reverse"] = True
            elif attr == 22:
                replace["bold"] = False
            elif attr == 23:
                replace["italic"] = False
            elif attr == 24:
                replace["underline"] = False
            elif attr == 25:
                replace["blink"] = False
            elif attr == 27:
                replace["reverse"] = False
            elif not attr:
                replace = {}
                self._attrs = Attrs(
                    color=None, bgcolor=None, bold=False, underline=False,
                    italic=False, blink=False, reverse=False)

            elif attr in (38, 48):
                n = attrs.pop()

                # 256 colors.
                if n == 5:
                    if attr == 38:
                        m = attrs.pop()
                        replace["color"] = _256_colors.get(1024 + m)
                    elif attr == 48:
                        m = attrs.pop()
                        replace["bgcolor"] = _256_colors.get(1024 + m)

                # True colors.
                if n == 2:
                    try:
                        color_str = '%02x%02x%02x' % (attrs.pop(), attrs.pop(), attrs.pop())
                    except IndexError:
                        pass
                    else:
                        if attr == 38:
                            replace["color"] = color_str
                        elif attr == 48:
                            replace["bgcolor"] = color_str

        self._attrs = self._attrs._replace(**replace)