Esempio n. 1
0
def wait_for_exit(runner: Runner, main_process: Popen) -> None:
    """
    Monitor main process and background items until done
    """
    runner.write("Everything launched. Waiting to exit...")
    main_command = str_command(str(arg) for arg in main_process.args)
    span = runner.span()
    while True:
        sleep(0.1)
        main_code = main_process.poll()
        if main_code is not None:
            # Shell exited, we're done. Automatic shutdown cleanup will kill
            # subprocesses.
            runner.write(
                "Main process ({})\n exited with code {}.".format(
                    main_command, main_code
                )
            )
            span.end()
            runner.set_success(True)
            raise SystemExit(main_code)
        if runner.tracked:
            dead_bg = runner.tracked.which_dead()
        else:
            dead_bg = None
        if dead_bg:
            # Unfortunately torsocks doesn't deal well with connections
            # being lost, so best we can do is shut down.
            # FIXME: Look at bg.critical and do something smarter
            runner.show(
                "Proxy to Kubernetes exited. This is typically due to"
                " a lost connection."
            )
            span.end()
            raise runner.fail("Exiting...", code=3)
Esempio n. 2
0
    def wait_for_exit(self, main_process: Popen) -> None:
        """
        Monitor main process and background items until done
        """
        self.write("Everything launched. Waiting to exit...")
        main_code = None
        span = self.span()
        while not self.quitting and main_code is None:
            sleep(0.1)
            main_code = main_process.poll()
        span.end()

        if main_code is not None:
            # Shell exited, we're done. Automatic shutdown cleanup
            # will kill subprocesses.
            main_command = str_command(str(arg) for arg in main_process.args)
            self.write("Main process ({})".format(main_command))
            self.write(" exited with code {}.".format(main_code))
            raise self.exit()

        # Something else exited, setting the quitting flag.
        # Unfortunately torsocks doesn't deal well with connections
        # being lost, so best we can do is shut down.
        if self.ended:
            self.show("\n")
            self.show_raw(self.ended[0])
        self.show("\n")
        message = ("Proxy to Kubernetes exited. This is typically due to"
                   " a lost connection.")
        raise self.fail(message, code=3)
Esempio n. 3
0
    def __init__(self, logfile_path: str) -> None:
        """
        Create output handle

        :param logfile_path: Path or string file path or "-" for stdout
        """

        if logfile_path == "-":
            self.logfile = sys.stdout
        else:
            # Log file path should be absolute since some processes may run in
            # different directories:
            logfile_path = os.path.abspath(logfile_path)
            self.logfile = _open_logfile(logfile_path)
        self.logfile_path = logfile_path

        self.start_time = curtime()
        self.logtail = deque(maxlen=25)  # type: deque  # keep last 25 lines

        self.write("Telepresence {} launched at {}".format(
            __version__, ctime()))
        self.write("  {}".format(str_command(sys.argv)))
        if version_override:
            self.write("  TELEPRESENCE_VERSION is {}".format(image_version))
        elif image_version != __version__:
            self.write("  Using images version {} (dev)".format(image_version))
Esempio n. 4
0
    def __init__(self, logfile_path: str) -> None:
        """
        Create output handle

        :param logfile_path: Path or string file path or "-" for stdout
        """

        # Fail if current working directory does not exist so we don't crash in
        # standard library path-handling code.
        try:
            os.getcwd()
        except OSError:
            exit("T: Error: Current working directory does not exist.")

        if logfile_path == "-":
            self.logfile = sys.stdout
        else:
            # Log file path should be absolute since some processes may run in
            # different directories:
            logfile_path = os.path.abspath(logfile_path)
            self.logfile = _open_logfile(logfile_path)
        self.logfile_path = logfile_path

        self.start_time = curtime()
        self.logtail = deque(maxlen=25)  # type: deque  # keep last 25 lines

        self.write("Telepresence {} launched at {}".format(
            __version__, ctime()))
        self.write("  {}".format(str_command(sys.argv)))
        if version_override:
            self.write("  TELEPRESENCE_VERSION is {}".format(image_version))
        elif image_version != __version__:
            self.write("  Using images version {} (dev)".format(image_version))
