修复浏览器进程归属与清理诊断
固定 Windows 浏览器 root 与子进程启动身份,Job 绑定失败时回收完整进程树并拒绝 PID 复用。 保留 Web 预检 shutdown 的结构化清理诊断,补充身份不匹配回归测试。
This commit is contained in:
@@ -451,8 +451,18 @@ async fn cleanup_failed_launch(
|
||||
tree_control_confirmed: bool,
|
||||
) -> BrowserLaunchFailure {
|
||||
let subprocess_exited = process.reap().await;
|
||||
let tree_reaped =
|
||||
tree_control_confirmed && subprocess_exited && process.confirm_reaped().await.is_ok();
|
||||
let tree_reaped = if tree_control_confirmed && subprocess_exited {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
process.job.is_none() || process.confirm_reaped().await.is_ok()
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
process.confirm_reaped().await.is_ok()
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
drop(process);
|
||||
let cleanup_confirmed = if tree_reaped {
|
||||
temp.close().is_ok()
|
||||
@@ -483,9 +493,17 @@ async fn launch_owned_browser(
|
||||
}
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let root_identity = child.inner.id().and_then(|pid| {
|
||||
crate::process_identity::external_agent_runner_process_start_identity(pid)
|
||||
.ok()
|
||||
.flatten()
|
||||
});
|
||||
#[cfg(windows)]
|
||||
let job = match WindowsProcessJob::assign_tokio(&child.inner) {
|
||||
Ok(job) => {
|
||||
let Some(root_pid) = child.inner.id() else {
|
||||
let (Some(root_pid), Some(root_identity)) =
|
||||
(child.inner.id(), root_identity.as_deref())
|
||||
else {
|
||||
let process = BrowserProcessGuard {
|
||||
child,
|
||||
job: Some(job),
|
||||
@@ -498,16 +516,11 @@ async fn launch_owned_browser(
|
||||
)
|
||||
.await);
|
||||
};
|
||||
if super::sweep::ensure_browser_tree_in_job(root_pid, &job).is_ok() {
|
||||
if super::sweep::ensure_browser_tree_in_job(root_pid, root_identity, &job).is_ok() {
|
||||
Some(job)
|
||||
} else {
|
||||
let tree_reaped =
|
||||
crate::process_identity::external_agent_runner_process_start_identity(root_pid)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some_and(|identity| {
|
||||
super::sweep::kill_browser_process_tree(root_pid, &identity).is_ok()
|
||||
});
|
||||
super::sweep::kill_browser_process_tree(root_pid, root_identity).is_ok();
|
||||
let process = BrowserProcessGuard {
|
||||
child,
|
||||
job: Some(job),
|
||||
@@ -522,12 +535,18 @@ async fn launch_owned_browser(
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
let tree_reaped = match (child.inner.id(), root_identity.as_deref()) {
|
||||
(Some(root_pid), Some(root_identity)) => {
|
||||
super::sweep::kill_browser_process_tree(root_pid, root_identity).is_ok()
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let process = BrowserProcessGuard { child, job: None };
|
||||
return Err(cleanup_failed_launch(
|
||||
process,
|
||||
temp,
|
||||
OwnedBrowserLaunchError::SpawnFailed,
|
||||
false,
|
||||
tree_reaped,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
@@ -709,10 +728,8 @@ pub(crate) async fn validate_local_preview_in_browser_with_cancellation(
|
||||
_=cancelled => Err("宿主已停止本轮浏览器验证".to_string()),
|
||||
};
|
||||
|
||||
if owned.shutdown().await.is_err() {
|
||||
return Err(
|
||||
"browser-cleanup-unconfirmed: 浏览器收束后无法证明退出,请核对本轮验证进程".into(),
|
||||
);
|
||||
if let Err(error) = owned.shutdown().await {
|
||||
return Err(error);
|
||||
}
|
||||
let mut result = validation?;
|
||||
result.completed_at_unix_ms = unix_time_ms();
|
||||
|
||||
@@ -307,15 +307,17 @@ pub(super) fn kill_browser_process_tree(
|
||||
for _ in 0..TREE_KILL_MAX_PASSES {
|
||||
let snapshot = windows_process_snapshot()?;
|
||||
let root_present = snapshot.iter().any(|(pid, _)| *pid == root_pid);
|
||||
if root_present && process_identity_matches(root_pid, expected_identity) {
|
||||
if root_present {
|
||||
if !process_identity_matches(root_pid, expected_identity) {
|
||||
return Err("browser-sweep-root-identity-mismatch".into());
|
||||
}
|
||||
// root 仍是目标浏览器:发现并追踪当前整棵子树。
|
||||
for pid in windows_process_tree_pids_from(&snapshot, root_pid)? {
|
||||
track_process(&mut tracked, pid);
|
||||
}
|
||||
} else if !root_present {
|
||||
} else {
|
||||
// root 已退出且 PID 未被复用:发现临终前才拉起、仍挂在旧父
|
||||
// PID 上的孤儿子进程。PID 已被复用时不做发现,避免误认
|
||||
// 复用者的子进程。
|
||||
// PID 上的孤儿子进程。PID 已被复用时不会进入此分支。
|
||||
for (pid, ppid) in &snapshot {
|
||||
if *ppid == root_pid {
|
||||
track_process(&mut tracked, *pid);
|
||||
@@ -443,23 +445,48 @@ fn windows_process_tree_pids_from(
|
||||
#[cfg(windows)]
|
||||
pub(super) fn ensure_browser_tree_in_job(
|
||||
root_pid: u32,
|
||||
root_identity: &str,
|
||||
job: &WindowsProcessJob,
|
||||
) -> Result<(), String> {
|
||||
for _ in 0..TREE_KILL_MAX_PASSES {
|
||||
let snapshot = windows_process_snapshot()?;
|
||||
if !snapshot.iter().any(|(pid, _)| *pid == root_pid)
|
||||
|| !process_identity_matches(root_pid, root_identity)
|
||||
{
|
||||
return Err("browser-job-root-identity-unconfirmed".into());
|
||||
}
|
||||
let tree = windows_process_tree_pids_from(&snapshot, root_pid)?;
|
||||
if tree.is_empty() {
|
||||
return Err("browser-job-root-exited-before-coverage".into());
|
||||
}
|
||||
let mut members = Vec::with_capacity(tree.len());
|
||||
for pid in tree {
|
||||
let identity = if pid == root_pid {
|
||||
root_identity.to_string()
|
||||
} else {
|
||||
crate::process_identity::external_agent_runner_process_start_identity(pid)
|
||||
.ok()
|
||||
.flatten()
|
||||
.ok_or_else(|| "browser-job-process-identity-unconfirmed".to_string())?
|
||||
};
|
||||
members.push((pid, identity));
|
||||
}
|
||||
for (pid, identity) in members {
|
||||
if !process_identity_matches(pid, &identity) {
|
||||
return Err("browser-job-process-identity-changed".into());
|
||||
}
|
||||
if !job.contains_pid(pid)? {
|
||||
job.assign_pid(pid)?;
|
||||
}
|
||||
if !process_identity_matches(pid, &identity) {
|
||||
return Err("browser-job-process-identity-changed".into());
|
||||
}
|
||||
}
|
||||
|
||||
let verified = windows_process_snapshot()?;
|
||||
let verified_tree = windows_process_tree_pids_from(&verified, root_pid)?;
|
||||
if !verified_tree.is_empty()
|
||||
if process_identity_matches(root_pid, root_identity)
|
||||
&& !verified_tree.is_empty()
|
||||
&& verified_tree
|
||||
.iter()
|
||||
.all(|pid| job.contains_pid(*pid).unwrap_or(false))
|
||||
@@ -754,9 +781,12 @@ mod tests {
|
||||
.spawn()
|
||||
.expect("spawn fixture");
|
||||
let pid = child.id();
|
||||
let identity = crate::process_identity::external_agent_runner_process_start_identity(pid)
|
||||
.expect("fixture identity")
|
||||
.expect("fixture identity present");
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
let job = WindowsProcessJob::assign_std(&child).expect("assign fixture job");
|
||||
ensure_browser_tree_in_job(pid, &job).expect("adopt fixture tree");
|
||||
ensure_browser_tree_in_job(pid, &identity, &job).expect("adopt fixture tree");
|
||||
let snapshot = windows_process_snapshot().expect("snapshot after adoption");
|
||||
let tree = windows_process_tree_pids_from(&snapshot, pid).expect("tree after adoption");
|
||||
assert!(tree
|
||||
@@ -767,6 +797,28 @@ mod tests {
|
||||
assert!(job.is_empty().expect("fixture job empty"));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn kill_browser_process_tree_rejects_root_identity_mismatch() {
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
let mut child = Command::new("cmd.exe")
|
||||
.args(["/c", "ping", "127.0.0.1", "-n", "60"])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn fixture");
|
||||
let pid = child.id();
|
||||
let result = kill_browser_process_tree(pid, "not-the-fixture-identity");
|
||||
let _ = child.kill();
|
||||
let _ = child.wait();
|
||||
assert_eq!(
|
||||
result.expect_err("identity mismatch must fail closed"),
|
||||
"browser-sweep-root-identity-mismatch"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn kill_browser_process_tree_reaps_fixture_tree() {
|
||||
|
||||
Reference in New Issue
Block a user