def run(self): # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self._args # Broadcast our process group ID (in PID form - i.e. negated) to the remote client so # they can send signals (e.g. SIGINT) to all processes in the runners process group. with self._maybe_shutdown_socket.lock: NailgunProtocol.send_pid(self._maybe_shutdown_socket.socket, os.getpid()) NailgunProtocol.send_pgrp(self._maybe_shutdown_socket.socket, os.getpgrp() * -1) # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio( self._maybe_shutdown_socket, self._env) as finalizer, DaemonExiter.override_global_exiter( self._maybe_shutdown_socket, finalizer), hermetic_environment_as( **self._env), encapsulated_global_logger(): try: # Clean global state. clean_global_runtime_state(reset_subsystem=True) options, _, options_bootstrapper = LocalPantsRunner.parse_options( self._args, self._env) graph_helper, specs, exit_code = self._scheduler_service.prepare_v1_graph_run_v2( options, options_bootstrapper, ) self.exit_code = exit_code # Otherwise, conduct a normal run. with ExceptionSink.exiter_as_until_exception( lambda _: PantsRunFailCheckerExiter()): runner = LocalPantsRunner.create( self._args, self._env, specs, graph_helper, options_bootstrapper, ) runner.set_start_time( self._maybe_get_client_start_time_from_env(self._env)) runner.run() except KeyboardInterrupt: self._exiter.exit_and_fail("Interrupted by user.\n") except _PantsRunFinishedWithFailureException as e: ExceptionSink.log_exception( "Pants run failed with exception: {}; exiting".format(e)) self._exiter.exit(e.exit_code) except Exception as e: # TODO: We override sys.excepthook above when we call ExceptionSink.set_exiter(). That # excepthook catches `SignalHandledNonLocalExit`s from signal handlers, which isn't # happening here, so something is probably overriding the excepthook. By catching Exception # and calling this method, we emulate the normal, expected sys.excepthook override. ExceptionSink._log_unhandled_exception_and_exit(exc=e) else: self._exiter.exit(self.exit_code if self. exit_code else PANTS_SUCCEEDED_EXIT_CODE)
def test_hermetic_environment_unicode(self): with environment_as(XXX='¡'): self.assertEqual(os.environ['XXX'], '¡') with hermetic_environment_as(AAA='¡'): self.assertIn('AAA', os.environ) self.assertEqual(os.environ['AAA'], '¡') self.assertEqual(os.environ['XXX'], '¡')
def run_sync(self): """Synchronously run pantsd.""" os.environ.pop("PYTHONPATH") global_bootstrap_options = self._bootstrap_options.for_global_scope() # Set the process name in ps output to 'pantsd' vs './pants compile src/etc:: -ldebug'. set_process_title(f"pantsd [{self._build_root}]") # Switch log output to the daemon's log stream, and empty `env` and `argv` to encourage all # further usage of those variables to happen via engine APIs and options. self._close_stdio() with initialize_stdio(global_bootstrap_options), argv_as( tuple()), hermetic_environment_as(): # Install signal and panic handling. ExceptionSink.install( log_location=init_workdir(global_bootstrap_options), pantsd_instance=True) native_engine.maybe_set_panic_handler() self._initialize_metadata() # Check periodically whether the core is valid, and exit if it is not. while self._core.is_valid(): time.sleep(self.JOIN_TIMEOUT_SECONDS) # We're exiting: join the server to avoid interrupting ongoing runs. self._logger.info( "Waiting for ongoing runs to complete before exiting...") native_engine.nailgun_server_await_shutdown(self._server) self._logger.info("Exiting pantsd")
def test_hermetic_environment_unicode(self) -> None: with environment_as(XXX="¡"): self.assertEqual(os.environ["XXX"], "¡") with hermetic_environment_as(AAA="¡"): self.assertIn("AAA", os.environ) self.assertEqual(os.environ["AAA"], "¡") self.assertEqual(os.environ["XXX"], "¡")
def __call__( self, command: str, args: Tuple[str, ...], env: Dict[str, str], working_directory: bytes, cancellation_latch: PySessionCancellationLatch, stdin_fd: int, stdout_fd: int, stderr_fd: int, ) -> ExitCode: request_timeout = float(env.get("PANTSD_REQUEST_TIMEOUT_LIMIT", -1)) # NB: Order matters: we acquire a lock before mutating either `sys.std*`, `os.environ`, etc. with self._one_run_at_a_time( stderr_fd, cancellation_latch=cancellation_latch, timeout=request_timeout, ), stdio_as(stdin_fd=stdin_fd, stdout_fd=stdout_fd, stderr_fd=stderr_fd), hermetic_environment_as( **env), argv_as((command, ) + args): # NB: Run implements exception handling, so only the most primitive errors will escape # this function, where they will be logged to the pantsd.log by the server. logger.info(f"handling request: `{' '.join(args)}`") try: return self.single_daemonized_run(working_directory.decode(), cancellation_latch) finally: logger.info(f"request completed: `{' '.join(args)}`")
def test_hermetic_environment_subprocesses(self): with self.ensure_user_defined_in_environment(): with hermetic_environment_as(AAA='333'): output = subprocess.check_output('env', shell=True).decode() self.assertNotIn('USER='******'AAA', os.environ) self.assertEqual(os.environ['AAA'], '333') self.assertIn('USER', os.environ) self.assertNotIn('AAA', os.environ)
def test_hermetic_environment_subprocesses(self) -> None: with self.ensure_user_defined_in_environment(): with hermetic_environment_as(AAA="333"): output = subprocess.check_output("env", shell=True).decode() self.assertNotIn("USER="******"AAA", os.environ) self.assertEqual(os.environ["AAA"], "333") self.assertIn("USER", os.environ) self.assertNotIn("AAA", os.environ)
def test_hermetic_environment_subprocesses(self): self.assertIn('USER', os.environ) with hermetic_environment_as(**dict(AAA='333')): output = subprocess.check_output('env', shell=True) self.assertNotIn('USER='******'AAA', os.environ) self.assertEquals(os.environ['AAA'], '333') self.assertIn('USER', os.environ) self.assertNotIn('AAA', os.environ)
def test_hermetic_environment_unicode(self): UNICODE_CHAR = '¡' ENCODED_CHAR = UNICODE_CHAR.encode('utf-8') with environment_as(**dict(XXX=UNICODE_CHAR)): self.assertEquals(os.environ['XXX'], ENCODED_CHAR) with hermetic_environment_as(**dict(AAA=UNICODE_CHAR)): self.assertIn('AAA', os.environ) self.assertEquals(os.environ['AAA'], ENCODED_CHAR) self.assertEquals(os.environ['XXX'], ENCODED_CHAR)
def test_hermetic_environment_subprocesses(self): self.assertIn('USER', os.environ) with hermetic_environment_as(**dict(AAA='333')): output = subprocess.check_output('env', shell=True).decode('utf-8') self.assertNotIn('USER='******'AAA', os.environ) self.assertEquals(os.environ['AAA'], '333') self.assertIn('USER', os.environ) self.assertNotIn('AAA', os.environ)
def post_fork_child(self): """Post-fork child process callback executed via ProcessManager.daemonize().""" # Set the Exiter exception hook post-fork so as not to affect the pantsd processes exception # hook with socket-specific behavior. Note that this intentionally points the faulthandler # trace stream to sys.stderr, which at this point is still a _LoggerStream object writing to # the `pantsd.log`. This ensures that in the event of e.g. a hung but detached pantsd-runner # process that the stacktrace output lands deterministically in a known place vs to a stray # terminal window. # TODO: test the above! ExceptionSink.reset_exiter(self._exiter) ExceptionSink.reset_interactive_output_stream( sys.stderr.buffer if PY3 else sys.stderr) # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self._args # Set context in the process title. set_process_title('pantsd-runner [{}]'.format(' '.join(self._args))) # Broadcast our process group ID (in PID form - i.e. negated) to the remote client so # they can send signals (e.g. SIGINT) to all processes in the runners process group. NailgunProtocol.send_pid(self._socket, os.getpid()) NailgunProtocol.send_pgrp(self._socket, os.getpgrp() * -1) # Stop the services that were paused pre-fork. for service in self._services.services: service.terminate() # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio(self._socket, self._env) as finalizer,\ hermetic_environment_as(**self._env): try: # Setup the Exiter's finalizer. self._exiter.set_finalizer(finalizer) # Clean global state. clean_global_runtime_state(reset_subsystem=True) # Re-raise any deferred exceptions, if present. self._raise_deferred_exc() # Otherwise, conduct a normal run. runner = LocalPantsRunner.create(self._exiter, self._args, self._env, self._target_roots, self._graph_helper, self._options_bootstrapper) runner.set_start_time( self._maybe_get_client_start_time_from_env(self._env)) runner.run() except KeyboardInterrupt: self._exiter.exit_and_fail('Interrupted by user.\n') except Exception: ExceptionSink._log_unhandled_exception_and_exit() else: self._exiter.exit(0)
def test_hermetic_environment_unicode(self): UNICODE_CHAR = '¡' ENCODED_CHAR = UNICODE_CHAR.encode('utf-8') expected_output = UNICODE_CHAR if PY3 else ENCODED_CHAR with environment_as(**dict(XXX=UNICODE_CHAR)): self.assertEquals(os.environ['XXX'], expected_output) with hermetic_environment_as(**dict(AAA=UNICODE_CHAR)): self.assertIn('AAA', os.environ) self.assertEquals(os.environ['AAA'], expected_output) self.assertEquals(os.environ['XXX'], expected_output)
def run(self): # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self._args # Broadcast our process group ID (in PID form - i.e. negated) to the remote client so # they can send signals (e.g. SIGINT) to all processes in the runners process group. with self._maybe_shutdown_socket.lock: NailgunProtocol.send_pid(self._maybe_shutdown_socket.socket, os.getpid()) NailgunProtocol.send_pgrp(self._maybe_shutdown_socket.socket, os.getpgrp() * -1) # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio(self._maybe_shutdown_socket, self._env) as finalizer, \ hermetic_environment_as(**self._env), \ encapsulated_global_logger(): try: # Raise any exceptions we may have found when precomputing products. # NB: We raise it here as opposed to earlier because we have setup logging and stdio. if self._exception is not None: raise self._exception # Clean global state. clean_global_runtime_state(reset_subsystem=True) # Setup the Exiter's finalizer. self._exiter.set_finalizer(finalizer) # Otherwise, conduct a normal run. runner = LocalPantsRunner.create( PantsRunFailCheckerExiter(), self._args, self._env, self._target_roots, self._graph_helper, self._options_bootstrapper, ) runner.set_start_time(self._maybe_get_client_start_time_from_env(self._env)) runner.run() except KeyboardInterrupt: self._exiter.exit_and_fail('Interrupted by user.\n') except _PantsRunFinishedWithFailureException as e: ExceptionSink.log_exception( 'Pants run failed with exception: {}; exiting'.format(e)) self._exiter.exit(e.exit_code) except _PantsProductPrecomputeFailed as e: ExceptionSink.log_exception(repr(e)) self._exiter.exit(e.exit_code) except Exception as e: # TODO: We override sys.excepthook above when we call ExceptionSink.set_exiter(). That # excepthook catches `SignalHandledNonLocalExit`s from signal handlers, which isn't # happening here, so something is probably overriding the excepthook. By catching Exception # and calling this method, we emulate the normal, expected sys.excepthook override. ExceptionSink._log_unhandled_exception_and_exit(exc=e) else: self._exiter.exit(PANTS_SUCCEEDED_EXIT_CODE)
def post_fork_child(self): """Post-fork child process callback executed via ProcessManager.daemonize().""" # Set the Exiter exception hook post-fork so as not to affect the pantsd processes exception # hook with socket-specific behavior. Note that this intentionally points the faulthandler # trace stream to sys.stderr, which at this point is still a _LoggerStream object writing to # the `pantsd.log`. This ensures that in the event of e.g. a hung but detached pantsd-runner # process that the stacktrace output lands deterministically in a known place vs to a stray # terminal window. self._exiter.set_except_hook(sys.stderr) # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self._args # Set context in the process title. set_process_title('pantsd-runner [{}]'.format(' '.join(self._args))) # Setup a SIGINT signal handler. self._setup_sigint_handler() # Broadcast our process group ID (in PID form - i.e. negated) to the remote client so # they can send signals (e.g. SIGINT) to all processes in the runners process group. pid = str(os.getpgrp() * -1).encode('ascii') NailgunProtocol.send_pid(self._socket, pid) # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio(self._socket, self._env) as finalizer,\ hermetic_environment_as(**self._env): try: # Setup the Exiter's finalizer. self._exiter.set_finalizer(finalizer) # Clean global state. clean_global_runtime_state(reset_subsystem=True) # Re-raise any deferred exceptions, if present. self._raise_deferred_exc() # Otherwise, conduct a normal run. runner = LocalPantsRunner.create( self._exiter, self._args, self._env, self._target_roots, self._graph_helper, self._options_bootstrapper ) runner.set_start_time(self._maybe_get_client_start_time_from_env(self._env)) runner.run() except KeyboardInterrupt: self._exiter.exit(1, msg='Interrupted by user.\n') except Exception: self._exiter.handle_unhandled_exception(add_newline=True) else: self._exiter.exit(0)
def test_hermetic_environment(self): self.assertIn('USER', os.environ) with hermetic_environment_as(**{}): self.assertNotIn('USER', os.environ)
def post_fork_child(self): """Post-fork child process callback executed via ProcessManager.daemonize().""" # Set the Exiter exception hook post-fork so as not to affect the pantsd processes exception # hook with socket-specific behavior. Note that this intentionally points the faulthandler # trace stream to sys.stderr, which at this point is still a _LoggerStream object writing to # the `pantsd.log`. This ensures that in the event of e.g. a hung but detached pantsd-runner # process that the stacktrace output lands deterministically in a known place vs to a stray # terminal window. # TODO: test the above! ExceptionSink.reset_exiter(self._exiter) ExceptionSink.reset_interactive_output_stream(sys.stderr.buffer if PY3 else sys.stderr) ExceptionSink.reset_signal_handler(DaemonSignalHandler()) # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self._args # Set context in the process title. set_process_title('pantsd-runner [{}]'.format(' '.join(self._args))) # Broadcast our process group ID (in PID form - i.e. negated) to the remote client so # they can send signals (e.g. SIGINT) to all processes in the runners process group. NailgunProtocol.send_pid(self._socket, os.getpid()) NailgunProtocol.send_pgrp(self._socket, os.getpgrp() * -1) # Stop the services that were paused pre-fork. for service in self._services.services: service.terminate() # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio(self._socket, self._env) as finalizer,\ hermetic_environment_as(**self._env): try: # Setup the Exiter's finalizer. self._exiter.set_finalizer(finalizer) # Clean global state. clean_global_runtime_state(reset_subsystem=True) # Otherwise, conduct a normal run. runner = LocalPantsRunner.create( self._exiter, self._args, self._env, self._target_roots, self._graph_helper, self._options_bootstrapper ) runner.set_start_time(self._maybe_get_client_start_time_from_env(self._env)) # Re-raise any deferred exceptions, if present. self._raise_deferred_exc() runner.run() except KeyboardInterrupt: self._exiter.exit_and_fail('Interrupted by user.\n') except _GracefulTerminationException as e: ExceptionSink.log_exception( 'Encountered graceful termination exception {}; exiting'.format(e)) self._exiter.exit(e.exit_code) except Exception: # TODO: We override sys.excepthook above when we call ExceptionSink.set_exiter(). That # excepthook catches `SignalHandledNonLocalExit`s from signal handlers, which isn't # happening here, so something is probably overriding the excepthook. By catching Exception # and calling this method, we emulate the normal, expected sys.excepthook override. ExceptionSink._log_unhandled_exception_and_exit() else: self._exiter.exit(PANTS_SUCCEEDED_EXIT_CODE)
def run(self): # Ensure anything referencing sys.argv inherits the Pailgun'd args. sys.argv = self.args # Invoke a Pants run with stdio redirected and a proxied environment. with self.nailgunned_stdio( self.maybe_shutdown_socket, self.env) as finalizer, DaemonExiter.override_global_exiter( self.maybe_shutdown_socket, finalizer), hermetic_environment_as(**self.env): exit_code = PANTS_SUCCEEDED_EXIT_CODE try: # Clean global state. clean_global_runtime_state(reset_subsystem=True) options_bootstrapper = OptionsBootstrapper.create( args=self.args, env=self.env) options, build_config = LocalPantsRunner.parse_options( options_bootstrapper) global_options = options.for_global_scope() session = self.scheduler_service.prepare_graph(options) specs = SpecsCalculator.create( options=options, session=session.scheduler_session, exclude_patterns=tuple( global_options.exclude_target_regexp), tags=tuple(global_options.tag) if global_options.tag else (), ) if options.help_request: help_printer = HelpPrinter( options=options, union_membership=UnionMembership( build_config.union_rules()), ) exit_code = help_printer.print_help() else: exit_code = self.scheduler_service.graph_run_v2( session, specs, options, options_bootstrapper) # self.scheduler_service.graph_run_v2 will already run v2 or ambiguous goals. We should # only enter this code path if v1 is set. if global_options.v1: with ExceptionSink.exiter_as_until_exception( lambda _: PantsRunFailCheckerExiter()): runner = LocalPantsRunner.create( self.env, options_bootstrapper, specs, session) env_start_time = self.env.pop( "PANTSD_RUNTRACKER_CLIENT_START_TIME", None) start_time = float( env_start_time) if env_start_time else None runner.set_start_time(start_time) runner.run() except KeyboardInterrupt: self._exiter.exit_and_fail("Interrupted by user.\n") except _PantsRunFinishedWithFailureException as e: ExceptionSink.log_exception( "Pants run failed with exception: {}; exiting".format(e)) self._exiter.exit(e.exit_code) except Exception as e: # TODO: We override sys.excepthook above when we call ExceptionSink.set_exiter(). That # excepthook catches `SignalHandledNonLocalExit`s from signal handlers, which isn't # happening here, so something is probably overriding the excepthook. By catching Exception # and calling this method, we emulate the normal, expected sys.excepthook override. ExceptionSink._log_unhandled_exception_and_exit(exc=e) else: self._exiter.exit(exit_code)
def test_hermetic_environment(self) -> None: with self.ensure_user_defined_in_environment(): with hermetic_environment_as(): self.assertNotIn("USER", os.environ)