Esempio n. 5
0
 def _run_command(self, track, msg1, msg2, out_cb, err_cb, args, **kwargs):
     """Run a command synchronously"""
     self.output.write("[{}] {}: {}".format(track, msg1, str_command(args)))
     span = self.span("{} {}".format(track, str_command(args))[:80],
                      False,
                      verbose=False)
     process = self._launch_command(track, out_cb, err_cb, args, **kwargs)
     process.wait()
     spent = span.end()
     retcode = process.poll()
     if retcode:
         self.output.write("[{}] exit {} in {:0.2f} secs.".format(
             track, retcode, spent))
         raise CalledProcessError(retcode, args)
     if spent > 1:
         self.output.write("[{}] {} in {:0.2f} secs.".format(
             track, msg2, spent))
Esempio n. 6
0
 def done(proc):
     retcode = proc.wait()
     self.output.write("[{}] exit {}".format(track, retcode))
     self.quitting = True
     recent_lines = [str(line) for line in capture if line is not None]
     recent = "  ".join(recent_lines).strip()
     if recent:
         recent = "\nRecent output was:\n  {}".format(recent)
     message = ("Background process ({}) exited with return code {}. "
                "Command was:\n  {}\n{}").format(
                    name, retcode, str_command(args), recent)
     self.ended.append(message)
Esempio n. 7
0
        def wait_for_process(p: "Popen[str]") -> None:
            """Wait for process and set main_code and self.quitting flag

            Note that main_code is defined in the parent function,
            so it is declared as nonlocal

            See https://github.com/telepresenceio/telepresence/issues/1003
            """
            nonlocal main_code
            main_code = p.wait()
            main_command = str_command(str(arg) for arg in main_process.args)
            self.write("Main process ({})".format(main_command))
            self.write(" exited with code {}.".format(main_code))
            self.quitting = True
Esempio n. 8
0
    def wait_for_exit(self, main_process: "Popen[str]") -> None:
        """
        Monitor main process and background items until done
        """
        main_code = None

        def wait_for_process(p: "Popen[str]") -> None:
            """Wait for process and set main_code and self.quitting flag

            Note that main_code is defined in the parent function,
            so it is declared as nonlocal

            See https://github.com/telepresenceio/telepresence/issues/1003
            """
            nonlocal main_code
            main_code = p.wait()
            self.quitting = True

        self.write("Everything launched. Waiting to exit...")
        span = self.span()
        Thread(target=wait_for_process, args=(main_process, )).start()
        while not self.quitting:
            sleep(0.1)
        span.end()

        if main_code is not None:
            # User process exited, we're done. Automatic shutdown cleanup
            # will kill subprocesses.
            main_command = str_command(str(arg) for arg in main_process.args)
            self.write("Main process ({})".format(main_command))
            self.write(" exited with code {}.".format(main_code))
            message = "Your process "
            if main_code:
                message += "exited with return code {}.".format(main_code)
            else:
                message += "has exited."
            self.show(message)
            raise self.exit(main_code)

        # Something else exited, setting the quitting flag.
        # Unfortunately torsocks doesn't deal well with connections
        # being lost, so best we can do is shut down.
        if self.ended:
            self.show("\n")
            self.show_raw(self.ended[0])
        self.show("\n")
        message = ("Proxy to Kubernetes exited. This is typically due to"
                   " a lost connection.")
        raise self.fail(message)
Esempio n. 9
0
    def popen(self, args, **kwargs) -> Popen:
        """Return Popen object."""
        self.counter = track = self.counter + 1
        out_cb = err_cb = self.make_logger(track)

        def done(proc):
            self._popen_done(track, proc)

        self.output.write(
            "[{}] Launching: {}".format(track, str_command(args))
        )
        process = self.launch_command(
            track, out_cb, err_cb, args, done=done, **kwargs
        )
        return process
Esempio n. 10
0
 def done(proc: "Popen[str]") -> None:
     retcode = proc.wait()
     self.output.write("[{}] {}: exit {}".format(track, name, retcode))
     recent = "\n  ".join(out_logger.get_captured().split("\n"))
     if recent:
         recent = "\nRecent output was:\n  {}".format(recent)
     message = ("Background process ({}) exited with return code {}. "
                "Command was:\n  {}\n{}").format(
                    name, retcode, str_command(args), recent)
     self.ended.append(message)
     if is_critical:
         # End the program because this is a critical subprocess
         self.quitting = True
     else:
         # Record the failure but don't quit
         self.output.write(message)
Esempio n. 11
0
    def _popen(self, name: str, args, **kwargs) -> typing.Tuple[int, Popen]:
        """Return Popen object."""
        self.counter = track = self.counter + 1
        out_cb = err_cb = self._make_logger(track)

        def done(proc):
            retcode = proc.wait()
            self.output.write("[{}] exit {}".format(track, retcode))

        self.output.write(
            "[{}] Launching {}: {}".format(track, name, str_command(args))
        )
        process = self._launch_command(
            track, out_cb, err_cb, args, done=done, **kwargs
        )
        return track, process
