diff --git a/shrinkwrap/commands/run.py b/shrinkwrap/commands/run.py index 3fa9d40776c3a36f090aced073ced9124a9c134e..699d8de718c60a4e48f04fb4aef394a1e47a81ad 100644 --- a/shrinkwrap/commands/run.py +++ b/shrinkwrap/commands/run.py @@ -61,6 +61,12 @@ def add_parser(parser, formatter): required=False, default=False, action='store_true', help="""If specified, logs will not be colorized.""") + cmdp.add_argument('-t', '--timeout', + required=False, default=None, type=int, + help="""Number of days after which to automatically shutdown the + container, if using a container runtime. Defaults to 1 day. + """) + return cmd_name @@ -229,7 +235,8 @@ def dispatch(args): # on the host or may execute commands in a container, depending on what # the user specified. with runtime.Runtime(name=args.runtime, image=config.get_image([resolveb], args), - ssh_agent_keys=args.ssh_agent_keys) as rt: + ssh_agent_keys=args.ssh_agent_keys, + timeout=args.timeout) as rt: for rtvar in resolver['run']['rtvars'].values(): if rtvar['type'] == 'path': rt.add_volume(rtvar['value']) diff --git a/shrinkwrap/utils/runtime.py b/shrinkwrap/utils/runtime.py index 52be6942a9c14c1f2edfbe43c5bca967d2d2f9fd..e8c5342668ca2f1f0ca6f59945f2dd6f6086f6dc 100644 --- a/shrinkwrap/utils/runtime.py +++ b/shrinkwrap/utils/runtime.py @@ -25,9 +25,11 @@ class Runtime: an abstracted runtime. Multiple runtimes are supported, identified by a `name`. The 'null' runtime simply executes the commands on the native host. The 'docker', 'docker-local', 'podman' and 'podman-local' runtimes - execute the commands in a container. + execute the commands in a container. `timeout`, if provided, is an + integer number of days after which the container will be shutdown if + still running. If None, the default timeout is used. """ - def __init__(self, *, name, image=None, ssh_agent_keys=None): + def __init__(self, *, name, image=None, ssh_agent_keys=None, timeout=None): self._rt = None self._mountpoints = set() @@ -36,6 +38,7 @@ class Runtime: is_mac = sys.platform.startswith('darwin') is_docker = name.startswith('docker') + is_container = is_docker or name.startswith('podman') # MacOS uses GIDs that overlap with already defined GIDs in the # container so we can't just bind the macos host UID/GID to the @@ -57,6 +60,23 @@ class Runtime: self._rt.set_user('shrinkwrap') self._rt.set_group('shrinkwrap') + # Tuxmake always starts the container with "sleep 1d" so that it + # automatically shuts down after 1 day if it ends up dangling. + # This is problematic for some long running FVP test cases, so + # we allow overriding it on the command line. We add a filter + # for spawn_container() which modifies the sleep argument which + # is the last element in the cmd list. Avoid hooking unless the + # user explicitly specified a timeout that is different from the + # expected default. + if is_container and timeout and timeout != 1: + spawn_container_orig = self._rt.spawn_container + def spawn_container_new(self, cmd): + if (cmd[-1] == '1d'): + cmd[-1] = f'{timeout}d' + return spawn_container_orig(cmd) + self._rt.spawn_container = \ + types.MethodType(spawn_container_new, self._rt) + for key in ssh_agent_keys: ssh_agent_lib.add(key) @@ -143,7 +163,13 @@ print(ip) assert _instance == self _instance = None if self._rt: - self._rt.cleanup() + # Horrible hack alert; I've observed that docker can + # take over 10 seconds to stop the container on at least + # 1 system running Ubuntu 24.04. Let's cleanup the + # container asynchonously in a child process. + if os.fork() == 0: + self._rt.cleanup() + exit() self._rt = None