From f1b158e7e55ee9e452c99c16a7e7e800ab80c2ec Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 17:54:41 +0100 Subject: [PATCH 1/6] added new script and executable to be used as mid process for starting applications on linux --- linux_app_launcher.py | 43 +++++++++++++++++++++++++++++++++++++++++++ setup.py | 19 +++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 linux_app_launcher.py diff --git a/linux_app_launcher.py b/linux_app_launcher.py new file mode 100644 index 0000000000..274ef8b82b --- /dev/null +++ b/linux_app_launcher.py @@ -0,0 +1,43 @@ +"""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) + + 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/setup.py b/setup.py index cd3ed4f82c..de4887ae3c 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" # ----------------------------------------------------------------------- @@ -71,7 +77,7 @@ include_files = [ "README.md" ] -if sys.platform == "win32": +if IS_WINDOWS: install_requires.extend([ # `pywin32` packages "win32ctypes", @@ -103,6 +109,15 @@ executables = [ Executable("start.py", base=None, target_name="openpype_console", icon=icon_path.as_posix()) ] +if IS_LINUX: + executables.append( + Executable( + "linux_app_launcher.py", + base=None, + target_name="linux_app_launcher", + icon=icon_path.as_posix() + ) + ) setup( name="OpenPype", From 3e096f41bedfff6e8cd39c482ce64aac55da1766 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 18:00:07 +0100 Subject: [PATCH 2/6] added helper function to return linux app launcher arguments --- openpype/lib/__init__.py | 2 ++ openpype/lib/execute.py | 46 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index efd2cddf7e..936ee9ea8d 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, @@ -174,6 +175,7 @@ terminal = Terminal __all__ = [ "get_pype_execute_args", + "get_linux_launcher_args", "execute", "run_subprocess", "path_to_subprocess_arg", diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index ad77b2f899..a36a1f0bf4 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 = "linux_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 From 0b9f9450f6e4a9a63543c491e4a18443a4aebdc2 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Tue, 14 Dec 2021 18:02:29 +0100 Subject: [PATCH 3/6] use new function on application launch --- openpype/lib/applications.py | 47 ++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 6eb44a9694..97cc213646 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 @@ -921,6 +922,46 @@ 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 + json_data = { + "args": self.launch_args + } + # 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. @@ -957,8 +998,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: From 5fc7307be6e74df213fad40941cd86e22f1ccec6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 14:56:00 +0100 Subject: [PATCH 4/6] added ability to pass environment variables --- linux_app_launcher.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/linux_app_launcher.py b/linux_app_launcher.py index 274ef8b82b..6dc1518370 100644 --- a/linux_app_launcher.py +++ b/linux_app_launcher.py @@ -28,6 +28,12 @@ def main(input_json_path): 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) From dd99682df3284e4a5e2ada02adc4da9414603cb5 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Thu, 16 Dec 2021 14:57:02 +0100 Subject: [PATCH 5/6] pass environments to set into the json instead of changing them for the subprocess --- openpype/lib/applications.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 97cc213646..2101303771 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -938,8 +938,10 @@ class ApplicationLaunchContext: # 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 + "args": self.launch_args, + "env": self.kwargs.pop("env", {}) } # Create temp file json_temp = tempfile.NamedTemporaryFile( From 86bdddf343513fb34aefcceaad2d1a50b0643e4c Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Wed, 22 Dec 2021 18:30:40 +0100 Subject: [PATCH 6/6] renamed linux_app_launcher to app_launcher --- linux_app_launcher.py => app_launcher.py | 0 openpype/lib/execute.py | 2 +- setup.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) rename linux_app_launcher.py => app_launcher.py (100%) diff --git a/linux_app_launcher.py b/app_launcher.py similarity index 100% rename from linux_app_launcher.py rename to app_launcher.py diff --git a/openpype/lib/execute.py b/openpype/lib/execute.py index a36a1f0bf4..f97617d906 100644 --- a/openpype/lib/execute.py +++ b/openpype/lib/execute.py @@ -193,7 +193,7 @@ def get_linux_launcher_args(*args): list: Executables with possible positional argument to script when called from code. """ - filename = "linux_app_launcher" + filename = "app_launcher" openpype_executable = os.environ["OPENPYPE_EXECUTABLE"] executable_filename = os.path.basename(openpype_executable) diff --git a/setup.py b/setup.py index de4887ae3c..a16984ca8e 100644 --- a/setup.py +++ b/setup.py @@ -112,9 +112,9 @@ executables = [ if IS_LINUX: executables.append( Executable( - "linux_app_launcher.py", + "app_launcher.py", base=None, - target_name="linux_app_launcher", + target_name="app_launcher", icon=icon_path.as_posix() ) )