Esempio n. 12
0
    def launch(self,
               name: str,
               args,
               killer=None,
               keep_session=False,
               **kwargs) -> None:
        if not keep_session:
            # This prevents signals from getting forwarded, but breaks sudo
            # if it is configured to ask for a password.
            kwargs["start_new_session"] = True
        assert "stderr" not in kwargs
        kwargs["stderr"] = STDOUT
        self.counter = track = self.counter + 1
        capture = deque(maxlen=10)  # type: typing.MutableSequence[str]
        out_cb = err_cb = self._make_logger(track, capture=capture)

        def done(proc):
            retcode = proc.wait()
            self.output.write("[{}] exit {}".format(track, retcode))
            self.quitting = True
            recent_lines = [str(line) for line in capture if line is not None]
            recent = "  ".join(recent_lines).strip()
            if recent:
                recent = "\nRecent output was:\n  {}".format(recent)
            message = ("Background process ({}) exited with return code {}. "
                       "Command was:\n  {}\n{}").format(
                           name, retcode, str_command(args), recent)
            self.ended.append(message)

        self.output.write("[{}] Launching {}: {}".format(
            track, name, str_command(args)))
        try:
            process = _launch_command(args,
                                      out_cb,
                                      err_cb,
                                      done=done,
                                      **kwargs)
        except OSError as exc:
            self.output.write("[{}] {}".format(track, exc))
            raise
        self.add_cleanup(
            "Kill BG process [{}] {}".format(track, name),
            killer if killer else partial(kill_process, process),
        )
Esempio n. 13
0
 def report_subprocess_failure(
     self, exc: typing.Union[CalledProcessError, TimeoutExpired]
 ) -> None:
     if isinstance(exc, TimeoutExpired):
         command = exc.cmd
         message = "Timed out after {:.2f} seconds)".format(exc.timeout)
         if exc.output:
             output = exc.output
         else:
             output = "[no output]"
     elif isinstance(exc, CalledProcessError):
         command = exc.cmd
         message = "Exited with return code {}".format(exc.returncode)
         if exc.output:
             output = exc.output
         else:
             output = "[no output]"
     else:
         raise exc  # i.e. crash
     indent = "  "
     output = indent + ("\n" + indent).join(output.splitlines())
     self.show("{}$ {}".format(indent, str_command(command)))
     self.show_raw(output)
     self.show(indent + "--> " + message)
Esempio n. 14
0
 def command_span(self, track, args):
     return self.span(
         "{} {}".format(track, str_command(args))[:80],
         False,
         verbose=False
     )
Esempio n. 15
0
    def launch(
        self,
        name: str,
        args: typing.List[str],
        killer: typing.Optional[typing.Callable[[], None]] = None,
        notify: bool = False,
        keep_session: bool = False,
        bufsize: int = -1,
        is_critical: bool = True,
    ) -> None:
        """Asyncrounously run a process.

        :param name: A human-friendly name to describe the process.

        :param args: The command to run.

        :param killer: How to signal to the process that it should
        stop.  The default is to call Popen.terminate(), which on
        POSIX OSs sends SIGTERM.

        :param notify: Whether to synchronously wait for the process
        to send "READY=1" via the ``sd_notify(3)`` interface before
        returning.

        :param keep_session: Whether to run the process in the current
        session (as in ``setsid()``), or in a new session.  The
        default is to run in a new session, in order to prevent
        keyboard signals from getting forwarded.  However, running in
        a new session breaks sudo if it is configured to ask for a
        password.

        :parmam bufsize: See ``subprocess.Popen()`.

        :param is_critical: Whether this process quitting should end this
        Telepresence session. Default is True because that used to be the
        only supported behavior.

        :return: ``None``.

        """
        self.counter = track = self.counter + 1
        out_logger = self._make_logger(track, True, True, 10)

        def done(proc: "Popen[str]") -> None:
            retcode = proc.wait()
            self.output.write("[{}] {}: exit {}".format(track, name, retcode))
            recent = "\n  ".join(out_logger.get_captured().split("\n"))
            if recent:
                recent = "\nRecent output was:\n  {}".format(recent)
            message = ("Background process ({}) exited with return code {}. "
                       "Command was:\n  {}\n{}").format(
                           name, retcode, str_command(args), recent)
            self.ended.append(message)
            if is_critical:
                # End the program because this is a critical subprocess
                self.quitting = True
            else:
                # Record the failure but don't quit
                self.output.write(message)

        self.output.write("[{}] Launching {}: {}".format(
            track, name, str_command(args)))
        env = os.environ.copy()
        if notify:
            sockname = str(self.temp / "notify-{}".format(track))
            sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
            sock.bind(sockname)
            env["NOTIFY_SOCKET"] = sockname
        try:
            process = _launch_command(
                args,
                out_logger,
                out_logger,  # Won't be used
                done=done,
                # kwargs
                start_new_session=not keep_session,
                stderr=STDOUT,
                bufsize=bufsize,
                env=env)
        except OSError as exc:
            self.output.write("[{}] {}".format(track, exc))
            raise
        if killer is None:
            killer = partial(kill_process, process)
        self.add_cleanup("Kill BG process [{}] {}".format(track, name), killer)
        if notify:
            # We need a select()able notification of death in case the
            # process dies before sending READY=1.  In C, I'd do this
            # same pipe trick, but close the pipe from a SIGCHLD
            # handler, which is lighter than a thread.  But I fear
            # that a SIGCHLD handler would interfere with the Python
            # runtime?  We're already using several threads per
            # launched process, so what's the harm in one more?
            pr, pw = os.pipe()

            def pipewait() -> None:
                process.wait()
                os.close(pw)

            Thread(target=pipewait, daemon=True).start()

            # Block until either the process exits or we get a READY=1
            # line on the socket.
            while process.poll() is None:
                r, _, x = select.select([pr, sock], [], [pr, sock])
                if sock in r or sock in x:
                    lines = sock.recv(4096).decode("utf-8").split("\n")
                    if "READY=1" in lines:
                        break

            os.close(pr)
            sock.close()
