|
27 | 27 | ) |
28 | 28 | from openjd.model import version as model_version |
29 | 29 | 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, |
30 | 37 | ValueReferenceConstants as ValueReferenceConstants_2023_09, |
31 | 38 | ) |
32 | 39 | from ._action_filter import ActionMessageKind, ActionMonitoringFilter |
@@ -900,6 +907,122 @@ def _run_task_without_session_env( |
900 | 907 | # than after -- run() itself may end up setting the action state to FAILED. |
901 | 908 | self._runner.run() |
902 | 909 |
|
| 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 | + |
903 | 1026 | # ========================= |
904 | 1027 | # Helpers |
905 | 1028 |
|
|
0 commit comments