1use std::{
19 sync::{Arc, Mutex},
20 time::{Duration, Instant},
21};
22
23use futures_util::StreamExt;
24use serde::Serialize;
25use tokio::sync::watch;
26use tokio_util::{codec::FramedRead, sync::CancellationToken};
27
28use crate::{
29 bridge::{
30 protocol::{AppInfo, FrameCodec, PROTOCOL_VERSION},
31 Bridge, CallOptions, ChannelSink, EventSink, Frame, Message, Payload, Writer,
32 },
33 error::{codes, Error, Result},
34 runtime::process::{self, ProcessConfig, Spawned},
35};
36
37const RESTART_DELAY: Duration = Duration::from_secs(1);
39const MAX_RESTART_DELAY: Duration = Duration::from_secs(30);
41const STABLE_UPTIME: Duration = Duration::from_secs(30);
43const SHUTDOWN_GRACE: Duration = Duration::from_secs(1);
45
46enum Exit {
48 Stopped,
50 Failed,
52 Crashed { code: Option<i32>, uptime: Duration },
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, strum::IntoStaticStr, strum::AsRefStr)]
58#[serde(rename_all = "lowercase")]
59#[strum(serialize_all = "lowercase")]
60pub enum Status {
61 Starting,
63 Running,
65 Crashed,
67 Stopped,
69 Failed,
71}
72
73#[derive(Debug, Clone)]
75pub struct SupervisorConfig {
76 pub process: ProcessConfig,
78 pub handshake_timeout: Duration,
80 pub invoke_timeout: Duration,
82 pub shutdown_timeout: Duration,
84 pub restart_on_crash: bool,
87 pub max_frame_size: usize,
89 pub app_name: String,
91 pub app_version: String,
93}
94
95pub struct Supervisor {
100 config: SupervisorConfig,
101 bridge: Bridge,
102 sink: Arc<dyn EventSink>,
103 status: watch::Sender<Status>,
104 writer: Mutex<Option<Writer>>,
106 last_error: Mutex<Option<Error>>,
108 stop: CancellationToken,
110 kill: CancellationToken,
112}
113
114impl Supervisor {
115 pub fn new(config: SupervisorConfig, sink: Arc<dyn EventSink>) -> Arc<Self> {
117 let (status, _) = watch::channel(Status::Starting);
118 Arc::new(Self {
119 config,
120 bridge: Bridge::new(),
121 sink,
122 status,
123 writer: Mutex::new(None),
124 last_error: Mutex::new(None),
125 stop: CancellationToken::new(),
126 kill: CancellationToken::new(),
127 })
128 }
129
130 pub fn status(&self) -> Status {
132 *self.status.borrow()
133 }
134
135 pub fn start(self: &Arc<Self>) {
140 *self.last_error.lock().unwrap() = None;
141 self.status.send_replace(Status::Starting);
142 let this = self.clone();
143 tauri::async_runtime::spawn(async move { this.run().await });
146 }
147
148 async fn run(self: Arc<Self>) {
150 let mut crashes = 0u32;
151 loop {
152 let (code, uptime) = match self.run_once().await {
153 Exit::Stopped | Exit::Failed => return,
154 Exit::Crashed { code, uptime } => (code, uptime),
155 };
156 log::error!(target: "py-runtime", "python runtime exited unexpectedly (code {code:?})");
157 self.bridge.fail_all(codes::RUNTIME_EXITED, "python runtime exited");
158
159 if !self.config.restart_on_crash {
160 self.status.send_replace(Status::Crashed);
161 self.sink.exited(code);
162 return;
163 }
164
165 self.sink.exited(code);
166 self.status.send_replace(Status::Starting);
167 crashes = if uptime >= STABLE_UPTIME { 1 } else { crashes + 1 };
169 let delay = RESTART_DELAY
170 .saturating_mul(1 << (crashes - 1).min(5))
171 .min(MAX_RESTART_DELAY);
172 log::info!(target: "py-runtime", "restarting python runtime in {delay:?}");
173 tokio::select! {
174 _ = tokio::time::sleep(delay) => {}
175 _ = self.stop.cancelled() => {}
176 }
177 if self.stop.is_cancelled() {
178 self.status.send_replace(Status::Stopped);
179 return;
180 }
181 }
182 }
183
184 async fn run_once(&self) -> Exit {
186 let Spawned { mut child, stdin, stdout, stderr } = match process::spawn(&self.config.process) {
187 Ok(spawned) => spawned,
188 Err(e) => {
189 self.fail(codes::RUNTIME_UNAVAILABLE, &format!("failed to start python: {e}"));
190 return Exit::Failed;
191 }
192 };
193 process::spawn_stderr_logger(stderr);
194 let writer = Writer::spawn(stdin, self.config.max_frame_size);
195 let mut frames = FramedRead::new(stdout, FrameCodec::new(self.config.max_frame_size));
196
197 let handshake = tokio::select! {
198 result = tokio::time::timeout(self.config.handshake_timeout, frames.next()) => result,
199 _ = self.stop.cancelled() => return self.stop_child(child).await,
200 };
201 let failure = match handshake {
202 Ok(Some(Ok(Ok(Frame { message: Message::Hello { protocol, sdk, commands }, .. })))) => {
203 if protocol == PROTOCOL_VERSION {
204 log::debug!(target: "py-runtime", "python sdk {sdk} registered {} command(s)", commands.len());
205 None
206 } else {
207 let message = format!("python sdk speaks protocol {protocol}, expected {PROTOCOL_VERSION}");
208 Some((codes::PROTOCOL_MISMATCH, message))
209 }
210 }
211 Ok(Some(Ok(Ok(frame)))) => {
212 Some((codes::RUNTIME_UNAVAILABLE, format!("expected hello, got {}", <&str>::from(&frame.message))))
213 }
214 Ok(Some(Ok(Err(e)))) => Some((codes::PROTOCOL_MISMATCH, format!("could not decode handshake: {e}"))),
216 Ok(Some(Err(e))) => Some((codes::RUNTIME_UNAVAILABLE, format!("handshake failed: {e}"))),
217 Ok(None) => Some((codes::RUNTIME_UNAVAILABLE, "python exited before handshake".into())),
218 Err(_) => Some((codes::RUNTIME_UNAVAILABLE, "handshake timed out".into())),
219 };
220 if let Some((code, message)) = failure {
221 let _ = child.kill().await;
222 self.fail(code, &message);
223 return Exit::Failed;
224 }
225
226 let app = AppInfo { name: self.config.app_name.clone(), version: self.config.app_version.clone() };
227 let _ = writer.send(Frame::new(Message::Ready { protocol: PROTOCOL_VERSION, app }));
228 *self.writer.lock().unwrap() = Some(writer);
229 if self.stop.is_cancelled() {
232 *self.writer.lock().unwrap() = None;
233 return self.stop_child(child).await;
234 }
235 self.status.send_replace(Status::Running);
236 let running_since = Instant::now();
237 log::info!(target: "py-runtime", "python runtime is running");
238
239 loop {
240 tokio::select! {
241 next = frames.next() => match next {
242 Some(Ok(Ok(frame))) => self.bridge.handle(frame, self.sink.as_ref()),
243 Some(Ok(Err(e))) => log::warn!(target: "py-runtime", "dropped frame from python: {e}"),
244 Some(Err(e)) => {
246 log::error!(target: "py-runtime", "failed to read from python: {e}");
247 let _ = child.kill().await;
248 break;
249 }
250 None => break,
251 },
252 _ = self.kill.cancelled() => break,
253 }
254 }
255
256 *self.writer.lock().unwrap() = None;
257 let exit = tokio::select! {
258 status = child.wait() => status.ok(),
259 _ = self.kill.cancelled() => {
260 let _ = child.kill().await;
261 child.wait().await.ok()
262 }
263 };
264 let code = exit.and_then(|s| s.code());
265
266 if self.stop.is_cancelled() {
267 self.bridge.fail_all(codes::RUNTIME_UNAVAILABLE, "python runtime stopped");
268 self.status.send_replace(Status::Stopped);
269 log::info!(target: "py-runtime", "python runtime stopped (code {code:?})");
270 return Exit::Stopped;
271 }
272 Exit::Crashed { code, uptime: running_since.elapsed() }
273 }
274
275 async fn stop_child(&self, mut child: tokio::process::Child) -> Exit {
277 let _ = child.kill().await;
278 let _ = child.wait().await;
279 self.bridge.fail_all(codes::RUNTIME_UNAVAILABLE, "python runtime stopped");
280 self.status.send_replace(Status::Stopped);
281 log::info!(target: "py-runtime", "python runtime stopped during startup");
282 Exit::Stopped
283 }
284
285 fn fail(&self, code: &str, message: &str) {
287 log::error!(target: "py-runtime", "{message}");
288 *self.writer.lock().unwrap() = None;
289 *self.last_error.lock().unwrap() = Some(Error::new(code, message));
290 self.bridge.fail_all(code, message);
291 self.status.send_replace(Status::Failed);
292 }
293
294 async fn wait_running(&self) -> Result<Writer> {
299 let mut rx = self.status.subscribe();
300 let ready = async {
301 loop {
302 let status = *rx.borrow_and_update();
303 if status == Status::Running {
304 if let Some(writer) = self.writer.lock().unwrap().clone() {
305 return Some(writer);
306 }
307 } else if status != Status::Starting {
308 return None;
309 }
310 if rx.changed().await.is_err() {
311 return None;
312 }
313 }
314 };
315 let writer = tokio::time::timeout(self.config.handshake_timeout, ready).await.ok().flatten();
316 writer.ok_or_else(|| {
317 let status = self.status();
318 if status == Status::Failed {
319 if let Some(error) = self.last_error.lock().unwrap().clone() {
320 return error;
321 }
322 }
323 Error::new(codes::RUNTIME_UNAVAILABLE, format!("python runtime is {}", status.as_ref()))
324 })
325 }
326
327 pub async fn call(
339 &self,
340 cmd: &str,
341 args: Payload,
342 channels: Vec<(String, ChannelSink)>,
343 opts: CallOptions,
344 ) -> Result<Payload> {
345 let writer = self.wait_running().await?;
346 self.bridge.call(&writer, cmd, args, channels, opts, self.config.invoke_timeout).await
347 }
348
349 pub fn cancel(&self, window: &str, client_id: &str) -> bool {
353 let writer = self.writer.lock().unwrap().clone();
354 self.bridge.cancel(writer.as_ref(), window, client_id)
355 }
356
357 pub fn emit(&self, event: &str, payload: Payload, window: Option<&str>) -> Result<()> {
366 let writer = self
367 .writer
368 .lock()
369 .unwrap()
370 .clone()
371 .ok_or_else(|| Error::new(codes::RUNTIME_UNAVAILABLE, "python runtime is not running"))?;
372 let (payload, body) = payload.split();
373 let message = Message::Event { event: event.into(), target: None, window: window.map(Into::into), payload };
374 writer.send(Frame { message, body })
375 }
376
377 pub async fn shutdown(&self) {
383 self.stop.cancel();
384 let budget = self.config.shutdown_timeout + SHUTDOWN_GRACE;
387 let writer = self.writer.lock().unwrap().take();
388 match writer {
389 Some(writer) => {
390 let timeout_ms = self.config.shutdown_timeout.as_millis() as u64;
391 let _ = writer.send(Frame::new(Message::Shutdown { timeout_ms }));
392 }
393 None if !matches!(self.status(), Status::Starting | Status::Running) => return,
394 None => {}
395 }
396
397 if !self.wait_stopped(budget).await {
398 log::warn!(target: "py-runtime", "python runtime did not exit in time, killing it");
399 self.kill.cancel();
400 self.wait_stopped(Duration::from_secs(1)).await;
401 }
402 }
403
404 async fn wait_stopped(&self, timeout: Duration) -> bool {
406 let mut rx = self.status.subscribe();
407 let stopped = async {
408 loop {
409 if matches!(*rx.borrow_and_update(), Status::Stopped | Status::Crashed | Status::Failed) {
410 return;
411 }
412 if rx.changed().await.is_err() {
413 return;
414 }
415 }
416 };
417 tokio::time::timeout(timeout, stopped).await.is_ok()
418 }
419}
420