Applications: Use prelaunch hooks to extract environments (#5387)

* ApplicationManager can have more granular way how applications are launched

* executable is optional to be able create ApplicationLaunchContext

* launch context can run prelaunch hooks without launching application

* 'get_app_environments_for_context' is using launch context to prepare environments

* added 'launch_type' as one of filtering options for LaunchHook

* added 'local' launch type filter to existing launch hooks

* define 'automated' launch type in remote publish function

* modified publish and extract environments cli commands

* launch types are only for local by default

* fix import

* fix launch types of global host data

* change order or kwargs

* change unreal filter attribute
This commit is contained in:
Jakub Trllo 2023-08-03 10:04:15 +02:00 committed by GitHub
parent 55aead8470
commit 7e9f42b447
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 266 additions and 152 deletions

View file

@ -12,10 +12,6 @@ from abc import ABCMeta, abstractmethod
import six
from openpype import AYON_SERVER_ENABLED, PACKAGE_DIR
from openpype.client import (
get_project,
get_asset_by_name,
)
from openpype.settings import (
get_system_settings,
get_project_settings,
@ -47,6 +43,25 @@ CUSTOM_LAUNCH_APP_GROUPS = {
}
class LaunchTypes:
"""Launch types are filters for pre/post-launch hooks.
Please use these variables in case they'll change values.
"""
# Local launch - application is launched on local machine
local = "local"
# Farm render job - application is on farm
farm_render = "farm-render"
# Farm publish job - integration post-render job
farm_publish = "farm-publish"
# Remote launch - application is launched on remote machine from which
# can be started publishing
remote = "remote"
# Automated launch - application is launched with automated publishing
automated = "automated"
def parse_environments(env_data, env_group=None, platform_name=None):
"""Parse environment values from settings byt group and platform.
@ -483,6 +498,42 @@ class ApplicationManager:
break
return output
def create_launch_context(self, app_name, **data):
"""Prepare launch context for application.
Args:
app_name (str): Name of application that should be launched.
**data (Any): Any additional data. Data may be used during
Returns:
ApplicationLaunchContext: Launch context for application.
Raises:
ApplicationNotFound: Application was not found by entered name.
"""
app = self.applications.get(app_name)
if not app:
raise ApplicationNotFound(app_name)
executable = app.find_executable()
return ApplicationLaunchContext(
app, executable, **data
)
def launch_with_context(self, launch_context):
"""Launch application using existing launch context.
Args:
launch_context (ApplicationLaunchContext): Prepared launch
context.
"""
if not launch_context.executable:
raise ApplictionExecutableNotFound(launch_context.application)
return launch_context.launch()
def launch(self, app_name, **data):
"""Launch procedure.
@ -503,18 +554,10 @@ class ApplicationManager:
failed. Exception should contain explanation message,
traceback should not be needed.
"""
app = self.applications.get(app_name)
if not app:
raise ApplicationNotFound(app_name)
executable = app.find_executable()
if not executable:
raise ApplictionExecutableNotFound(app)
context = self.create_launch_context(app_name, **data)
return self.launch_with_context(context)
context = ApplicationLaunchContext(
app, executable, **data
)
return context.launch()
class EnvironmentToolGroup:
@ -736,13 +779,17 @@ class LaunchHook:
# Order of prelaunch hook, will be executed as last if set to None.
order = None
# List of host implementations, skipped if empty.
hosts = []
# List of application groups
app_groups = []
# List of specific application names
app_names = []
# List of platform availability, skipped if empty.
platforms = []
hosts = set()
# Set of application groups
app_groups = set()
# Set of specific application names
app_names = set()
# Set of platform availability
platforms = set()
# Set of launch types for which is available
# - if empty then is available for all launch types
# - by default has 'local' which is most common reason for launc hooks
launch_types = {LaunchTypes.local}
def __init__(self, launch_context):
"""Constructor of launch hook.
@ -790,6 +837,10 @@ class LaunchHook:
if launch_context.app_name not in cls.app_names:
return False
if cls.launch_types:
if launch_context.launch_type not in cls.launch_types:
return False
return True
@property
@ -859,9 +910,9 @@ class PostLaunchHook(LaunchHook):
class ApplicationLaunchContext:
"""Context of launching application.
Main purpose of context is to prepare launch arguments and keyword arguments
for new process. Most important part of keyword arguments preparations
are environment variables.
Main purpose of context is to prepare launch arguments and keyword
arguments for new process. Most important part of keyword arguments
preparations are environment variables.
During the whole process is possible to use `data` attribute to store
object usable in multiple places.
@ -874,14 +925,30 @@ class ApplicationLaunchContext:
insert argument between `nuke.exe` and `--NukeX`. To keep them together
it is better to wrap them in another list: `[["nuke.exe", "--NukeX"]]`.
Notes:
It is possible to use launch context only to prepare environment
variables. In that case `executable` may be None and can be used
'run_prelaunch_hooks' method to run prelaunch hooks which prepare
them.
Args:
application (Application): Application definition.
executable (ApplicationExecutable): Object with path to executable.
env_group (Optional[str]): Environment variable group. If not set
'DEFAULT_ENV_SUBGROUP' is used.
launch_type (Optional[str]): Launch type. If not set 'local' is used.
**data (dict): Any additional data. Data may be used during
preparation to store objects usable in multiple places.
"""
def __init__(self, application, executable, env_group=None, **data):
def __init__(
self,
application,
executable,
env_group=None,
launch_type=None,
**data
):
from openpype.modules import ModulesManager
# Application object
@ -896,6 +963,10 @@ class ApplicationLaunchContext:
self.executable = executable
if launch_type is None:
launch_type = LaunchTypes.local
self.launch_type = launch_type
if env_group is None:
env_group = DEFAULT_ENV_SUBGROUP
@ -903,8 +974,11 @@ class ApplicationLaunchContext:
self.data = dict(data)
launch_args = []
if executable is not None:
launch_args = executable.as_args()
# subprocess.Popen launch arguments (first argument in constructor)
self.launch_args = executable.as_args()
self.launch_args = launch_args
self.launch_args.extend(application.arguments)
if self.data.get("app_args"):
self.launch_args.extend(self.data.pop("app_args"))
@ -946,6 +1020,7 @@ class ApplicationLaunchContext:
self.postlaunch_hooks = None
self.process = None
self._prelaunch_hooks_executed = False
@property
def env(self):
@ -1215,6 +1290,27 @@ class ApplicationLaunchContext:
# Return process which is already terminated
return process
def run_prelaunch_hooks(self):
"""Run prelaunch hooks.
This method will be executed only once, any future calls will skip
the processing.
"""
if self._prelaunch_hooks_executed:
self.log.warning("Prelaunch hooks were already executed.")
return
# Discover launch hooks
self.discover_launch_hooks()
# Execute prelaunch hooks
for prelaunch_hook in self.prelaunch_hooks:
self.log.debug("Executing prelaunch hook: {}".format(
str(prelaunch_hook.__class__.__name__)
))
prelaunch_hook.execute()
self._prelaunch_hooks_executed = True
def launch(self):
"""Collect data for new process and then create it.
@ -1227,15 +1323,8 @@ class ApplicationLaunchContext:
self.log.warning("Application was already launched.")
return
# Discover launch hooks
self.discover_launch_hooks()
# Execute prelaunch hooks
for prelaunch_hook in self.prelaunch_hooks:
self.log.debug("Executing prelaunch hook: {}".format(
str(prelaunch_hook.__class__.__name__)
))
prelaunch_hook.execute()
if not self._prelaunch_hooks_executed:
self.run_prelaunch_hooks()
self.log.debug("All prelaunch hook executed. Starting new process.")
@ -1353,6 +1442,7 @@ def get_app_environments_for_context(
task_name,
app_name,
env_group=None,
launch_type=None,
env=None,
modules_manager=None
):
@ -1363,54 +1453,33 @@ def get_app_environments_for_context(
task_name (str): Name of task.
app_name (str): Name of application that is launched and can be found
by ApplicationManager.
env (dict): Initial environment variables. `os.environ` is used when
not passed.
modules_manager (ModulesManager): Initialized modules manager.
env_group (Optional[str]): Name of environment group. If not passed
default group is used.
launch_type (Optional[str]): Type for which prelaunch hooks are
executed.
env (Optional[dict[str, str]]): Initial environment variables.
`os.environ` is used when not passed.
modules_manager (Optional[ModulesManager]): Initialized modules
manager.
Returns:
dict: Environments for passed context and application.
"""
from openpype.modules import ModulesManager
from openpype.pipeline import Anatomy
from openpype.lib.openpype_version import is_running_staging
# Project document
project_doc = get_project(project_name)
asset_doc = get_asset_by_name(project_name, asset_name)
if modules_manager is None:
modules_manager = ModulesManager()
# Prepare app object which can be obtained only from ApplciationManager
# Prepare app object which can be obtained only from ApplicationManager
app_manager = ApplicationManager()
app = app_manager.applications[app_name]
# Project's anatomy
anatomy = Anatomy(project_name)
data = EnvironmentPrepData({
"project_name": project_name,
"asset_name": asset_name,
"task_name": task_name,
"app": app,
"project_doc": project_doc,
"asset_doc": asset_doc,
"anatomy": anatomy,
"env": env
})
data["env"].update(anatomy.root_environments())
if is_running_staging():
data["env"]["OPENPYPE_IS_STAGING"] = "1"
prepare_app_environments(data, env_group, modules_manager)
prepare_context_environments(data, env_group, modules_manager)
return data["env"]
context = app_manager.create_launch_context(
app_name,
project_name=project_name,
asset_name=asset_name,
task_name=task_name,
env_group=env_group,
launch_type=launch_type,
env=env,
modules_manager=modules_manager,
)
context.run_prelaunch_hooks()
return context.env
def _merge_env(env, current_env):