Skip to content

Commit 80026e9

Browse files
committed
feat: Add run_subprocess function to the Session class
This new function provides the ability to run an ad hoc command within the session environment, choosing whether to use the environment variables from the session enter actions or to ignore them. The parameters for the command are direct, so you don't have to construct a temporary Action or StepAction object to run a simple command. Signed-off-by: Mark <[email protected]>
1 parent 3768bca commit 80026e9

File tree

2 files changed

+1236
-0
lines changed

2 files changed

+1236
-0
lines changed

src/openjd/sessions/_session.py

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
)
2828
from openjd.model import version as model_version
2929
from openjd.model.v2023_09 import (
30+
Action as Action_2023_09,
31+
ArgString as ArgString_2023_09,
32+
CancelationMethodTerminate as CancelationMethodTerminate_2023_09,
33+
CancelationMode as CancelationMode_2023_09,
34+
CommandString as CommandString_2023_09,
35+
StepActions as StepActions_2023_09,
36+
StepScript as StepScript_2023_09,
3037
ValueReferenceConstants as ValueReferenceConstants_2023_09,
3138
)
3239
from ._action_filter import ActionMessageKind, ActionMonitoringFilter
@@ -900,6 +907,122 @@ def _run_task_without_session_env(
900907
# than after -- run() itself may end up setting the action state to FAILED.
901908
self._runner.run()
902909

910+
def run_subprocess(
911+
self,
912+
*,
913+
command: str,
914+
args: Optional[list[str]] = None,
915+
timeout: Optional[int] = None,
916+
os_env_vars: Optional[dict[str, str]] = None,
917+
use_session_env_vars: bool = True,
918+
log_banner_message: Optional[str] = None,
919+
) -> None:
920+
"""Run an ad-hoc subprocess within the Session.
921+
922+
This method is non-blocking; it will exit when the subprocess is either
923+
confirmed to have started running, or has failed to be started.
924+
925+
Arguments:
926+
command (str): The command/executable to run. Used exactly as provided
927+
without format string substitution.
928+
args (Optional[list[str]]): Arguments to pass to the command. Used exactly
929+
as provided without format string substitution. Defaults to None.
930+
timeout (Optional[int]): Maximum allowed runtime of the subprocess in seconds.
931+
Must be a positive integer if provided. If None, the subprocess can run
932+
indefinitely. Defaults to None.
933+
os_env_vars (Optional[dict[str, str]]): Additional OS environment variables
934+
to inject into the subprocess. Values provided override original process
935+
environment variables and are overridden by environment-defined variables.
936+
use_session_env_vars (bool): If True, includes environment variables from
937+
the session and entered environments. If False, only uses os_env_vars
938+
and original process environment variables. Defaults to True.
939+
log_banner_message (Optional[str]): Custom message to display in a banner
940+
before running the subprocess. If provided, logs a banner with this message.
941+
If None, no banner is logged. Defaults to None.
942+
943+
Raises:
944+
RuntimeError: If the Session is not in the READY state.
945+
ValueError: If timeout is provided and is not a positive integer, or if command is empty.
946+
"""
947+
# State validation
948+
if self.state != SessionState.READY:
949+
raise RuntimeError(
950+
f"Session must be in the READY state to run a subprocess. "
951+
f"Current state: {self.state.value}"
952+
)
953+
954+
# Parameter validation
955+
if timeout is not None and timeout <= 0:
956+
raise ValueError("timeout must be a positive integer")
957+
958+
if not command or not command.strip():
959+
raise ValueError("command must be a non-empty string")
960+
961+
# Log banner if requested
962+
if log_banner_message:
963+
log_section_banner(self._logger, log_banner_message)
964+
965+
# Reset action state
966+
self._reset_action_state()
967+
968+
# Construct Action model
969+
cancelation = CancelationMethodTerminate_2023_09(mode=CancelationMode_2023_09.TERMINATE)
970+
971+
action_command = CommandString_2023_09(command)
972+
action_args = [ArgString_2023_09(arg) for arg in args] if args else None
973+
974+
action = Action_2023_09(
975+
command=action_command,
976+
args=action_args,
977+
timeout=timeout,
978+
cancelation=cancelation,
979+
)
980+
981+
# Construct StepScript model
982+
step_actions = StepActions_2023_09(onRun=action)
983+
984+
step_script = StepScript_2023_09(
985+
actions=step_actions,
986+
embeddedFiles=None,
987+
)
988+
989+
# Create empty symbol table (no format string substitution for ad-hoc subprocesses)
990+
symtab = SymbolTable()
991+
992+
# Evaluate environment variables
993+
if use_session_env_vars:
994+
action_env_vars = self._evaluate_current_session_env_vars(os_env_vars)
995+
else:
996+
action_env_vars = dict[str, Optional[str]](self._process_env) # Make a copy
997+
if os_env_vars:
998+
action_env_vars.update(**os_env_vars)
999+
1000+
# Note: Path mapping is not materialized for ad-hoc subprocesses since it's only
1001+
# accessible via template variable substitution (e.g., {{Session.PathMappingRulesFile}}),
1002+
# which is explicitly disabled for run_subprocess to ensure predictable behavior.
1003+
1004+
# Create and start StepScriptRunner
1005+
self._runner = StepScriptRunner(
1006+
logger=self._logger,
1007+
user=self._user,
1008+
os_env_vars=action_env_vars,
1009+
session_working_directory=self.working_directory,
1010+
startup_directory=self.working_directory,
1011+
callback=self._action_callback,
1012+
script=step_script,
1013+
symtab=symtab,
1014+
session_files_directory=self.files_directory,
1015+
)
1016+
1017+
# Sets the subprocess running.
1018+
# Returns immediately after it has started, or is running
1019+
self._action_state = ActionState.RUNNING
1020+
self._state = SessionState.RUNNING
1021+
# Note: This may fail immediately (e.g. if we cannot write embedded files to disk),
1022+
# so it's important to set the action_state to RUNNING before calling run(), rather
1023+
# than after -- run() itself may end up setting the action state to FAILED.
1024+
self._runner.run()
1025+
9031026
# =========================
9041027
# Helpers
9051028

0 commit comments

Comments
 (0)