diff --git a/CHANGELOG.md b/CHANGELOG.md index 4579406..3bb1e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The client here will eventually be released as "spython" (and eventually to singularity on pypi), and the versions here will coincide with these releases. ## [master](https://github.com/singularityhub/singularity-cli/tree/master) + - support for background process with client run (0.2.11) - parser bugfixes, arg from Docker not properly parsed (0.2.1) - version checks removed to support Singularity 3.x and above (0.2.0) - adding support for SIF (oras pull) (0.1.18) diff --git a/spython/main/base/command.py b/spython/main/base/command.py index 9ac1536..3e19e8d 100644 --- a/spython/main/base/command.py +++ b/spython/main/base/command.py @@ -15,7 +15,8 @@ def init_command(self, action, flags=None): - """return the initial Singularity command with any added flags. + """ + Return the initial Singularity command with any added flags. Parameters ========== @@ -37,7 +38,8 @@ def init_command(self, action, flags=None): def generate_bind_list(self, bindlist=None): - """generate bind string will take a single string or list of binds, and + """ + Generate bind string will take a single string or list of binds, and return a list that can be added to an exec or run command. For example, the following map as follows: @@ -81,7 +83,8 @@ def generate_bind_list(self, bindlist=None): def send_command(self, cmd, sudo=False, stderr=None, stdout=None): - """send command is a non interactive version of run_command, meaning + """ + Send command is a non interactive version of run_command, meaning that we execute the command and return the return value, but don't attempt to stream any content (text from the screen) back to the user. This is useful for commands interacting with OCI bundles. @@ -109,9 +112,11 @@ def run_command( return_result=False, sudo_options=None, environ=None, + background=False, ): - """run_command is a wrapper for the global run_command, checking first + """ + Run_command is a wrapper for the global run_command, checking first for sudo and exiting on error if needed. The message is returned as a list of lines for the calling function to parse, and stdout uses the parent process so it appears for the user. @@ -124,7 +129,7 @@ def run_command( return_result: return the result, if not successful (default False). sudo_options: string or list of strings that will be passed as options to sudo On success, returns result. - + background: run the instance in the background (just Popen) """ # First preference to function, then to client setting if quiet is None: @@ -137,8 +142,12 @@ def run_command( quiet=quiet, sudo_options=sudo_options, environ=environ, + background=background, ) + if background: + return + # If one line is returned, squash dimension if len(result["message"]) == 1: result["message"] = result["message"][0] diff --git a/spython/main/instances.py b/spython/main/instances.py index 4fe0efe..53cabfc 100644 --- a/spython/main/instances.py +++ b/spython/main/instances.py @@ -19,7 +19,8 @@ def list_instances( sudo_options=None, singularity_options=None, ): - """list instances. For Singularity, this is provided as a command sub + """ + List instances. For Singularity, this is provided as a command sub group. singularity instance list @@ -106,7 +107,8 @@ def list_instances( def stopall(self, sudo=False, quiet=True, singularity_options=None): - """stop ALL instances. This command is only added to the command group + """ + Stop ALL instances. This command is only added to the command group as it doesn't make sense to call from a single instance Parameters diff --git a/spython/main/run.py b/spython/main/run.py index fa03349..03fb3dc 100644 --- a/spython/main/run.py +++ b/spython/main/run.py @@ -25,6 +25,7 @@ def run( singularity_options=None, return_result=False, quiet=False, + background=False, ): """ run will run the container, with or withour arguments (which @@ -100,7 +101,10 @@ def run( if not quiet: bot.info(" ".join(cmd)) - if not stream: + if background: + return self._run_command(cmd, sudo=sudo, background=True) + + elif not stream: result = self._run_command(cmd, sudo=sudo, return_result=return_result) else: return stream_command(cmd, sudo=sudo) diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py index 5f8cd0a..2fd32f4 100644 --- a/spython/utils/terminal.py +++ b/spython/utils/terminal.py @@ -27,14 +27,15 @@ def _process_sudo_cmd(cmd, sudo, sudo_options): if sudo and sudo_options is not None: if isinstance(sudo_options, str): sudo_options = shlex.split(sudo_options) - cmd = ["sudo"] + sudo_options + cmd + cmd = ["sudo", "-E"] + sudo_options + cmd elif sudo: - cmd = ["sudo"] + cmd - return cmd + cmd = ["sudo", "-E"] + cmd + return [x for x in cmd if x] def check_install(software="singularity", quiet=True): - """check_install will attempt to run the singularity command, and + """ + check_install will attempt to run the singularity command, and return True if installed. The command line utils will not run without this check. """ @@ -66,7 +67,8 @@ def which(software="singularity"): def get_singularity_version(): - """get the full singularity client version as reported by + """ + get the full singularity client version as reported by singularity --version [...]. For Singularity 3.x, this means: "singularity version 3.0.1-1" """ @@ -85,17 +87,23 @@ def get_singularity_version(): def get_userhome(): - """get the user home based on the effective uid""" + """ + Get the user home based on the effective uid + """ return pwd.getpwuid(os.getuid())[5] def get_username(): - """get the user name based on the effective uid""" + """ + Get the user name based on the effective uid + """ return pwd.getpwuid(os.getuid())[0] def get_singularity_version_info(): - """get the full singularity client version as a semantic version" """ + """ + Get the full singularity client version as a semantic version" + """ version_string = get_singularity_version() prefix = "singularity version " if version_string.startswith(prefix): @@ -106,7 +114,9 @@ def get_singularity_version_info(): def get_installdir(): - """get_installdir returns the installation directory of the application""" + """ + Get_installdir returns the installation directory of the application + """ return os.path.abspath(os.path.dirname(os.path.dirname(__file__))) @@ -117,7 +127,8 @@ def stream_command( sudo_options=None, output_type="stdout", ): - """stream a command (yield) back to the user, as each line is available. + """ + Stream a command (yield) back to the user, as each line is available. # Example usage: results = [] @@ -167,9 +178,11 @@ def run_command( quiet=False, sudo_options=None, environ=None, + background=False, ): - """run_command uses subprocess to send a command to the terminal. If + """ + run_command uses subprocess to send a command to the terminal. If capture is True, we use the parent stdout, so the progress bar (and other commands of interest) are piped to the user. This means we don't return the output to parse. @@ -184,6 +197,7 @@ def run_command( option can print a progress bar, but won't return the lines as output. sudo_options: string or list of strings that will be passed as options to sudo + background: run in background and don't try to get output. """ cmd = _process_sudo_cmd(cmd, sudo, sudo_options) @@ -192,8 +206,12 @@ def run_command( stdout = subprocess.PIPE # Use the parent stdout and stderr + if background: + subprocess.Popen(cmd, env=environ) + return process = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=stdout, env=environ) + lines = [] found_match = False @@ -222,7 +240,8 @@ def run_command( def format_container_name(name, special_characters=None): - """format_container_name will take a name supplied by the user, + """ + format_container_name will take a name supplied by the user, remove all special characters (except for those defined by "special-characters" and return the new image name. """ @@ -232,7 +251,8 @@ def format_container_name(name, special_characters=None): def split_uri(container): - """Split the uri of a container into the protocol and image part + """ + Split the uri of a container into the protocol and image part An empty protocol is returned if none found. A trailing slash is removed from the image part. @@ -247,5 +267,7 @@ def split_uri(container): def remove_uri(container): - """remove_uri will remove docker:// or shub:// or library:// from the uri""" + """ + remove_uri will remove docker:// or shub:// or library:// from the uri + """ return split_uri(container)[1] diff --git a/spython/version.py b/spython/version.py index 6248fe9..9ea7560 100644 --- a/spython/version.py +++ b/spython/version.py @@ -5,7 +5,7 @@ # with this file, You can obtain one at http://mozilla.org/MPL/2.0/. -__version__ = "0.2.1" +__version__ = "0.2.11" AUTHOR = "Vanessa Sochat" AUTHOR_EMAIL = "vsoch@users.noreply.github.com" NAME = "spython"