Merge pull request #2408 from pypeclub/feature/OP-2088_Run-applications-as-separate-processes-under-Linux

General: Run applications as separate processes under linux
This commit is contained in:
Jakub Trllo 2022-01-03 15:33:22 +01:00 committed by GitHub
commit 1e7378c80f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 159 additions and 6 deletions

49
app_launcher.py Normal file
View file

@ -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])

View file

@ -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",

View file

@ -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:

View file

@ -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

View file

@ -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",