Skip to content

Commit 0c1e13c

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 <399551+mwiebe@users.noreply.github.com>
1 parent 9695b97 commit 0c1e13c

File tree

2 files changed

+1238
-0
lines changed

2 files changed

+1238
-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
@@ -843,6 +850,122 @@ def run_task(
843850
# than after -- run() itself may end up setting the action state to FAILED.
844851
self._runner.run()
845852

853+
def run_subprocess(
854+
self,
855+
*,
856+
command: str,
857+
args: Optional[list[str]] = None,
858+
timeout: Optional[int] = None,
859+
os_env_vars: Optional[dict[str, str]] = None,
860+
use_session_env_vars: bool = True,
861+
log_banner_message: Optional[str] = None,
862+
) -> None:
863+
"""Run an ad-hoc subprocess within the Session.
864+
865+
This method is non-blocking; it will exit when the subprocess is either
866+
confirmed to have started running, or has failed to be started.
867+
868+
Arguments:
869+
command (str): The command/executable to run. Used exactly as provided
870+
without format string substitution.
871+
args (Optional[list[str]]): Arguments to pass to the command. Used exactly
872+
as provided without format string substitution. Defaults to None.
873+
timeout (Optional[int]): Maximum allowed runtime of the subprocess in seconds.
874+
Must be a positive integer if provided. If None, the subprocess can run
875+
indefinitely. Defaults to None.
876+
os_env_vars (Optional[dict[str, str]]): Additional OS environment variables
877+
to inject into the subprocess. Values provided override original process
878+
environment variables and are overridden by environment-defined variables.
879+
use_session_env_vars (bool): If True, includes environment variables from
880+
the session and entered environments. If False, only uses os_env_vars
881+
and original process environment variables. Defaults to True.
882+
log_banner_message (Optional[str]): Custom message to display in a banner
883+
before running the subprocess. If provided, logs a banner with this message.
884+
If None, no banner is logged. Defaults to None.
885+
886+
Raises:
887+
RuntimeError: If the Session is not in the READY state.
888+
ValueError: If timeout is provided and is not a positive integer, or if command is empty.
889+
"""
890+
# State validation
891+
if self.state != SessionState.READY:
892+
raise RuntimeError(
893+
f"Session must be in the READY state to run a subprocess. "
894+
f"Current state: {self.state.value}"
895+
)
896+
897+
# Parameter validation
898+
if timeout is not None and timeout <= 0:
899+
raise ValueError("timeout must be a positive integer")
900+
901+
if not command or not command.strip():
902+
raise ValueError("command must be a non-empty string")
903+
904+
# Log banner if requested
905+
if log_banner_message:
906+
log_section_banner(self._logger, log_banner_message)
907+
908+
# Reset action state
909+
self._reset_action_state()
910+
911+
# Construct Action model
912+
cancelation = CancelationMethodTerminate_2023_09(mode=CancelationMode_2023_09.TERMINATE)
913+
914+
action_command = CommandString_2023_09(command)
915+
action_args = [ArgString_2023_09(arg) for arg in args] if args else None
916+
917+
action = Action_2023_09(
918+
command=action_command,
919+
args=action_args,
920+
timeout=timeout,
921+
cancelation=cancelation,
922+
)
923+
924+
# Construct StepScript model
925+
step_actions = StepActions_2023_09(onRun=action)
926+
927+
step_script = StepScript_2023_09(
928+
actions=step_actions,
929+
embeddedFiles=None,
930+
)
931+
932+
# Create empty symbol table (no format string substitution for ad-hoc subprocesses)
933+
symtab = SymbolTable()
934+
935+
# Evaluate environment variables
936+
if use_session_env_vars:
937+
action_env_vars = self._evaluate_current_session_env_vars(os_env_vars)
938+
else:
939+
action_env_vars = dict[str, Optional[str]](self._process_env) # Make a copy
940+
if os_env_vars:
941+
action_env_vars.update(**os_env_vars)
942+
943+
# Note: Path mapping is not materialized for ad-hoc subprocesses since it's only
944+
# accessible via template variable substitution (e.g., {{Session.PathMappingRulesFile}}),
945+
# which is explicitly disabled for run_subprocess to ensure predictable behavior.
946+
947+
# Create and start StepScriptRunner
948+
self._runner = StepScriptRunner(
949+
logger=self._logger,
950+
user=self._user,
951+
os_env_vars=action_env_vars,
952+
session_working_directory=self.working_directory,
953+
startup_directory=self.working_directory,
954+
callback=self._action_callback,
955+
script=step_script,
956+
symtab=symtab,
957+
session_files_directory=self.files_directory,
958+
)
959+
960+
# Sets the subprocess running.
961+
# Returns immediately after it has started, or is running
962+
self._action_state = ActionState.RUNNING
963+
self._state = SessionState.RUNNING
964+
# Note: This may fail immediately (e.g. if we cannot write embedded files to disk),
965+
# so it's important to set the action_state to RUNNING before calling run(), rather
966+
# than after -- run() itself may end up setting the action state to FAILED.
967+
self._runner.run()
968+
846969
# =========================
847970
# Helpers
848971

0 commit comments

Comments
 (0)