def run(*popenargs, input=None, timeout=None, check=False, **kwargs): """Emulating the run routine found in future subprocess modules """ if input is not None: if 'stdin' in kwargs: raise ValueError('stdin and input args may not both be used.') kwargs['stdin'] = subprocess.PIPE with subprocess.Popen(*popenargs, **kwargs) as process: try: stdout, stderr = process.communicate(input, timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() raise subprocess.TimeoutExpired(process.args, timeout, output=stdout, stderr=stderr) except: process.kill() process.wait() raise retcode = process.poll() if check and retcode: raise subprocess.CalledProcessError(retcode, process.args, output=stdout + stderr) return CompletedProcess(process.args, retcode, stdout, stderr)
def test_pull(self): source = "source-file" destination = mock.MagicMock(spec=io.BufferedIOBase) self.popen_mock.return_value.stdin = None self.popen_mock.return_value.stdout.read.return_value = b"stdout data" self.popen_mock.return_value.communicate.side_effect = ( subprocess.TimeoutExpired("foo", 1), (b"communicate data", b"error"), ) self.multipass_command.pull_file(source=source, destination=destination) self.assertEqual( [ mock.call( ["multipass", "transfer", source, "-"], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, ), mock.call().stdout.read(1024), mock.call().communicate(timeout=1), mock.call().communicate(timeout=1), ], self.popen_mock.mock_calls, ) self.assertEqual( [mock.call(b"stdout data"), mock.call(b"communicate data")], destination.write.mock_calls, )
def test_buffered_push(self): source = mock.MagicMock(spec=io.BufferedIOBase) source.read.side_effect = (b"read data", b"") destination = "{}/destination-file".format(self.instance_name) self.popen_mock.return_value.stdout = None self.popen_mock.return_value.communicate.side_effect = ( subprocess.TimeoutExpired("foo", 1), (b"communicate data", b"error"), ) self.multipass_command.push_file( source=source, destination=destination, bufsize=9 ) self.assertEqual( [ mock.call( ["multipass", "transfer", "-", destination], stdin=subprocess.PIPE ), mock.call().stdin.write(b"read data"), mock.call().communicate(timeout=1), mock.call().communicate(timeout=1), ], self.popen_mock.mock_calls, ) self.assertEqual(source.read.mock_calls, [mock.call(9), mock.call(9)])
def start(self): self.proc = subprocess.Popen([ '{}/lightningd/lightningd'.format( LIGHTNING_SRC), '--lightning-dir={}'.format( self.lightning_dir), '--funding-confirms=3', '--dev-force-tmp-channel-id=0000000000000000000000000000000000000000000000000000000000000000', '--dev-force-privkey=0000000000000000000000000000000000000000000000000000000000000001', '--dev-force-bip32-seed=0000000000000000000000000000000000000000000000000000000000000001', '--dev-force-channel-secrets=0000000000000000000000000000000000000000000000000000000000000010/0000000000000000000000000000000000000000000000000000000000000011/0000000000000000000000000000000000000000000000000000000000000012/0000000000000000000000000000000000000000000000000000000000000013/0000000000000000000000000000000000000000000000000000000000000014/FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', '--dev-bitcoind-poll=1', '--dev-fast-gossip', '--dev-gossip-time=1565587763', '--bind-addr=127.0.0.1:{}'.format( self.lightning_port), '--network=regtest', '--bitcoin-rpcuser=rpcuser', '--bitcoin-rpcpassword=rpcpass', '--bitcoin-rpcport={}'.format( self.bitcoind.port), '--log-level=debug', '--log-file=log' ] + self.startup_flags) self.rpc = lightning.LightningRpc( os.path.join(self.lightning_dir, "regtest", "lightning-rpc")) def node_ready(rpc): try: rpc.getinfo() return True except Exception: return False if not wait_for(lambda: node_ready(self.rpc)): raise subprocess.TimeoutExpired(self.proc, "Could not contact lightningd") # Make sure that we see any funds that come to our wallet for i in range(5): self.rpc.newaddr()
def call_process(cmd, timeout=10, input='/dev/null', output=False, error=False, checkRC=False): assert cmd != '' curr = os.getcwd() stdout = os.path.join(curr, 'stdout') stderr = os.path.join(curr, 'stderr') command = '{} {} {} < "{}" 1>"{}" 2>"{}"'.format(config['tests']['timeout_prog'], timeout, cmd, input, stdout, stderr) #print('call_process', command) rc = os.system(command) return_code = (rc >> 8) & 0xff if return_code == 124: #gtimeout TIMEOUT status raise subprocess.TimeoutExpired(command, timeout) def get_data(filename): with open(filename, 'r', errors='ignore') as f: return f.read() if checkRC and return_code != 0: raise subprocess.CalledProcessError(return_code, command, output=get_data(stdout),stderr=get_data(stderr)) if output and error: return get_data(stdout) and get_data(stderr) if output: return get_data(stdout) if error: return get_data(stderr)
async def test_subprocess_exceptions(caplog: LogCaptureFixture, hass: HomeAssistant) -> None: """Test that notify subprocess exceptions are handled correctly.""" with patch("homeassistant.components.command_line.notify.subprocess.Popen" ) as check_output: check_output.return_value.__enter__ = check_output check_output.return_value.communicate.side_effect = [ subprocess.TimeoutExpired("cmd", 10), None, subprocess.SubprocessError(), ] await setup_test_service(hass, {"command": "exit 0"}) assert await hass.services.async_call(DOMAIN, "test", {"message": "error"}, blocking=True) assert check_output.call_count == 2 assert "Timeout for command" in caplog.text assert await hass.services.async_call(DOMAIN, "test", {"message": "error"}, blocking=True) assert check_output.call_count == 4 assert "Error trying to exec command" in caplog.text
def runCmd(cmd, cmd_timeout=300): ''' run command without showing console window on windows - return stdout and stderr as strings ''' startupinfo = None output = "" output_err = "" debug_log("runCmd: {}".format(cmd)) if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW try: proc = subprocess.Popen(cmd, bufsize=-1, startupinfo=startupinfo, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=None, shell=False, universal_newlines=False) except SubprocessError as e: proc = None debug_log("exception in runCmd: {}".format(e), logging.ERROR) if proc is not None: try: outputb, output_errb = proc.communicate() output = outputb.decode('utf-8', 'replace') output_err = output_errb.decode('utf-8', 'replace') except subprocess.TimeoutExpired(timeout=cmd_timeout): proc.kill() debug_log("runCmd: Process killed due to timeout", logging.WARNING) else: debug_log("runCmd: Proc was none", logging.WARNING) return output, output_err
def run(*popenargs, timeout=None, check=False, ssh=None, **kwargs): """Run command for ZFS functions that also works over ssh.""" if ssh is None: with sp.Popen(*popenargs, **kwargs) as process: try: stdout, stderr = process.communicate(timeout=timeout) except sp.TimeoutExpired: process.kill() stdout, stderr = process.communicate() raise sp.TimeoutExpired(process.args, timeout, output=stdout, stderr=stderr) except: process.kill() process.wait() raise retcode = process.poll() else: args = ' '.join(popenargs[0]) if not isinstance(popenargs[0], str) else popenargs[0] try: stdin, stdout, stderr = ssh.exec_command(args, *popenargs[1:], timeout=timeout) if kwargs.get('stdin', None): shutil.copyfileobj(kwargs['stdin'], stdin, 128 * 1024) stdin.close() retcode = stdout.channel.recv_exit_status() stdout, stderr = ''.join(stdout.readlines()), ''.join( stderr.readlines()) except socket.timeout: stdout, stderr = None, None raise sp.TimeoutExpired(popenargs[0], timeout, output=stdout, stderr=stderr) if check and retcode: raise sp.CalledProcessError(retcode, popenargs[0], output=stdout, stderr=stderr) return sp.CompletedProcess(popenargs[0], retcode, stdout, stderr)
def docker_runs(args, images, docker_args=(), image_args=()): docker_argv = check_docker() container_id = f"satex{os.getpid()}" argv = ["run", "--name", container_id, "--rm"] if hasattr(args, "timeout"): argv += ["-e", f"TIMEOUT={args.timeout}"] quiet = hasattr(args, "quiet") and args.quiet for opt in _docker_opts: if getattr(args, opt) is not None: val = getattr(args, opt) if isinstance(val, list): for v in val: if opt == "volume": v = easy_volume(v) argv += ["--%s" % opt, v] else: argv += ["--%s" % opt, val] argv += list(docker_args) image_argv = ["--mode", args.mode ] if hasattr(args, "mode") and args.mode else [] image_argv += list(image_args) run_args = {} if quiet: run_args["stdout"] = subprocess.DEVNULL run_args["stderr"] = subprocess.DEVNULL global stop stop = False for image in images: image = f"{DOCKER_NS}/{image}" prepare_image(args, docker_argv, image) cmd = docker_argv + argv + [image] + image_argv if args.pretend: print(" ".join(cmd)) else: def killer(s, f): global stop stop = True warn("Killing solver...") argv = docker_argv + ["kill", container_id] subprocess.run(argv, stdout=subprocess.DEVNULL) signal.signal(signal.SIGINT, killer) info(" ".join(cmd)) if not quiet else None ret = subprocess.run(cmd, **run_args).returncode signal.signal(signal.SIGINT, signal.SIG_DFL) if stop: sys.exit(1) if ret == 124: if args.fail_if_timeout: raise subprocess.TimeoutExpired(image, args.timeout) elif not quiet: warn(f"{image} timeout") else: if not (ret == 0 or 10 <= ret <= 20): if not quiet: error(f"Solver failed with return code {ret}") if not args.pretend: return ret
def run_subprocess(*popenargs, input=None, capture_output=False, timeout=None, check=False, communicate_fn=None, **kwargs): """ a clone of :function:`subprocess.run` which allows custom handling of communication :param popenargs: :param input: :param capture_output: :param timeout: :param check: :param communicate_fn: :param kwargs: :return: """ if input is not None: if 'stdin' in kwargs: raise ValueError('stdin and input arguments may not both be used.') kwargs['stdin'] = subprocess.PIPE if capture_output: if kwargs.get('stdout') is not None or kwargs.get( 'stderr') is not None: raise ValueError('stdout and stderr arguments may not be used ' 'with capture_output.') kwargs['stdout'] = subprocess.PIPE kwargs['stderr'] = subprocess.PIPE def communicate(process, input=None, timeout=None): return (communicate_fn(process, input=input, timeout=timeout) if communicate_fn else process.communicate(input=input, timeout=timeout)) with subprocess.Popen(*popenargs, **kwargs) as process: try: stdout, stderr = communicate(process, input, timeout=timeout) except subprocess.TimeoutExpired as e: process.kill() if sys.platform == 'win32': e.stdout, e.stderr = communicate(process) else: process.wait() raise subprocess.TimeoutExpired(process.args, timeout, output=stdout, stderr=stderr) except: # also handles kb interrupts process.kill() raise retcode = process.poll() if check and retcode: raise subprocess.CalledProcessError(retcode, process.args, output=stdout, stderr=stderr) return subprocess.CompletedProcess(process.args, retcode, stdout, stderr)
def run_helper( cmd: List[str], check_exit_code: bool = True, timeout: Optional[float] = None, env: Optional[Dict[str, str]] = None, working_dir: Optional[Path] = None, ) -> types.CompletedProcess: check( bool(cmd) and cmd[0] is not None and isinstance(cmd[0], str), AssertionError("run takes a list of str, not a str"), ) # When using catchsegv, only SIGSEGV will cause a backtrace to be printed. # We can also include SIGABRT by setting the following environment variable. if cmd[0].endswith("catchsegv"): if env is None: env = {} env["SEGFAULT_SIGNALS"] = "SEGV ABRT" env_child: Optional[Dict[str, str]] = None if env: log(f"Extra environment variables are: {env}") env_child = os.environ.copy() env_child.update(env) with subprocess.Popen( cmd, encoding="utf-8", errors="ignore", start_new_session=True, env=env_child, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_dir, ) as process: try: stdout, stderr = process.communicate(input=None, timeout=timeout) except subprocess.TimeoutExpired: try: posix_kill_group(process) except AttributeError: process.kill() stdout, stderr = process.communicate() assert timeout # noqa raise subprocess.TimeoutExpired(process.args, timeout, stdout, stderr) except: # noqa try: posix_kill_group(process) except AttributeError: process.kill() raise exit_code = process.poll() if check_exit_code and exit_code != 0: raise subprocess.CalledProcessError(exit_code, process.args, stdout, stderr) return subprocess.CompletedProcess(process.args, exit_code, stdout, stderr)
def test_does_not_crash_on_exception(): GIT_REPO = "[email protected]:rossumai/_non_existing_repo_" with patch("chart_updater.git.run") as run: run.side_effect = subprocess.TimeoutExpired("cmd", 1) updater = Updater(Git(GIT_REPO), HelmRepo(HELM_REPO_URL)) updater.update_loop(one_shot=True) run.assert_called_once()
def test_exec_cmd_timed_out_without_serial(self, mock_run_command): mock_run_command.side_effect = subprocess.TimeoutExpired( cmd='mock_command', timeout=0.01) with self.assertRaisesRegex( adb.AdbTimeoutError, 'Timed out executing command "adb fake-cmd" after 0.01s.'): adb.AdbProxy().fake_cmd(timeout=0.01)
async def read_async(self) -> str or None: self._set_timeout_handler() await self._create_async_subprocess() stdout, stderr = await self._process.communicate() self._timeout_handler and self._timeout_handler.cancel() if self._is_timeout: raise subprocess.TimeoutExpired(self._cmd, self._timeout) return self._read_std(stdout, stderr)
def popen_mock(args, **kwargs): # force the login processes to raise TimeoutExpired if args[2] == 'login': mock_process = create_mock_process() mock_process.communicate.side_effect = subprocess.TimeoutExpired( cmd='docker login', timeout=30) return mock_process return create_mock_process()
def subprocess_run(*popenargs, input=None, timeout=None, check=False, **kwargs): """Run command with arguments and return a CompletedProcess instance. The returned instance will have attributes args, returncode, stdout and stderr. By default, stdout and stderr are not captured, and those attributes will be None. Pass stdout=PIPE and/or stderr=PIPE in order to capture them. If check is True and the exit code was non-zero, it raises a CalledProcessError. The CalledProcessError object will have the return code in the returncode attribute, and output & stderr attributes if those streams were captured. If timeout is given, and the process takes too long, a TimeoutExpired exception will be raised. There is an optional argument "input", allowing you to pass a string to the subprocess's stdin. If you use this argument you may not also use the Popen constructor's "stdin" argument, as it will be used internally. The other arguments are the same as for the Popen constructor. If universal_newlines=True is passed, the "input" argument must be a string and stdout/stderr in the returned object will be strings rather than bytes. """ #pylint: disable=redefined-builtin if input is not None: if 'stdin' in kwargs: raise ValueError('stdin and input arguments may not both be used.') kwargs['stdin'] = subprocess.PIPE with subprocess.Popen(*popenargs, **kwargs) as process: try: stdout, stderr = process.communicate(input, timeout=timeout) except subprocess.TimeoutExpired: process.kill() stdout, stderr = process.communicate() raise subprocess.TimeoutExpired(process.args, timeout, output=stdout, stderr=stderr) except: process.kill() process.wait() raise retcode = process.poll() if check and retcode: raise subprocess.CalledProcessError(retcode, process.args, output=stdout, stderr=stderr) return CompletedProcess(process.args, retcode, stdout, stderr)
def test_long_command_no_status_output(self, popen, sleep): proc = popen.return_value proc.returncode = 1 # FIXME: what's the real exit code for a SIGKILL? proc.poll.return_value = False # command not completed proc.wait.side_effect = [ subprocess.TimeoutExpired('gogo', 2), lambda *_: True] log_mock = MagicMock() runner('gogo', stop_event=make_event(is_set=True), log=log_mock) self.assertEqual(log_mock.call_count, 4) # calls to add in spacing
def communicate(self, msg, timeout=None): """Feeds msg to stdin, waits for peer to exit, and returns (stdout, stderr).""" #Stop read loop - it seems we can't 'communicate' while doing readline self.write_to_stdin("say_quit\n") try: self.expect_output("QUITTING", 1) except subprocess.TimeoutExpired: raise subprocess.TimeoutExpired("Peer unresponsive, cannot 'communicate'", 1) return self.process.communicate(bytes(msg, "utf-8"), timeout)
def test_javac_timeout(self): JAVA_HOME = 'java_home' jdk = JDK(JAVA_HOME) subprocess.check_call = MagicMock( side_effect=subprocess.TimeoutExpired('', 0)) self.assertFalse(jdk.run_javac('file.java', 10, None))
def run(*popenargs, timeout=None, check=False, ssh=None, **kwargs): """Run command with ZFS arguments and return a CompletedProcess instance. Works over ssh. Parameters: ---------- *popenargs : {} Variable length argument list, same as Popen constructor **kwargs : {} Arbitrary keyword arguments, same as Popen constructor timeout : {float}, optional Timeout in seconds, if process takes too long TimeoutExpired will be raised (the default is None, meaning no timeout) check : {bool}, optional Check return code (the default is False, meaning return code will not be checked) ssh : {ssh.SSH}, optional Open ssh connection for remote execution (the default is None) Raises ------ sp.TimeoutExpired Raised if process takes longer than given timeout sp.CalledProcessError Raised if check=True and return code != 0 Returns ------- subprocess.CompletedProcess Return instance of CompletedProcess with given return code, stdout and stderr """ logger = logging.getLogger(__name__) if ssh: popenargs = (ssh.cmd + popenargs[0], *popenargs[1:]) logger.log(8, "RUN: {:s}".format(' '.join(*popenargs))) with sp.Popen(*popenargs, **kwargs) as process: try: stdout, stderr = process.communicate(timeout=timeout) except sp.TimeoutExpired: process.kill() stdout, stderr = process.communicate() raise sp.TimeoutExpired(process.args, timeout, output=stdout, stderr=stderr) except: process.kill() process.wait() raise retcode = process.poll() if check and retcode: raise sp.CalledProcessError(retcode, popenargs[0], output=stdout, stderr=stderr) return CompletedProcess(popenargs[0], retcode, stdout, stderr)
def test_wait_kills_after_timeout(self, *_): """Tests that if a TimeoutExpired error is thrown during wait, the process is killed.""" process = Process('cmd') process._process = mock.Mock() process._process.wait.side_effect = subprocess.TimeoutExpired('', '') process.wait(0) self.assertEqual(process._kill_process.called, True)
def test_execute_timeout(popen_mock, process_mock): # pylint: disable=redefined-outer-name side_effects = (subprocess.TimeoutExpired('command', 30), ('Other STDOUT', 'Other STDERR')) process_mock.communicate = MagicMock(side_effect=side_effects) popen_mock.configure_mock(return_value=process_mock) with pytest.raises(executor.Timeout): executor.execute('command') process_mock.kill.assert_called_with()
def test_timeout(self, mocked_popen, mocked_warning, _): """Tests run_container_command behaves as expected when the command times out.""" popen_magic_mock = mock.MagicMock() mocked_popen.return_value = popen_magic_mock popen_magic_mock.communicate.side_effect = subprocess.TimeoutExpired( ['cmd'], '1') result = docker.run_container_command(self.ARGUMENTS) self.assertEqual(mocked_warning.call_count, 1) self.assertTrue(result.timed_out)
def test_retry_when_failed_start(self): self.mock_subprocess.Popen.return_value.wait.side_effect = [ lambda **kwargs: None, # Indicates a problem, as wait() immediately returns subprocess.TimeoutExpired('cmd', 2) # Success as wait() times out ] mitmproxy.start('127.0.0.1', 0, {}) # Mitmproxy took two attempts to start self.assertEqual(2, self.mock_subprocess.Popen.call_count)
def wait(self, timeout=None): if timeout and timeout < self.__wait: self.__wait -= timeout raise subprocess.TimeoutExpired(self.args, timeout) if self.__thread is not None: self.__thread.join() if self.returncode is None and self.__returncode is not None: self.returncode = self.__returncode if self.__thread.exception: raise self.__thread.exception return self.returncode
def test_query_buck_relative_paths(self, find_buck_root) -> None: with patch.object(subprocess, "check_output") as buck_query: buck_query.return_value = json.dumps({ "targetA": { "buck.base_path": "src/python", "srcs": { "a.py": "a.py", "b/c.py": "otherDirectory/c.py" }, }, "targetB": { "buck.base_path": "src/python", "buck.base_module": "com.companyname", "srcs": { "package.py": "package.py" }, }, }).encode("utf-8") paths = [ "/PROJECT_BUCK_ROOT/src/python/a.py", # tracked paths "/PROJECT_BUCK_ROOT/src/python/b/c.py", "/PROJECT_BUCK_ROOT/src/python/package.py", "/PROJECT_BUCK_ROOT/src/java/python/a.py", # untracked paths "/PROJECT_BUCK_ROOT/com/companyname/package.py" "/OTHER_PROJECT/src/python/a.py", ] self.assertDictEqual( buck.query_buck_relative_paths(paths, targets=["targetA", "targetB"]), { "/PROJECT_BUCK_ROOT/src/python/a.py": "src/python/a.py", "/PROJECT_BUCK_ROOT/src/python/b/c.py": "src/python/otherDirectory/c.py", "/PROJECT_BUCK_ROOT/src/python/package.py": "com/companyname/package.py", }, ) self.assertDictEqual( buck.query_buck_relative_paths(paths, targets=["targetA"]), { "/PROJECT_BUCK_ROOT/src/python/a.py": "src/python/a.py", "/PROJECT_BUCK_ROOT/src/python/b/c.py": "src/python/otherDirectory/c.py", }, ) with patch.object(subprocess, "check_output", side_effect=subprocess.TimeoutExpired("cmd", 30)): self.assertRaises(buck.BuckException, buck.query_buck_relative_paths, [], ["targetA"])
def test_docker_pull_image_timeout(self): with patch( "subprocess.run", side_effect=subprocess.TimeoutExpired(cmd=[], timeout=DOCKER_TIMEOUT), ): with pytest.raises( click.exceptions.ClickException, match='docker pull ggshield-non-existant" timed out', ): docker_pull_image("ggshield-non-existant", DOCKER_TIMEOUT)
def test_stop_subprocess_suppress_kill(): """It should suppress kill signal errors.""" mock_process = Mock(spec=subprocess.Popen, strict=True, name="Popen") mock_process.communicate.side_effect = [ subprocess.TimeoutExpired([], timeout=3), subprocess.TimeoutExpired([], timeout=3), subprocess.CalledProcessError(9, ["test"], stderr="stderr of test") ] stdout, stderr = stop_process(mock_process, timeout=3) assert stdout is None assert stderr == "stderr of test" assert mock_process.method_calls == [ call.send_signal(signal.SIGINT), call.communicate(timeout=3), call.send_signal(signal.SIGTERM), call.communicate(timeout=3), call.send_signal(signal.SIGKILL), call.communicate(timeout=None), ]
def stop(self): if not self.running: return end_time = time() + self.timeout self._queue.put(self._end_of_stream) self._queue.join() self._process.stdin.flush() self._process.stdin.close() self.running = False try: if self._process.stdin: self._stdin_thread.join(timeout=self._remaining_time(end_time)) if self._stdin_thread.is_alive(): raise subprocess.TimeoutExpired(self._process.args, self.timeout) if self._process.stdout: self._stdout_thread.join( timeout=self._remaining_time(end_time)) if self._stdout_thread.is_alive(): raise subprocess.TimeoutExpired(self._process.args, self.timeout) if self._process.stderr: self._stderr_thread.join( timeout=self._remaining_time(end_time)) if self._stderr_thread.is_alive(): raise subprocess.TimeoutExpired(self._process.args, self.timeout) self.exit_code = self._process.wait( timeout=self._remaining_time(end_time)) except Exception as e: self.error = e self._process.kill() self._process.wait() finally: self._process = None self._queue = None
def run_docker_command(command: Union[str, List[str]], cwd: Optional[str] = None, user: Optional[Union[int, Tuple[int, int]]] = None, directory_mapping: Optional[Dict[str, str]] = None, timeout: Optional[float] = None, **kwargs) -> CommandResult: r"""Run a command inside a container based on the ``gcc-custom`` Docker image. :param command: The command to run. Should be either a `str` or a list of `str`. Note: they're treated the same way, because a shell is always spawn in the entry point. :param cwd: The working directory of the command to run. If None, uses the default (probably user home). :param user: The user ID to use inside the Docker container. Additionally, group ID can be specified by passing a tuple of two `int`\ s for this argument. If not specified, the current user and group IDs are used. As a special case, pass in ``0`` to run as root. :param directory_mapping: Mapping of host directories to container paths. Mapping is performed via "bind mount". :param timeout: Maximum running time for the command. If running time exceeds the specified limit, ``subprocess.TimeoutExpired`` is thrown. :param kwargs: Additional keyword arguments to pass to :meth:`ghcc.utils.run_command`. """ # Validate `command` argument, and append call to `bash` if `shell` is True. if isinstance(command, list): command = ' '.join(command) command = f"'{command}'" # Construct the `docker run` command. docker_command = ["docker", "run", "--rm"] for host, container in (directory_mapping or {}).items(): docker_command.extend(["-v", f"{os.path.abspath(host)}:{container}"]) if cwd is not None: docker_command.extend(["-w", cwd]) # Assign user and group IDs based on `user` argument. if user != 0: user_id: Union[str, int] = "`id -u $USER`" group_id: Union[str, int] = "`id -g $USER`" if user is not None: if isinstance(user, tuple): user_id, group_id = user else: user_id = user docker_command.extend(["-e", f"LOCAL_USER_ID={user_id}"]) docker_command.extend(["-e", f"LOCAL_GROUP_ID={group_id}"]) docker_command.append("gcc-custom") if timeout is not None: # Timeout is implemented by calling `timeout` inside Docker container. docker_command.extend(["timeout", f"{timeout}s"]) docker_command.append(command) ret = run_command(' '.join(docker_command), shell=True, **kwargs) # Check whether exceeded timeout limit by inspecting return code. if ret.return_code == 124: assert timeout is not None raise error_wrapper(subprocess.TimeoutExpired(ret.command, timeout, output=ret.captured_output)) return ret