继续收敛插件 stderr 错误

保留有界脱敏的插件进程 stderr

退出状态同时显示退出码和插件正文
This commit is contained in:
kdletters
2026-10-06 10:49:16 +08:00
parent 052f679965
commit 482961d1c3
2 changed files with 55 additions and 4 deletions
@@ -7,7 +7,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::{BufRead, BufReader, Write};
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Component, Path, PathBuf};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
@@ -43,6 +43,7 @@ const RPC_TIMEOUT: Duration = Duration::from_secs(10);
const EDITOR_RPC_TIMEOUT: Duration = Duration::from_secs(90);
const MAX_MANIFEST_BYTES: u64 = 1024 * 1024;
const MAX_RPC_BYTES: usize = 2 * 1024 * 1024;
const MAX_PLUGIN_STDERR_BYTES: usize = 64 * 1024;
const KNOWN_PERMISSIONS: &[&str] = &[
"events.subscribe",
@@ -163,6 +164,7 @@ struct RunningPlugin {
lines: Option<Receiver<Result<String, String>>>,
pending: PendingRpc,
registrations: Arc<Mutex<PluginRegistrations>>,
stderr_summary: Arc<Mutex<String>>,
next_request_id: u64,
editor_context: Option<ProjectContext>,
active: Arc<AtomicBool>,
@@ -578,7 +580,7 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result<RunningPlugin,
.env("AGC_PLUGIN_API_VERSION", &manifest.api_version)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
.stderr(Stdio::piped());
for variable in [
"PATH",
"SystemRoot",
@@ -606,6 +608,35 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result<RunningPlugin,
let mut child = command
.spawn()
.map_err(|error| format!("启动插件失败:{error}"))?;
let stderr = match child.stderr.take() {
Some(stderr) => stderr,
None => {
let _ = child.kill();
let _ = child.wait();
return Err("插件 stderr 不可用".to_string());
}
};
let stderr_summary = Arc::new(Mutex::new(String::new()));
let stderr_summary_writer = Arc::clone(&stderr_summary);
thread::spawn(move || {
let mut bytes = Vec::new();
let result = stderr
.take((MAX_PLUGIN_STDERR_BYTES + 1) as u64)
.read_to_end(&mut bytes);
let summary = match result {
Ok(_) if bytes.len() > MAX_PLUGIN_STDERR_BYTES => {
"插件 stderr 超过大小上限".to_string()
}
Ok(_) => crate::sanitize_diagnostic_message(&String::from_utf8_lossy(&bytes), None)
.chars()
.take(1200)
.collect(),
Err(error) => format!("读取插件 stderr 失败:{error}"),
};
if let Ok(mut stored) = stderr_summary_writer.lock() {
*stored = summary;
}
});
#[cfg(windows)]
let job = match crate::process_session::WindowsProcessJob::assign_std(&child) {
Ok(job) => job,
@@ -651,6 +682,7 @@ fn spawn_plugin(manifest: &PluginManifest, root: &Path) -> Result<RunningPlugin,
lines: Some(receiver),
pending: Arc::new(Mutex::new(BTreeMap::new())),
registrations: Arc::new(Mutex::new(PluginRegistrations::default())),
stderr_summary,
next_request_id: 1,
editor_context: None,
active: Arc::new(AtomicBool::new(true)),
@@ -982,9 +1014,27 @@ impl PluginHost {
.as_mut()
.and_then(|running| match running.child.try_wait() {
Ok(Some(status)) => Some(if let Some(code) = status.code() {
format!("插件进程已退出:exitCode={code}")
format!(
"插件进程已退出:exitCode={code}{}",
running
.stderr_summary
.lock()
.ok()
.filter(|summary| !summary.is_empty())
.map(|summary| format!(";stderr={summary}"))
.unwrap_or_default()
)
} else {
"插件进程已被信号终止".to_string()
format!(
"插件进程已被信号终止{}",
running
.stderr_summary
.lock()
.ok()
.filter(|summary| !summary.is_empty())
.map(|summary| format!(";stderr={summary}"))
.unwrap_or_default()
)
}),
Ok(None) => None,
Err(error) => Some(format!("读取插件进程状态失败:{error}")),
@@ -100,3 +100,4 @@ DirectProject 回合失败在确认不是客户端内部不可归类故障时,
- 发布构建预检原先把 `package.json` 元数据/读取/解析错误当成“没有 build”,把 `package-lock.json` 读取/解析错误当成“没有锁定版本”;现分别区分 NotFound 与真实读盘/JSON 错误,保留相对工作区路径和底层正文。
- 发布尝试账本和工作区偏好文件原先把损坏、权限、类型和 JSON 解析失败当成空账本/默认偏好;现只有文件不存在才使用默认,其他读取错误沿发布幂等和建项目录命令返回具体正文。
- 插件主机扫描工作区、读取进程状态、停止插件和禁用插件时原先吞掉目录项/`try_wait`/kill/wait 错误;现保留插件状态错误并阻止“停止失败却显示已禁用”,正常主动 kill 的非零退出码不再误报。
- 插件进程 stderr 原先使用 `Stdio::null()`,即使进程退出也只能显示退出码;现以有界脱敏 sidecar 保留插件 stderr,并在退出状态中附带正文。