def execute(self): try: pantsd_client = PantsDaemonClient(OptionsBootstrapper.create().bootstrap_options) with pantsd_client.lifecycle_lock: pantsd_client.terminate() except ProcessManager.NonResponsiveProcess as e: raise TaskError("failure while terminating pantsd: {}".format(e))
def __init__( self, args: List[str], env: Mapping[str, str], options_bootstrapper: OptionsBootstrapper, stdin=None, stdout=None, stderr=None, ) -> None: """ :param args: The arguments (e.g. sys.argv) for this run. :param env: The environment (e.g. os.environ) for this run. :param options_bootstrapper: The bootstrap options. :param file stdin: The stream representing stdin. :param file stdout: The stream representing stdout. :param file stderr: The stream representing stderr. """ self._start_time = time.time() self._args = args self._env = env self._options_bootstrapper = options_bootstrapper self._bootstrap_options = options_bootstrapper.bootstrap_options self._client = PantsDaemonClient(self._bootstrap_options) self._stdin = stdin or sys.stdin self._stdout = stdout or sys.stdout.buffer self._stderr = stderr or sys.stderr.buffer
def kill_daemon(pid_dir=None): args = ["./pants"] if pid_dir: args.append(f"--pants-subprocessdir={pid_dir}") pantsd_client = PantsDaemonClient( OptionsBootstrapper.create(env=os.environ, args=args, allow_pantsrc=False).bootstrap_options ) with pantsd_client.lifecycle_lock: pantsd_client.terminate()
def __init__( self, args: List[str], env: Mapping[str, str], options_bootstrapper: OptionsBootstrapper, ) -> None: """ :param args: The arguments (e.g. sys.argv) for this run. :param env: The environment (e.g. os.environ) for this run. :param options_bootstrapper: The bootstrap options. """ self._args = args self._env = env self._options_bootstrapper = options_bootstrapper self._bootstrap_options = options_bootstrapper.bootstrap_options self._client = PantsDaemonClient(self._bootstrap_options)
class RemotePantsRunner: """A thin client variant of PantsRunner.""" class Fallback(Exception): """Raised when fallback to an alternate execution mode is requested.""" class Terminated(Exception): """Raised when an active run is terminated mid-flight.""" def __init__( self, args: List[str], env: Mapping[str, str], options_bootstrapper: OptionsBootstrapper, ) -> None: """ :param args: The arguments (e.g. sys.argv) for this run. :param env: The environment (e.g. os.environ) for this run. :param options_bootstrapper: The bootstrap options. """ self._start_time = time.time() self._args = args self._env = env self._options_bootstrapper = options_bootstrapper self._bootstrap_options = options_bootstrapper.bootstrap_options self._client = PantsDaemonClient(self._bootstrap_options) def run(self) -> ExitCode: """Starts up a pantsd instance if one is not already running, then connects to it via nailgun.""" pantsd_handle = self._client.maybe_launch() logger.debug(f"Connecting to pantsd on port {pantsd_handle.port}") return self._connect_and_execute(pantsd_handle) def _connect_and_execute( self, pantsd_handle: PantsDaemonClient.Handle) -> ExitCode: global_options = self._bootstrap_options.for_global_scope() executor = GlobalOptions.create_py_executor(global_options) # Merge the nailgun TTY capability environment variables with the passed environment dict. ng_env = NailgunProtocol.ttynames_to_env(sys.stdin, sys.stdout, sys.stderr) modified_env = { **self._env, **ng_env, "PANTSD_RUNTRACKER_CLIENT_START_TIME": str(self._start_time), "PANTSD_REQUEST_TIMEOUT_LIMIT": str(global_options.pantsd_timeout_when_multiple_invocations), } command = self._args[0] args = self._args[1:] retries = 3 attempt = 1 while True: port = pantsd_handle.port logger.debug( f"Connecting to pantsd on port {port} attempt {attempt}/{retries}" ) # We preserve TTY settings since the server might write directly to the TTY, and we'd like # to clean up any side effects before exiting. # # We ignore keyboard interrupts because the nailgun client will handle them. with STTYSettings.preserved(), interrupts_ignored(): try: return native_engine.nailgun_client_create( executor, port).execute(command, args, modified_env) # NailgunConnectionException represents a failure connecting to pantsd, so we retry # up to the retry limit. except NailgunConnectionException as e: if attempt > retries: raise self.Fallback(e) # Wait one second before retrying logger.warning( f"Pantsd was unresponsive on port {port}, retrying.") time.sleep(1) # One possible cause of the daemon being non-responsive during an attempt might be if a # another lifecycle operation is happening concurrently (incl teardown). To account for # this, we won't begin attempting restarts until at least 1 attempt has passed. if attempt > 1: pantsd_handle = self._client.restart() attempt += 1
class RemotePantsRunner: """A thin client variant of PantsRunner.""" class Fallback(Exception): """Raised when fallback to an alternate execution mode is requested.""" class Terminated(Exception): """Raised when an active run is terminated mid-flight.""" RECOVERABLE_EXCEPTIONS = ( NailgunClient.NailgunConnectionError, NailgunClient.NailgunExecutionError, ) def __init__( self, args: List[str], env: Mapping[str, str], options_bootstrapper: OptionsBootstrapper, ) -> None: """ :param args: The arguments (e.g. sys.argv) for this run. :param env: The environment (e.g. os.environ) for this run. :param options_bootstrapper: The bootstrap options. """ self._start_time = time.time() self._args = args self._env = env self._options_bootstrapper = options_bootstrapper self._bootstrap_options = options_bootstrapper.bootstrap_options self._client = PantsDaemonClient(self._bootstrap_options) @staticmethod def _backoff(attempt): """Minimal backoff strategy for daemon restarts.""" time.sleep(attempt + (attempt - 1)) def run(self) -> ExitCode: """Runs pants remotely with retry and recovery for nascent executions.""" pantsd_handle = self._client.maybe_launch() retries = 3 attempt = 1 while True: logger.debug( "connecting to pantsd on port {} (attempt {}/{})".format( pantsd_handle.port, attempt, retries)) try: return self._connect_and_execute(pantsd_handle) except self.RECOVERABLE_EXCEPTIONS as e: if attempt > retries: raise self.Fallback(e) self._backoff(attempt) logger.warning( "pantsd was unresponsive on port {}, retrying ({}/{})". format(pantsd_handle.port, attempt, retries)) # One possible cause of the daemon being non-responsive during an attempt might be if a # another lifecycle operation is happening concurrently (incl teardown). To account for # this, we won't begin attempting restarts until at least 1 second has passed (1 attempt). if attempt > 1: pantsd_handle = self._client.restart() attempt += 1 except NailgunClient.NailgunError as e: # Ensure a newline. logger.critical("") logger.critical("lost active connection to pantsd!") traceback = sys.exc_info()[2] raise self._extract_remote_exception( pantsd_handle.pid, e).with_traceback(traceback) def _connect_and_execute( self, pantsd_handle: PantsDaemonClient.Handle) -> ExitCode: port = pantsd_handle.port pid = pantsd_handle.pid global_options = self._bootstrap_options.for_global_scope() # Merge the nailgun TTY capability environment variables with the passed environment dict. ng_env = NailgunProtocol.ttynames_to_env(sys.stdin, sys.stdout.buffer, sys.stderr.buffer) modified_env = { **self._env, **ng_env, "PANTSD_RUNTRACKER_CLIENT_START_TIME": str(self._start_time), "PANTSD_REQUEST_TIMEOUT_LIMIT": str(global_options.pantsd_timeout_when_multiple_invocations), } # Instantiate a NailgunClient. client = NailgunClient( port=port, remote_pid=pid, ins=sys.stdin, out=sys.stdout.buffer, err=sys.stderr.buffer, exit_on_broken_pipe=True, metadata_base_dir=pantsd_handle.metadata_base_dir, ) timeout = global_options.pantsd_pailgun_quit_timeout pantsd_signal_handler = PailgunClientSignalHandler(client, pid=pid, timeout=timeout) with ExceptionSink.trapped_signals( pantsd_signal_handler), STTYSettings.preserved(): # Execute the command on the pailgun. return client.execute(self._args[0], self._args[1:], modified_env) def _extract_remote_exception(self, pantsd_pid, nailgun_error): """Given a NailgunError, returns a Terminated exception with additional info (where possible). This method will include the entire exception log for either the `pid` in the NailgunError, or failing that, the `pid` of the pantsd instance. """ sources = [pantsd_pid] exception_text = None for source in sources: log_path = ExceptionSink.exceptions_log_path(for_pid=source) exception_text = maybe_read_file(log_path) if exception_text: break exception_suffix = ("\nRemote exception:\n{}".format(exception_text) if exception_text else "") return self.Terminated( "abruptly lost active connection to pantsd runner: {!r}{}".format( nailgun_error, exception_suffix))