diff --git a/app_launcher.py b/app_launcher.py new file mode 100644 index 0000000000..6dc1518370 --- /dev/null +++ b/app_launcher.py @@ -0,0 +1,49 @@ +"""Launch process that is not child process of python or OpenPype. + +This is written for linux distributions where process tree may affect what +is when closed or blocked to be closed. +""" + +import os +import sys +import subprocess +import json + + +def main(input_json_path): + """Read launch arguments from json file and launch the process. + + Expected that json contains "args" key with string or list of strings. + + Arguments are converted to string using `list2cmdline`. At the end is added + `&` which will cause that launched process is detached and running as + "background" process. + + ## Notes + @iLLiCiT: This should be possible to do with 'disown' or double forking but + I didn't find a way how to do it properly. Disown didn't work as + expected for me and double forking killed parent process which is + unexpected too. + """ + with open(input_json_path, "r") as stream: + data = json.load(stream) + + # Change environment variables + env = data.get("env") or {} + for key, value in env.items(): + os.environ[key] = value + + # Prepare launch arguments + args = data["args"] + if isinstance(args, list): + args = subprocess.list2cmdline(args) + + # Run the command as background process + shell_cmd = args + " &" + os.system(shell_cmd) + sys.exit(0) + + +if __name__ == "__main__": + # Expect that last argument is path to a json with launch args information + main(sys.argv[-1]) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 0b8a4dfa15..34926453cb 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -25,6 +25,7 @@ from .env_tools import ( from .terminal import Terminal from .execute import ( get_pype_execute_args, + get_linux_launcher_args, execute, run_subprocess, path_to_subprocess_arg, @@ -173,6 +174,7 @@ terminal = Terminal __all__ = [ "get_pype_execute_args", + "get_linux_launcher_args", "execute", "run_subprocess", "path_to_subprocess_arg", diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 86cf0229df..d0438e12a6 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1,8 +1,8 @@ import os import sys -import re import copy import json +import tempfile import platform import collections import inspect @@ -37,6 +37,7 @@ from .python_module_tools import ( modules_from_path, classes_from_module ) +from .execute import get_linux_launcher_args _logger = None @@ -1022,6 +1023,48 @@ class ApplicationLaunchContext: def manager(self): return self.application.manager + def _run_process(self): + # Windows and MacOS have easier process start + low_platform = platform.system().lower() + if low_platform in ("windows", "darwin"): + return subprocess.Popen(self.launch_args, **self.kwargs) + + # Linux uses mid process + # - it is possible that the mid process executable is not + # available for this version of OpenPype in that case use standard + # launch + launch_args = get_linux_launcher_args() + if launch_args is None: + return subprocess.Popen(self.launch_args, **self.kwargs) + + # Prepare data that will be passed to midprocess + # - store arguments to a json and pass path to json as last argument + # - pass environments to set + json_data = { + "args": self.launch_args, + "env": self.kwargs.pop("env", {}) + } + # Create temp file + json_temp = tempfile.NamedTemporaryFile( + mode="w", prefix="op_app_args", suffix=".json", delete=False + ) + json_temp.close() + json_temp_filpath = json_temp.name + with open(json_temp_filpath, "w") as stream: + json.dump(json_data, stream) + + launch_args.append(json_temp_filpath) + + # Create mid-process which will launch application + process = subprocess.Popen(launch_args, **self.kwargs) + # Wait until the process finishes + # - This is important! The process would stay in "open" state. + process.wait() + # Remove the temp file + os.remove(json_temp_filpath) + # Return process which is already terminated + return process + def launch(self): """Collect data for new process and then create it. @@ -1058,8 +1101,10 @@ class ApplicationLaunchContext: self.app_name, args_len_str, args ) ) + self.launch_args = args + # Run process - self.process = subprocess.Popen(args, **self.kwargs) + self.process = self._run_process() # Process post launch hooks for postlaunch_hook in self.postlaunch_hooks: diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index ad77b2f899..f97617d906 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -1,7 +1,6 @@ import os -import shlex import subprocess -import platform +import distutils.spawn from .log import PypeLogger as Logger @@ -175,3 +174,46 @@ def get_pype_execute_args(*args): pype_args.extend(args) return pype_args + + +def get_linux_launcher_args(*args): + """Path to application mid process executable. + + This function should be able as arguments are different when used + from code and build. + + It is possible that this function is used in OpenPype build which does + not have yet the new executable. In that case 'None' is returned. + + Args: + args (iterable): List of additional arguments added after executable + argument. + + Returns: + list: Executables with possible positional argument to script when + called from code. + """ + filename = "app_launcher" + openpype_executable = os.environ["OPENPYPE_EXECUTABLE"] + + executable_filename = os.path.basename(openpype_executable) + if "python" in executable_filename.lower(): + script_path = os.path.join( + os.environ["OPENPYPE_ROOT"], + "{}.py".format(filename) + ) + launch_args = [openpype_executable, script_path] + else: + new_executable = os.path.join( + os.path.dirname(openpype_executable), + filename + ) + executable_path = distutils.spawn.find_executable(new_executable) + if executable_path is None: + return None + launch_args = [executable_path] + + if args: + launch_args.extend(args) + + return launch_args diff --git a/setup.py b/setup.py index a21645e66a..41ed066693 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,7 @@ import os import sys import re +import platform from pathlib import Path from cx_Freeze import setup, Executable @@ -18,8 +19,13 @@ with open(openpype_root / "openpype" / "version.py") as fp: version_match = re.search(r"(\d+\.\d+.\d+).*", version["__version__"]) __version__ = version_match.group(1) +low_platform_name = platform.system().lower() +IS_WINDOWS = low_platform_name == "windows" +IS_LINUX = low_platform_name == "linux" +IS_MACOS = low_platform_name == "darwin" + base = None -if sys.platform == "win32": +if IS_WINDOWS: base = "Win32GUI" # ----------------------------------------------------------------------- @@ -72,7 +78,7 @@ include_files = [ "README.md" ] -if sys.platform == "win32": +if IS_WINDOWS: install_requires.extend([ # `pywin32` packages "win32ctypes", @@ -104,6 +110,15 @@ executables = [ Executable("start.py", base=None, target_name="openpype_console", icon=icon_path.as_posix()) ] +if IS_LINUX: + executables.append( + Executable( + "app_launcher.py", + base=None, + target_name="app_launcher", + icon=icon_path.as_posix() + ) + ) setup( name="OpenPype",