DUNE-DAQ
DUNE Trigger and Data Acquisition software
Loading...
Searching...
No Matches
multiprocess_runcontrol_driver.py
Go to the documentation of this file.
1#!/bin/env python3
2
3# August 2026, KAB: As part of developing functionality to support multiple user-specified
4# applications running in our integration tests, I ran some web searches to learn more about
5# how to start and receive output from multiple processes in Python. The Python code that
6# was suggested in the response was very helpful.
7# I modified that sample code to start drunc-unified-shell, drunc-process-manager, and
8# drunc-process-manager-shell processes. The modified script was so helpful that I wanted
9# to capture it for later use, and this is that script.
10# I can imagine this script being used by non-run-control experts to learn about how the
11# different RC apps interact, and maybe the script could be used to provide an easy way
12# to start up a different set of applications.
13# This code is far from production-ready. So, if we ever decide to make it a general-purpose
14# tool, we should improve various aspects of it. The integrationtest/async_proc_mgmt
15# code (which used this script as a starting point) might be useful when thinking about any
16# possible improvements.
17#
18# The script can by run by typing 'multiprocess_runcontrol_driver.py' with no arguments.
19# Once the script has been started, commands can be sent to one of the three processes by
20# pre-pending the process nickname to the command (with a colon separator). For example, 'drunc:ps'.
21# Typing 'exit' (with no process prefix) will exit the script.
22
23import asyncio
24import sys
25import time
26from daqconf.utils import find_free_port
27
28last_msg_time = 0
29
30async def read_stream(stream, process_name, completion_event):
31 """Asynchronously reads lines from a stream and prints them immediately."""
32 global last_msg_time
33 while True:
34 line = await stream.readline()
35 if not line:
36 break
37 decoded_line = line.decode().rstrip()
38
39 if "*** COMMAND HAS COMPLETED ***" in decoded_line:
40 completion_event.set()
41 continue
42
43 # Decode and strip line endings
44 print(f"[{process_name}] {line.decode().rstrip()}", flush=True)
45 last_msg_time = time.time()
46
47async def interactive_manager(commands):
48 processes = {}
49 tasks = []
50 command_completion_event = asyncio.Event()
51 global last_msg_time
52
53 # 1. Start all interactive subprocesses
54 for cmd in commands:
55 name = cmd.pop(0)
56 print()
57 print(f"*** Starting \"{cmd[0]}\" with local process name \"{name}\"...")
58 print()
59 proc = await asyncio.create_subprocess_exec(
60 *cmd,
61 stdin=asyncio.subprocess.PIPE,
62 stdout=asyncio.subprocess.PIPE,
63 stderr=asyncio.subprocess.STDOUT
64 )
65 processes[name] = proc
66
67 # 2. Schedule output reading tasks concurrently
68 tasks.append(asyncio.create_task(read_stream(proc.stdout, name, command_completion_event)))
69
70 time.sleep(2)
71
72 print()
73 print(f"*** Started {len(processes)} processes. Type: '<process_name>:<input>' (e.g., shell:help)")
74 print("*** Type 'exit' to quit everything.")
75 print()
76
77 # 3. Handle interactive user input from the main terminal
78 loop = asyncio.get_running_loop()
79 reader = asyncio.StreamReader()
80 protocol = asyncio.StreamReaderProtocol(reader)
81 await loop.connect_read_pipe(lambda: protocol, sys.stdin)
82
83 try:
84 while True:
85 print("\nmprc_drvr> ", end="", flush=True)
86 user_line = await reader.readline()
87 if not user_line:
88 break
89
90 command_text = user_line.decode().strip()
91 if command_text.lower() == 'exit':
92 break
93
94 # Parse target process and message (Format: Proc-1:your_command)
95 if ":" in command_text:
96 target, msg = command_text.split(":", 1)
97 target = target.strip()
98
99 if target in processes:
100 proc = processes[target]
101 if proc.returncode is None: # Check if still running
102 cmd_start_time = time.time()
103 proc.stdin.write((msg + "\n").encode())
104 await proc.stdin.drain()
105 print(f"[System] Sent to {target}: {msg}")
106 if "drunc" in target:
107 proc.stdin.write(("echo '*** COMMAND HAS COMPLETED ***'\n").encode())
108 await proc.stdin.drain()
109 print(f"[System] Sent to {target}: echo '*** COMMAND HAS COMPLETED ***'")
110 await command_completion_event.wait()
111 command_completion_event.clear()
112 else:
113 now = time.time()
114 while True:
115 if last_msg_time <= cmd_start_time:
116 if now - cmd_start_time > 5:
117 break
118 else:
119 if now - last_msg_time >= 5:
120 break
121 await asyncio.sleep(0.25)
122 now = time.time()
123 else:
124 print(f"[System] Error: {target} has already exited.")
125 else:
126 print(f"[System] Error: Process '{target}' not found.")
127 else:
128 print("[System] Invalid format. Use: <process_name>:<command>")
129
130 except asyncio.CancelledError:
131 pass
132 finally:
133 # 4. Cleanup and terminate remaining processes
134 print("\n[System] Shutting down processes...")
135 for name, proc in reversed(processes.items()):
136 if proc.returncode is None:
137 proc.terminate()
138 await proc.wait()
139
140 # Cancel background reading tasks
141 for task in tasks:
142 task.cancel()
143
144if __name__ == "__main__":
145 pm_port = find_free_port(50001, 52000)
146
147 # This set of DUNE-DAQ control applications is the first useful one that came to mind.
148 # It allows testing of process-manager-as-a-service and gives us a way to see how these
149 # three run control programs interact. Of course, there may be different sets of apps
150 # that will be useful in the future. At that time, we may want to simply edit the following
151 # list to have different apps, or we may consider something more dynamic - to be decided.
152 interactive_cmds = [
153 ["pm", "drunc-process-manager", "ssh-standalone", str(pm_port)],
154 ["pmshell", "drunc-process-manager-shell", f"grpc://localhost:{pm_port}"],
155 ["drunc", "drunc-unified-shell", f"grpc://localhost:{pm_port}", "config/daqsystemtest/example-configs.data.xml", "local-1x1-config", "biery-local-test"]
156 ]
157
158 try:
159 asyncio.run(interactive_manager(interactive_cmds))
160 except KeyboardInterrupt:
161 print("\n[System] Exited via Ctrl+C.")
read_stream(stream, process_name, completion_event)