修复 Runner GUI owner 跨重启登记
保存 GUI owner attach 参数并按 AppData 与 bootId 重放 在 Runner endpoint 返回前失败关闭未完成的 owner 登记 补充幂等、重试、配置隔离测试和生命周期文档
This commit is contained in:
@@ -8,6 +8,7 @@ use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -16,6 +17,76 @@ const AGENT_RUNNER_LOG_INPUT_LINE_MAX_BYTES: usize = 8 * 1024;
|
||||
const AGENT_RUNNER_LOG_OUTPUT_MAX_CHARS: usize = 1_024;
|
||||
const AGENT_RUNNER_CLIENT_EXIT_TIMEOUT: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct ExternalAgentRunnerGuiOwnerAttachmentState {
|
||||
generation: u64,
|
||||
registration: Option<ExternalAgentRunnerGuiOwnerRegistration>,
|
||||
}
|
||||
|
||||
struct ExternalAgentRunnerGuiOwnerRegistration {
|
||||
generation: u64,
|
||||
config_dir: PathBuf,
|
||||
params: ExternalAgentRunnerRequestParams,
|
||||
attached_boot_id: Option<String>,
|
||||
}
|
||||
|
||||
static EXTERNAL_AGENT_RUNNER_GUI_OWNER_ATTACHMENT_STATE: OnceLock<
|
||||
Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
||||
> = OnceLock::new();
|
||||
|
||||
fn external_agent_runner_gui_owner_attachment_state(
|
||||
) -> &'static Mutex<ExternalAgentRunnerGuiOwnerAttachmentState> {
|
||||
EXTERNAL_AGENT_RUNNER_GUI_OWNER_ATTACHMENT_STATE
|
||||
.get_or_init(|| Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default()))
|
||||
}
|
||||
|
||||
pub(super) fn register_external_agent_runner_gui_owner_attachment(
|
||||
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
||||
config_dir: &Path,
|
||||
params: ExternalAgentRunnerRequestParams,
|
||||
) {
|
||||
let mut state = lock_unpoisoned(state);
|
||||
state.generation = state.generation.wrapping_add(1);
|
||||
let generation = state.generation;
|
||||
state.registration = Some(ExternalAgentRunnerGuiOwnerRegistration {
|
||||
generation,
|
||||
config_dir: config_dir.to_path_buf(),
|
||||
params,
|
||||
attached_boot_id: None,
|
||||
});
|
||||
}
|
||||
|
||||
pub(super) fn attach_registered_external_agent_runner_gui_owner_if_needed_with<F>(
|
||||
state: &Mutex<ExternalAgentRunnerGuiOwnerAttachmentState>,
|
||||
config_dir: &Path,
|
||||
endpoint: &ExternalAgentRunnerEndpoint,
|
||||
attach: F,
|
||||
) -> Result<(), String>
|
||||
where
|
||||
F: FnOnce(&ExternalAgentRunnerEndpoint, ExternalAgentRunnerRequestParams) -> Result<(), String>,
|
||||
{
|
||||
let Some((generation, params)) = ({
|
||||
let state = lock_unpoisoned(state);
|
||||
state.registration.as_ref().and_then(|registration| {
|
||||
(registration.config_dir == config_dir
|
||||
&& registration.attached_boot_id.as_deref() != Some(endpoint.boot_id.as_str()))
|
||||
.then(|| (registration.generation, registration.params.clone()))
|
||||
})
|
||||
}) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
attach(endpoint, params)?;
|
||||
|
||||
let mut state = lock_unpoisoned(state);
|
||||
if let Some(registration) = state.registration.as_mut() {
|
||||
if registration.generation == generation && registration.config_dir == config_dir {
|
||||
registration.attached_boot_id = Some(endpoint.boot_id.clone());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn redact_url_queries(line: &str) -> String {
|
||||
line.split_whitespace()
|
||||
.map(|token| {
|
||||
@@ -914,14 +985,22 @@ pub(crate) fn shutdown_external_agent_runner() -> Result<(), String> {
|
||||
pub(crate) fn attach_external_agent_runner_gui_owner() -> Result<(), String> {
|
||||
EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT
|
||||
.store(true, std::sync::atomic::Ordering::Release);
|
||||
let _configure = lock_unpoisoned(external_agent_runner_configure_lock());
|
||||
let config_dir = external_agent_runner_config_dir()
|
||||
.ok_or_else(|| "外部 Agent Runner 尚未配置 AppData;请显式传入 --config-dir".to_string())?;
|
||||
let endpoint = ensure_external_agent_runner(&config_dir)?;
|
||||
let result = send_external_agent_runner_request(
|
||||
&endpoint,
|
||||
"runner.attach_gui_owner",
|
||||
register_external_agent_runner_gui_owner_attachment(
|
||||
external_agent_runner_gui_owner_attachment_state(),
|
||||
&config_dir,
|
||||
ExternalAgentRunnerRequestParams::default(),
|
||||
)?;
|
||||
);
|
||||
ensure_external_agent_runner(&config_dir).map(|_| ())
|
||||
}
|
||||
|
||||
fn attach_external_agent_runner_gui_owner_at(
|
||||
endpoint: &ExternalAgentRunnerEndpoint,
|
||||
params: ExternalAgentRunnerRequestParams,
|
||||
) -> Result<(), String> {
|
||||
let result = send_external_agent_runner_request(endpoint, "runner.attach_gui_owner", params)?;
|
||||
if result.get("attached").and_then(Value::as_bool) == Some(true) {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -929,6 +1008,18 @@ pub(crate) fn attach_external_agent_runner_gui_owner() -> Result<(), String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn attach_registered_external_agent_runner_gui_owner_if_needed(
|
||||
config_dir: &Path,
|
||||
endpoint: &ExternalAgentRunnerEndpoint,
|
||||
) -> Result<(), String> {
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
external_agent_runner_gui_owner_attachment_state(),
|
||||
config_dir,
|
||||
endpoint,
|
||||
attach_external_agent_runner_gui_owner_at,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn shutdown_external_agent_runner_for_client_exit_at(
|
||||
config_dir: &Path,
|
||||
) -> Result<bool, String> {
|
||||
@@ -1024,6 +1115,9 @@ pub(super) fn ensure_external_agent_runner(
|
||||
match external_agent_runner_endpoint_reuse_decision(&endpoint, &executable_fingerprint) {
|
||||
ExternalAgentRunnerReuseDecision::Reuse => {
|
||||
if ping_external_agent_runner(&endpoint).is_ok() {
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed(
|
||||
config_dir, &endpoint,
|
||||
)?;
|
||||
return Ok(endpoint);
|
||||
}
|
||||
}
|
||||
@@ -1057,6 +1151,7 @@ pub(super) fn ensure_external_agent_runner(
|
||||
let _ = launched.child.wait();
|
||||
})
|
||||
.map_err(|error| format!("启动 Agent Runner 子进程回收线程失败:{error}"))?;
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed(config_dir, &endpoint)?;
|
||||
Ok(endpoint)
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::io::{self, Cursor};
|
||||
use std::net::{Ipv4Addr, SocketAddrV4, TcpListener};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
static TEST_DIRECTORY_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -551,6 +552,170 @@ fn runner_endpoint_rejects_hard_links() {
|
||||
assert!(error.contains("硬链接"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_owner_registration_replays_once_for_each_runner_boot() {
|
||||
let state = Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default());
|
||||
let config_dir = PathBuf::from("registered-gui-appdata");
|
||||
let params = ExternalAgentRunnerRequestParams {
|
||||
action_id: Some("registered-owner-params".to_string()),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
};
|
||||
register_external_agent_runner_gui_owner_attachment(&state, &config_dir, params);
|
||||
|
||||
let calls = std::cell::RefCell::new(Vec::new());
|
||||
let endpoint_a = test_endpoint(
|
||||
"gui-owner-replay-token-gui-owner-replay-token",
|
||||
"gui-owner-boot-a",
|
||||
31318,
|
||||
);
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint_a,
|
||||
|endpoint, params| {
|
||||
calls.borrow_mut().push((
|
||||
endpoint.boot_id.clone(),
|
||||
params.action_id.expect("registered params are retained"),
|
||||
));
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("first boot attaches");
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint_a,
|
||||
|_, _| panic!("same boot must not attach twice"),
|
||||
)
|
||||
.expect("same boot is idempotent");
|
||||
|
||||
let endpoint_b = test_endpoint(
|
||||
"gui-owner-replay-token-gui-owner-replay-token",
|
||||
"gui-owner-boot-b",
|
||||
31319,
|
||||
);
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint_b,
|
||||
|endpoint, params| {
|
||||
calls.borrow_mut().push((
|
||||
endpoint.boot_id.clone(),
|
||||
params.action_id.expect("registered params are replayed"),
|
||||
));
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("replacement boot reattaches");
|
||||
|
||||
assert_eq!(
|
||||
calls.into_inner(),
|
||||
vec![
|
||||
(
|
||||
"gui-owner-boot-a".to_string(),
|
||||
"registered-owner-params".to_string()
|
||||
),
|
||||
(
|
||||
"gui-owner-boot-b".to_string(),
|
||||
"registered-owner-params".to_string()
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_owner_registration_failed_replay_remains_pending_for_same_boot() {
|
||||
let state = Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default());
|
||||
let config_dir = PathBuf::from("retry-gui-appdata");
|
||||
register_external_agent_runner_gui_owner_attachment(
|
||||
&state,
|
||||
&config_dir,
|
||||
ExternalAgentRunnerRequestParams::default(),
|
||||
);
|
||||
let endpoint = test_endpoint(
|
||||
"gui-owner-retry-token-gui-owner-retry-token",
|
||||
"gui-owner-retry-boot",
|
||||
31320,
|
||||
);
|
||||
let attempts = std::cell::Cell::new(0_u32);
|
||||
|
||||
let error = attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint,
|
||||
|_, _| {
|
||||
attempts.set(attempts.get() + 1);
|
||||
Err("injected attach failure".to_string())
|
||||
},
|
||||
)
|
||||
.expect_err("failed attach must remain pending");
|
||||
assert_eq!(error, "injected attach failure");
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint,
|
||||
|_, _| {
|
||||
attempts.set(attempts.get() + 1);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("same boot retries after failure");
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&config_dir,
|
||||
&endpoint,
|
||||
|_, _| panic!("successful retry must mark the boot attached"),
|
||||
)
|
||||
.expect("successful retry is idempotent");
|
||||
assert_eq!(attempts.get(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_owner_registration_does_not_cross_config_dirs() {
|
||||
let state = Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default());
|
||||
let registered_config_dir = PathBuf::from("registered-gui-appdata");
|
||||
let other_config_dir = PathBuf::from("other-gui-appdata");
|
||||
register_external_agent_runner_gui_owner_attachment(
|
||||
&state,
|
||||
®istered_config_dir,
|
||||
ExternalAgentRunnerRequestParams::default(),
|
||||
);
|
||||
let endpoint = test_endpoint(
|
||||
"gui-owner-config-token-gui-owner-config-token",
|
||||
"gui-owner-config-boot",
|
||||
31321,
|
||||
);
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
&other_config_dir,
|
||||
&endpoint,
|
||||
|_, _| panic!("GUI owner registration must stay bound to its AppData"),
|
||||
)
|
||||
.expect("other AppData remains unattached");
|
||||
|
||||
let calls = std::cell::Cell::new(0_u32);
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&state,
|
||||
®istered_config_dir,
|
||||
&endpoint,
|
||||
|_, _| {
|
||||
calls.set(calls.get() + 1);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("registered AppData attaches");
|
||||
assert_eq!(calls.get(), 1);
|
||||
|
||||
let unregistered = Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default());
|
||||
attach_registered_external_agent_runner_gui_owner_if_needed_with(
|
||||
&unregistered,
|
||||
®istered_config_dir,
|
||||
&endpoint,
|
||||
|_, _| panic!("CLI state without GUI registration must not attach"),
|
||||
)
|
||||
.expect("unregistered CLI state remains unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gui_owner_lock_allows_only_one_frontend_process_per_appdata() {
|
||||
let directory = unique_test_directory();
|
||||
|
||||
Reference in New Issue
Block a user