parallax_runtime/
worker.rs1use parallax_core::{ErrorCode, ParallaxError, Remediation, RuntimeKind};
4use parallax_protocol::Envelope;
5use std::path::PathBuf;
6use std::process::Stdio;
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
10use tokio::process::{Child, ChildStdin, ChildStdout, Command};
11use tokio::sync::Mutex;
12use tokio::time::timeout;
13
14const DEFAULT_MAX_MESSAGE_BYTES: u64 = 16_777_216;
16
17pub struct WorkerProcess {
19 child: Child,
20 stdin: ChildStdin,
21 stdout: BufReader<ChildStdout>,
22 runtime: RuntimeKind,
23 stderr_buf: Arc<Mutex<String>>,
25 stderr_task: Option<tokio::task::JoinHandle<()>>,
26}
27
28impl WorkerProcess {
29 pub async fn spawn(
31 program: PathBuf,
32 args: &[String],
33 runtime: RuntimeKind,
34 ) -> parallax_core::Result<Self> {
35 let mut cmd = Command::new(&program);
36 cmd.args(args)
37 .stdin(Stdio::piped())
38 .stdout(Stdio::piped())
39 .stderr(Stdio::piped())
40 .kill_on_drop(true);
41 #[cfg(windows)]
42 {
43 const CREATE_NO_WINDOW: u32 = 0x08000000;
45 cmd.creation_flags(CREATE_NO_WINDOW);
46 }
47 let mut child = cmd.spawn().map_err(|e| {
48 ParallaxError::new(
49 ErrorCode::RuntimeUnavailable,
50 format!("failed to spawn {:?} worker: {e}", runtime),
51 )
52 .with_runtime(runtime.clone())
53 .with_source("parallax-runtime")
54 .with_operation("WorkerProcess::spawn")
55 .remediate(Remediation::new(
56 "Install the runtime or ensure it is on PATH",
57 ))
58 })?;
59 let stdin = child.stdin.take().ok_or_else(|| {
60 ParallaxError::new(ErrorCode::AdapterCrashed, "worker stdin missing")
61 .with_runtime(runtime.clone())
62 })?;
63 let stdout = child.stdout.take().ok_or_else(|| {
64 ParallaxError::new(ErrorCode::AdapterCrashed, "worker stdout missing")
65 .with_runtime(runtime.clone())
66 })?;
67 let stderr = child.stderr.take().ok_or_else(|| {
68 ParallaxError::new(ErrorCode::AdapterCrashed, "worker stderr missing")
69 .with_runtime(runtime.clone())
70 })?;
71
72 let stderr_buf = Arc::new(Mutex::new(String::new()));
74 let stderr_buf_task = Arc::clone(&stderr_buf);
75 let stderr_task = tokio::spawn(async move {
76 let mut reader = BufReader::new(stderr);
77 let mut chunk = [0u8; 4096];
78 const MAX_STDERR: usize = 64 * 1024;
79 loop {
80 match reader.read(&mut chunk).await {
81 Ok(0) => break,
82 Ok(n) => {
83 let mut guard = stderr_buf_task.lock().await;
84 if guard.len() < MAX_STDERR {
85 let take = (MAX_STDERR - guard.len()).min(n);
86 guard.push_str(&String::from_utf8_lossy(&chunk[..take]));
87 }
88 }
89 Err(_) => break,
90 }
91 }
92 });
93
94 Ok(Self {
95 child,
96 stdin,
97 stdout: BufReader::new(stdout),
98 runtime,
99 stderr_buf,
100 stderr_task: Some(stderr_task),
101 })
102 }
103
104 pub async fn request(
106 &mut self,
107 env: Envelope,
108 wait: Duration,
109 ) -> parallax_core::Result<Envelope> {
110 self.request_bounded(env, wait, DEFAULT_MAX_MESSAGE_BYTES)
111 .await
112 }
113
114 pub async fn request_bounded(
116 &mut self,
117 env: Envelope,
118 wait: Duration,
119 max_message_bytes: u64,
120 ) -> parallax_core::Result<Envelope> {
121 let id = env.id;
122 let line = env.to_ndjson_line()?;
123 if line.len() as u64 > max_message_bytes {
124 return Err(ParallaxError::new(
125 ErrorCode::ResourceLimitExceeded,
126 format!(
127 "outbound protocol message exceeds max_message_bytes ({})",
128 max_message_bytes
129 ),
130 )
131 .with_runtime(self.runtime.clone())
132 .with_operation("WorkerProcess::request"));
133 }
134 self.stdin.write_all(line.as_bytes()).await.map_err(|e| {
135 ParallaxError::new(ErrorCode::AdapterCrashed, format!("write failed: {e}"))
136 .with_runtime(self.runtime.clone())
137 .with_source("parallax-runtime")
138 .with_operation("WorkerProcess::request")
139 })?;
140 self.stdin.flush().await.map_err(|e| {
141 ParallaxError::new(ErrorCode::AdapterCrashed, format!("flush failed: {e}"))
142 .with_runtime(self.runtime.clone())
143 })?;
144
145 let runtime = self.runtime.clone();
146 let max_message_bytes = max_message_bytes as usize;
147 let read_fut = async {
148 loop {
149 let mut buf = String::new();
150 let n = self.stdout.read_line(&mut buf).await.map_err(|e| {
151 ParallaxError::new(ErrorCode::AdapterCrashed, format!("read failed: {e}"))
152 .with_runtime(runtime.clone())
153 })?;
154 if n == 0 {
155 let stderr = self.stderr_buf.lock().await.clone();
156 return Err(ParallaxError::new(
157 ErrorCode::AdapterCrashed,
158 "worker closed stdout",
159 )
160 .with_runtime(runtime.clone())
161 .with_diagnostic(stderr));
162 }
163 if buf.len() > max_message_bytes {
164 return Err(ParallaxError::new(
165 ErrorCode::ResourceLimitExceeded,
166 format!(
167 "inbound protocol message exceeds max_message_bytes ({max_message_bytes})"
168 ),
169 )
170 .with_runtime(runtime.clone())
171 .with_operation("WorkerProcess::request"));
172 }
173 let line = buf.trim();
174 if line.is_empty() {
175 continue;
176 }
177 let resp = Envelope::from_ndjson_line(line)?;
178 if resp.id == id {
179 return Ok(resp);
180 }
181 }
184 };
185
186 match timeout(wait, read_fut).await {
187 Ok(r) => r,
188 Err(_) => {
189 let _ = self.child.kill().await;
190 let stderr = self.stderr_buf.lock().await.clone();
191 Err(ParallaxError::new(
192 ErrorCode::ExecutionTimeout,
193 format!("worker request timed out after {:?}", wait),
194 )
195 .with_runtime(self.runtime.clone())
196 .with_operation("WorkerProcess::request")
197 .with_diagnostic(stderr))
198 }
199 }
200 }
201
202 pub async fn shutdown(&mut self) {
204 let env = Envelope::request("shutdown", serde_json::json!({}));
205 let _ = self.request(env, Duration::from_secs(2)).await;
206 let _ = self.child.kill().await;
207 if let Some(task) = self.stderr_task.take() {
208 let _ = task.await;
209 }
210 }
211}