Skip to main content

tauri_plugin_py_runtime/runtime/
process.rs

1//! Python 子进程的创建与 stderr 日志转发。
2//!
3//! 架构位置:由 [`crate::runtime::Supervisor`] 调用,产出的 stdin / stdout 分别交给
4//! [`crate::bridge::Writer`] 与 [`crate::bridge::protocol::FrameCodec`]。
5//!
6//! 不变量:stdout 专用于协议帧,日志只能走 stderr(Python SDK 会把 `sys.stdout` 重定向到 stderr)。
7
8use std::{path::PathBuf, process::Stdio};
9
10use tokio::{
11  io::{AsyncBufReadExt, AsyncReadExt, BufReader},
12  process::{Child, ChildStderr, ChildStdin, ChildStdout, Command},
13};
14
15/// 单条日志的最大字节数;超长的行会被拆成多条,避免无换行的输出无限占用内存。
16const MAX_LOG_LINE: u64 = 64 * 1024;
17
18/// 启动 Python 进程所需的参数。
19#[derive(Debug, Clone)]
20pub struct ProcessConfig {
21  /// Python 解释器路径或命令名。
22  pub program: PathBuf,
23  /// 命令行参数,插件构造时第一项为解析后的入口脚本路径。
24  pub args: Vec<String>,
25  /// 额外的环境变量;`PYTHONUNBUFFERED` 与 `PYTHONIOENCODING` 总会被强制设置。
26  pub envs: Vec<(String, String)>,
27  /// 工作目录;`None` 时继承当前进程的工作目录。
28  pub cwd: Option<PathBuf>,
29}
30
31/// 已启动的子进程及其三个标准流。
32pub struct Spawned {
33  /// 子进程句柄,被丢弃时进程会被杀死(`kill_on_drop`)。
34  pub child: Child,
35  /// 协议写入端。
36  pub stdin: ChildStdin,
37  /// 协议读取端。
38  pub stdout: ChildStdout,
39  /// 日志输出,交给 [`spawn_stderr_logger`]。
40  pub stderr: ChildStderr,
41}
42
43/// 按 `config` 启动 Python 进程,三个标准流均为管道。
44///
45/// Windows 上以 `CREATE_NO_WINDOW` 启动,避免弹出控制台窗口。
46///
47/// # Errors
48///
49/// 进程无法创建时(如解释器不存在)返回底层 I/O 错误。
50pub 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); // CREATE_NO_WINDOW
66
67  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
74/// 启动后台任务,把 stderr 逐行转发到 `log`(target 为 `py-runtime`)。
75///
76/// 行首的 `[DEBUG]` / `[INFO]` / `[WARN]` / `[ERROR]` 决定日志级别并被去掉,其余行按 `INFO` 记录。
77///
78/// 非 UTF-8 字节按有损方式解码;stderr 关闭或读取出错时任务结束。
79pub 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
95/// 解析行首的 `[DEBUG]` / `[INFO]` / `[WARN]` / `[ERROR]` 前缀,返回日志级别与去掉前缀后的内容;
96/// 无前缀时为 `INFO` 级别。
97pub(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}