Esempio n. 16
0
    def _run_command_sync(
        self,
        messages: typing.Tuple[str, str],
        log_stdout: bool,
        stderr_to_stdout: bool,
        args: typing.List[str],
        capture_limit: int,
        timeout: typing.Optional[float],
        input: typing.Optional[bytes],
        env: typing.Optional[typing.Dict[str, str]],
    ) -> str:
        """
        Run a command synchronously. Log stdout (optionally) and stderr (if not
        redirected to stdout). Capture stdout and stderr, at least for
        exceptions. Return output.
        """
        self.counter = track = self.counter + 1
        self.output.write("[{}] {}: {}".format(track, messages[0],
                                               str_command(args)))
        span = self.span("{} {}".format(track, str_command(args))[:80],
                         False,
                         verbose=False)
        kwargs = {}  # type: typing.Dict[str, typing.Any]
        if env is not None:
            kwargs["env"] = env
        if input is not None:
            kwargs["input"] = input

        # Set up capture/logging
        out_logger = self._make_logger(track, log_stdout or self.verbose, True,
                                       capture_limit)
        if stderr_to_stdout:
            # This logger won't be used
            err_logger = self._make_logger(track, False, False, capture_limit)
            kwargs["stderr"] = STDOUT
        else:
            err_logger = self._make_logger(track, True, True, capture_limit)

        # Launch the process and wait for it to finish
        try:
            process = _launch_command(args, out_logger, err_logger, **kwargs)
        except OSError as exc:
            # Failed to launch, so no need to wrap up capture stuff.
            self.output.write("[{}] {}".format(track, exc))
            raise

        TIMED_OUT_RETCODE = -999
        try:
            retcode = process.wait(timeout)
        except TimeoutExpired:
            retcode = TIMED_OUT_RETCODE  # sentinal for timeout
            process.terminate()
            try:
                process.wait(timeout=1)
            except TimeoutExpired:
                process.kill()
                process.wait()

        output = out_logger.get_captured()
        spent = span.end()

        if retcode == TIMED_OUT_RETCODE:
            # Command timed out. Need to raise TE.
            self.output.write("[{}] timed out after {:0.2f} secs.".format(
                track, spent))
            assert timeout is not None
            raise TimeoutExpired(
                args,
                timeout,
                output,
                None if stderr_to_stdout else err_logger.get_captured(),
            )
        if retcode:
            # Command failed. Need to raise CPE.
            self.output.write("[{}] exit {} in {:0.2f} secs.".format(
                track, retcode, spent))
            raise CalledProcessError(
                retcode,
                args,
                output,
                None if stderr_to_stdout else err_logger.get_captured(),
            )

        # Command succeeded. Just return the output
        self.output.write("[{}] {} in {:0.2f} secs.".format(
            track, messages[1], spent))
        return output