tauri_plugin_py_runtime/runtime/
process.rs1use std::{path::PathBuf, process::Stdio};
9
10use tokio::{
11 io::{AsyncBufReadExt, AsyncReadExt, BufReader},
12 process::{Child, ChildStderr, ChildStdin, ChildStdout, Command},
13};
14
15const MAX_LOG_LINE: u64 = 64 * 1024;
17
18#[derive(Debug, Clone)]
20pub struct ProcessConfig {
21 pub program: PathBuf,
23 pub args: Vec<String>,
25 pub envs: Vec<(String, String)>,
27 pub cwd: Option<PathBuf>,
29}
30
31pub struct Spawned {
33 pub child: Child,
35 pub stdin: ChildStdin,
37 pub stdout: ChildStdout,
39 pub stderr: ChildStderr,
41}
42
43pub fn spawn(config: &ProcessConfig) -> std::io::Result<Spawned> {
51 let mut command = Command::new(&config.program);
52 command
53 .args(&config.args)
54 .envs(config.envs.iter().map(|(k, v)| (k, v)))
55 .env("PYTHONUNBUFFERED", "1")
56 .env("PYTHONIOENCODING", "utf-8")
57 .stdin(Stdio::piped())
58 .stdout(Stdio::piped())
59 .stderr(Stdio::piped())
60 .kill_on_drop(true);
61 if let Some(cwd) = &config.cwd {
62 command.current_dir(cwd);
63 }
64 #[cfg(windows)]
65 command.creation_flags(0x0800_0000); let mut child = command.spawn()?;
68 let stdin = child.stdin.take().expect("stdin is piped");
69 let stdout = child.stdout.take().expect("stdout is piped");
70 let stderr = child.stderr.take().expect("stderr is piped");
71 Ok(Spawned { child, stdin, stdout, stderr })
72}
73
74pub fn spawn_stderr_logger(stderr: ChildStderr) {
80 tauri::async_runtime::spawn(async move {
81 let mut reader = BufReader::new(stderr);
82 let mut buf = Vec::new();
83 while let Ok(n) = (&mut reader).take(MAX_LOG_LINE).read_until(b'\n', &mut buf).await {
84 if n == 0 {
85 break;
86 }
87 let line = String::from_utf8_lossy(&buf);
88 let (level, message) = parse_level(line.trim_end_matches(['\r', '\n']));
89 log::log!(target: "py-runtime", level, "{message}");
90 buf.clear();
91 }
92 });
93}
94
95pub(crate) fn parse_level(line: &str) -> (log::Level, &str) {
98 const PREFIXES: [(&str, log::Level); 4] = [
99 ("[DEBUG]", log::Level::Debug),
100 ("[INFO]", log::Level::Info),
101 ("[WARN]", log::Level::Warn),
102 ("[ERROR]", log::Level::Error),
103 ];
104 for (prefix, level) in PREFIXES {
105 if let Some(rest) = line.strip_prefix(prefix) {
106 return (level, rest.trim_start());
107 }
108 }
109 (log::Level::Info, line)
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115
116 #[test]
117 fn parses_log_prefixes() {
118 assert_eq!(parse_level("[WARN] disk low"), (log::Level::Warn, "disk low"));
119 assert_eq!(parse_level("[ERROR]boom"), (log::Level::Error, "boom"));
120 assert_eq!(parse_level("plain"), (log::Level::Info, "plain"));
121 }
122}