fix: isolate core processes per installation directory
- Match core processes by their actual executable paths - Prevent different EasyCLIProxyAPI installations from interfering - Adopt orphaned core processes from the current cpa-core directory - Rescan and terminate owned core processes during shutdown - Enforce a single app instance per installation directory - Clean up core processes that fail to enter the Windows job object - Add multi-directory process isolation and instance lock tests
This commit is contained in:
1 parent
c4a66fd99c
commit
647806386b
7 files changed
+566
-94
No files matched your search
@@ -50,6 +50,7 @@ windows-sys = { version = "0.59", features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Security",
|
||||
"Win32_Storage_FileSystem",
|
||||
"Win32_System_Diagnostics_ToolHelp",
|
||||
"Win32_System_JobObjects",
|
||||
"Win32_System_Threading",
|
||||
] }
|
||||
+206
-76
@@ -1003,7 +1003,11 @@ pub(crate) fn start_core_process_inner(
|
||||
let binary_path = find_core_binary(&install_dir)
|
||||
.ok_or_else(|| "未安装 CPA 内核,请先安装最新版".to_string())?;
|
||||
|
||||
if process_state.managed_pid().is_some() || is_core_running(&binary_path) {
|
||||
let existing_process_ids = find_core_process_ids(&binary_path);
|
||||
if process_state.managed_pid().is_some() || !existing_process_ids.is_empty() {
|
||||
if !existing_process_ids.is_empty() {
|
||||
process_state.adopt_process_ids(&binary_path, existing_process_ids)?;
|
||||
}
|
||||
return Err("CPA 内核已经在运行".to_string());
|
||||
}
|
||||
let management_address = core_management_address(&gui_config.host, gui_config.port)?;
|
||||
@@ -1124,26 +1128,52 @@ pub(crate) fn configure_networked_command(command: &mut Command, proxy_url: &str
|
||||
}
|
||||
|
||||
pub(crate) fn stop_core_process_inner(process_state: &CoreProcessState) -> Result<(), String> {
|
||||
let mut stopped_any = false;
|
||||
let mut errors = Vec::new();
|
||||
|
||||
if let Some(mut child) = process_state.take_child() {
|
||||
terminate_child(&mut child)?;
|
||||
stopped_any = true;
|
||||
if let Err(error) = terminate_child(&mut child) {
|
||||
errors.push(error);
|
||||
}
|
||||
process_state.clear_lifetime_guard();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let install_dir = core_install_dir()?;
|
||||
let binary_path = find_core_binary(&install_dir)
|
||||
.ok_or_else(|| "未安装 CPA 内核,请先安装最新版".to_string())?;
|
||||
let process_ids = find_core_process_ids(&binary_path);
|
||||
|
||||
if process_ids.is_empty() {
|
||||
return Err("CPA 内核当前未运行".to_string());
|
||||
let mut process_ids = process_state
|
||||
.take_adopted_processes()
|
||||
.into_iter()
|
||||
.filter(|process| {
|
||||
process_executable_path(process.process_id)
|
||||
.as_deref()
|
||||
.is_some_and(|path| executable_paths_match(&process.binary_path, path))
|
||||
})
|
||||
.map(|process| process.process_id)
|
||||
.collect::<Vec<_>>();
|
||||
if let Ok(install_dir) = core_install_dir() {
|
||||
if let Some(binary_path) = find_core_binary(&install_dir) {
|
||||
process_ids.extend(find_core_process_ids(&binary_path));
|
||||
}
|
||||
}
|
||||
process_ids.sort_unstable();
|
||||
process_ids.dedup();
|
||||
|
||||
for process_id in process_ids {
|
||||
terminate_process_id(process_id)?;
|
||||
if !is_process_alive(process_id) {
|
||||
continue;
|
||||
}
|
||||
stopped_any = true;
|
||||
if let Err(error) = terminate_process_id(process_id) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if !errors.is_empty() {
|
||||
Err(errors.join(";"))
|
||||
} else if stopped_any {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("CPA 内核当前未运行".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn core_install_dir() -> Result<PathBuf, String> {
|
||||
@@ -1545,75 +1575,76 @@ pub(crate) fn is_core_running(binary_path: &Path) -> bool {
|
||||
}
|
||||
|
||||
pub(crate) fn find_core_process_ids(binary_path: &Path) -> Vec<u32> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
find_core_process_ids_linux(binary_path)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
let _ = binary_path;
|
||||
find_core_process_ids_by_name()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn find_core_process_ids_linux(binary_path: &Path) -> Vec<u32> {
|
||||
let Ok(expected) = fs::canonicalize(binary_path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let output = Command::new("pgrep")
|
||||
.args(["-x", core_binary_name()])
|
||||
.output();
|
||||
let Ok(output) = output else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| line.trim().parse::<u32>().ok())
|
||||
.filter(|pid| {
|
||||
fs::read_link(format!("/proc/{pid}/exe"))
|
||||
.ok()
|
||||
.and_then(|path| fs::canonicalize(path).ok())
|
||||
.map(|path| path == expected)
|
||||
.unwrap_or(false)
|
||||
find_candidate_core_process_ids()
|
||||
.into_iter()
|
||||
.filter(|process_id| {
|
||||
process_executable_path(*process_id)
|
||||
.as_deref()
|
||||
.is_some_and(|path| executable_paths_match(binary_path, path))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn executable_paths_match(expected: &Path, actual: &Path) -> bool {
|
||||
let expected = fs::canonicalize(expected).unwrap_or_else(|_| expected.to_path_buf());
|
||||
let actual = fs::canonicalize(actual).unwrap_or_else(|_| actual.to_path_buf());
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
path_to_string(&expected).eq_ignore_ascii_case(&path_to_string(&actual))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
expected == actual
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), target_os = "windows"))]
|
||||
pub(crate) fn find_core_process_ids_by_name() -> Vec<u32> {
|
||||
let image_name = core_binary_name();
|
||||
let filter = format!("IMAGENAME eq {image_name}");
|
||||
let mut command = Command::new("tasklist");
|
||||
command.args(["/FI", &filter, "/FO", "CSV", "/NH"]);
|
||||
configure_background_command(&mut command);
|
||||
let output = command.output();
|
||||
let Ok(output) = output else {
|
||||
return Vec::new();
|
||||
pub(crate) fn find_candidate_core_process_ids() -> Vec<u32> {
|
||||
use std::{ffi::OsString, mem, os::windows::ffi::OsStringExt};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, INVALID_HANDLE_VALUE},
|
||||
System::Diagnostics::ToolHelp::{
|
||||
CreateToolhelp32Snapshot, Process32FirstW, Process32NextW, PROCESSENTRY32W,
|
||||
TH32CS_SNAPPROCESS,
|
||||
},
|
||||
};
|
||||
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let columns = line
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.split("\",\"")
|
||||
.collect::<Vec<_>>();
|
||||
let name = columns.first()?;
|
||||
let pid = columns.get(1)?;
|
||||
let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) };
|
||||
if snapshot == INVALID_HANDLE_VALUE {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
name.eq_ignore_ascii_case(image_name)
|
||||
.then(|| pid.parse::<u32>().ok())
|
||||
.flatten()
|
||||
})
|
||||
.collect()
|
||||
let mut entry = unsafe { mem::zeroed::<PROCESSENTRY32W>() };
|
||||
entry.dwSize = mem::size_of::<PROCESSENTRY32W>() as u32;
|
||||
let mut process_ids = Vec::new();
|
||||
if unsafe { Process32FirstW(snapshot, &mut entry) } != 0 {
|
||||
loop {
|
||||
let name_length = entry
|
||||
.szExeFile
|
||||
.iter()
|
||||
.position(|character| *character == 0)
|
||||
.unwrap_or(entry.szExeFile.len());
|
||||
let image_name = OsString::from_wide(&entry.szExeFile[..name_length]);
|
||||
if entry.th32ProcessID != 0
|
||||
&& image_name
|
||||
.to_string_lossy()
|
||||
.eq_ignore_ascii_case(core_binary_name())
|
||||
{
|
||||
process_ids.push(entry.th32ProcessID);
|
||||
}
|
||||
if unsafe { Process32NextW(snapshot, &mut entry) } == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe { CloseHandle(snapshot) };
|
||||
process_ids
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "linux"), not(target_os = "windows")))]
|
||||
pub(crate) fn find_core_process_ids_by_name() -> Vec<u32> {
|
||||
pub(crate) fn find_candidate_core_process_ids() -> Vec<u32> {
|
||||
Command::new("pgrep")
|
||||
.args(["-x", core_binary_name()])
|
||||
.output()
|
||||
@@ -1627,17 +1658,99 @@ pub(crate) fn find_core_process_ids_by_name() -> Vec<u32> {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn find_candidate_core_process_ids() -> Vec<u32> {
|
||||
Command::new("pgrep")
|
||||
.args(["-x", core_binary_name()])
|
||||
.output()
|
||||
.ok()
|
||||
.map(|output| {
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| line.trim().parse::<u32>().ok())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn process_executable_path(process_id: u32) -> Option<PathBuf> {
|
||||
fs::read_link(format!("/proc/{process_id}/exe")).ok()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn process_executable_path(process_id: u32) -> Option<PathBuf> {
|
||||
use std::{ffi::OsString, os::unix::ffi::OsStringExt};
|
||||
|
||||
extern "C" {
|
||||
fn proc_pidpath(pid: i32, buffer: *mut std::ffi::c_void, buffer_size: u32) -> i32;
|
||||
}
|
||||
|
||||
let mut buffer = vec![0_u8; 4096];
|
||||
let length = unsafe {
|
||||
proc_pidpath(
|
||||
process_id.try_into().ok()?,
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len() as u32,
|
||||
)
|
||||
};
|
||||
if length <= 0 {
|
||||
return None;
|
||||
}
|
||||
buffer.truncate(length as usize);
|
||||
Some(PathBuf::from(OsString::from_vec(buffer)))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn process_executable_path(process_id: u32) -> Option<PathBuf> {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, HANDLE},
|
||||
System::Threading::{
|
||||
OpenProcess, QueryFullProcessImageNameW, PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
},
|
||||
};
|
||||
|
||||
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, process_id) };
|
||||
if handle.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut buffer = vec![0_u16; 32_768];
|
||||
let mut length = buffer.len() as u32;
|
||||
let success =
|
||||
unsafe { QueryFullProcessImageNameW(handle, 0, buffer.as_mut_ptr(), &mut length) };
|
||||
unsafe { CloseHandle(handle as HANDLE) };
|
||||
if success == 0 {
|
||||
return None;
|
||||
}
|
||||
buffer.truncate(length as usize);
|
||||
Some(PathBuf::from(OsString::from_wide(&buffer)))
|
||||
}
|
||||
|
||||
pub(crate) fn adopt_existing_core_processes(
|
||||
process_state: &CoreProcessState,
|
||||
) -> Result<Vec<u32>, String> {
|
||||
let install_dir = core_install_dir()?;
|
||||
let Some(binary_path) = find_core_binary(&install_dir) else {
|
||||
process_state.clear_adopted_processes()?;
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
let process_ids = find_core_process_ids(&binary_path);
|
||||
process_state.adopt_process_ids(&binary_path, process_ids.clone())?;
|
||||
Ok(process_ids)
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown_managed_core(
|
||||
process_state: &CoreProcessState,
|
||||
gui_config_state: &GuiConfigState,
|
||||
) {
|
||||
let was_running = process_state.managed_pid().is_some();
|
||||
let was_running = process_state.managed_pid().is_some()
|
||||
|| core_install_dir()
|
||||
.ok()
|
||||
.and_then(|install_dir| find_core_binary(&install_dir))
|
||||
.is_some_and(|binary_path| is_core_running(&binary_path));
|
||||
let _ = gui_config_state.set_run_on_startup(was_running);
|
||||
|
||||
if let Some(mut child) = process_state.take_child() {
|
||||
let _ = terminate_child(&mut child);
|
||||
}
|
||||
process_state.clear_lifetime_guard();
|
||||
let _ = stop_core_process_inner(process_state);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
@@ -1782,6 +1895,23 @@ pub(crate) fn send_process_signal(process_id: u32, signal: &str) -> Result<(), S
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn is_process_alive(process_id: u32) -> bool {
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, WAIT_TIMEOUT},
|
||||
System::Threading::{OpenProcess, WaitForSingleObject},
|
||||
};
|
||||
|
||||
const SYNCHRONIZE_ACCESS: u32 = 0x0010_0000;
|
||||
let handle = unsafe { OpenProcess(SYNCHRONIZE_ACCESS, 0, process_id) };
|
||||
if handle.is_null() {
|
||||
return false;
|
||||
}
|
||||
let result = unsafe { WaitForSingleObject(handle, 0) };
|
||||
unsafe { CloseHandle(handle) };
|
||||
result == WAIT_TIMEOUT
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub(crate) fn is_process_alive(process_id: u32) -> bool {
|
||||
let process_id = process_id.to_string();
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs, path::Path};
|
||||
|
||||
const APP_INSTANCE_LOCK_PREFIX: &str = "EasyCLIProxyAPI-instance";
|
||||
|
||||
pub(crate) struct AppInstanceGuard {
|
||||
#[cfg(windows)]
|
||||
handle: isize,
|
||||
#[cfg(unix)]
|
||||
_file: fs::File,
|
||||
}
|
||||
|
||||
pub(crate) fn acquire_app_instance_guard() -> Result<AppInstanceGuard, String> {
|
||||
let executable_dir = super::executable_dir()?;
|
||||
acquire_app_instance_guard_for(&executable_dir)
|
||||
}
|
||||
|
||||
pub(crate) fn app_instance_key(executable_dir: &Path) -> String {
|
||||
let resolved = fs::canonicalize(executable_dir)
|
||||
.unwrap_or_else(|_| executable_dir.to_path_buf())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
#[cfg(windows)]
|
||||
let resolved = resolved.to_lowercase();
|
||||
let digest = Sha256::digest(resolved.as_bytes());
|
||||
format!("{digest:x}")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) fn acquire_app_instance_guard_for(
|
||||
executable_dir: &Path,
|
||||
) -> Result<AppInstanceGuard, String> {
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use windows_sys::Win32::{
|
||||
Foundation::{CloseHandle, GetLastError, ERROR_ALREADY_EXISTS},
|
||||
System::Threading::CreateMutexW,
|
||||
};
|
||||
|
||||
let name = format!(
|
||||
"Local\\{APP_INSTANCE_LOCK_PREFIX}-{}",
|
||||
app_instance_key(executable_dir)
|
||||
);
|
||||
let wide_name = std::ffi::OsStr::new(&name)
|
||||
.encode_wide()
|
||||
.chain(Some(0))
|
||||
.collect::<Vec<_>>();
|
||||
let handle = unsafe { CreateMutexW(std::ptr::null(), 0, wide_name.as_ptr()) };
|
||||
if handle.is_null() {
|
||||
return Err(format!(
|
||||
"创建当前目录的软件实例锁失败: {}",
|
||||
std::io::Error::last_os_error()
|
||||
));
|
||||
}
|
||||
if unsafe { GetLastError() } == ERROR_ALREADY_EXISTS {
|
||||
unsafe { CloseHandle(handle) };
|
||||
return Err("当前 EasyCLIProxyAPI 目录已经有一个软件实例在运行".to_string());
|
||||
}
|
||||
|
||||
Ok(AppInstanceGuard {
|
||||
handle: handle as isize,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) fn acquire_app_instance_guard_for(
|
||||
executable_dir: &Path,
|
||||
) -> Result<AppInstanceGuard, String> {
|
||||
use std::{fs::OpenOptions, os::fd::AsRawFd};
|
||||
|
||||
let lock_path = std::env::temp_dir().join(format!(
|
||||
"{APP_INSTANCE_LOCK_PREFIX}-{}.lock",
|
||||
app_instance_key(executable_dir)
|
||||
));
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&lock_path)
|
||||
.map_err(|error| format!("打开当前目录的软件实例锁失败: {error}"))?;
|
||||
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
||||
if result != 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
let raw_error = error.raw_os_error();
|
||||
if raw_error == Some(libc::EWOULDBLOCK) || raw_error == Some(libc::EAGAIN) {
|
||||
return Err("当前 EasyCLIProxyAPI 目录已经有一个软件实例在运行".to_string());
|
||||
}
|
||||
return Err(format!("锁定当前 EasyCLIProxyAPI 目录失败: {error}"));
|
||||
}
|
||||
|
||||
Ok(AppInstanceGuard { _file: file })
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
impl Drop for AppInstanceGuard {
|
||||
fn drop(&mut self) {
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
|
||||
|
||||
unsafe { CloseHandle(self.handle as HANDLE) };
|
||||
}
|
||||
}
|
||||
+115
-18
@@ -9,6 +9,7 @@ mod codex_sessions;
|
||||
mod configuration_watcher;
|
||||
mod core_config;
|
||||
mod core_runtime;
|
||||
mod instance_lock;
|
||||
mod management_api;
|
||||
mod oauth_browser;
|
||||
mod provider_health;
|
||||
@@ -36,6 +37,7 @@ use core_config::*;
|
||||
use core_runtime::*;
|
||||
use flate2::read::GzDecoder;
|
||||
use futures_util::StreamExt;
|
||||
use instance_lock::*;
|
||||
#[cfg(target_os = "macos")]
|
||||
use objc2::MainThreadMarker;
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -290,11 +292,18 @@ struct AppUpdateInner {
|
||||
#[derive(Default)]
|
||||
struct CoreProcessState {
|
||||
child: Mutex<Option<Child>>,
|
||||
adopted_processes: Mutex<Vec<AdoptedCoreProcess>>,
|
||||
starting: AtomicBool,
|
||||
#[cfg(windows)]
|
||||
job: Mutex<Option<isize>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AdoptedCoreProcess {
|
||||
process_id: u32,
|
||||
binary_path: PathBuf,
|
||||
}
|
||||
|
||||
struct GuiConfigState {
|
||||
inner: Mutex<GuiConfigFile>,
|
||||
}
|
||||
@@ -1441,6 +1450,7 @@ impl CoreProcessState {
|
||||
fn new(starting: bool) -> Self {
|
||||
Self {
|
||||
child: Mutex::new(None),
|
||||
adopted_processes: Mutex::new(Vec::new()),
|
||||
starting: AtomicBool::new(starting),
|
||||
#[cfg(windows)]
|
||||
job: Mutex::new(None),
|
||||
@@ -1456,21 +1466,29 @@ impl CoreProcessState {
|
||||
}
|
||||
|
||||
fn managed_pid(&self) -> Option<u32> {
|
||||
let Ok(mut child) = self.child.lock() else {
|
||||
return None;
|
||||
};
|
||||
if let Ok(mut child) = self.child.lock() {
|
||||
if let Some(process) = child.as_mut() {
|
||||
if let Ok(None) = process.try_wait() {
|
||||
return Some(process.id());
|
||||
}
|
||||
|
||||
let process = child.as_mut()?;
|
||||
|
||||
if let Ok(None) = process.try_wait() {
|
||||
return Some(process.id());
|
||||
*child = None;
|
||||
drop(child);
|
||||
self.clear_lifetime_guard();
|
||||
}
|
||||
}
|
||||
|
||||
*child = None;
|
||||
drop(child);
|
||||
self.clear_lifetime_guard();
|
||||
|
||||
None
|
||||
self.adopted_processes
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut processes| {
|
||||
processes.retain(|process| {
|
||||
process_executable_path(process.process_id)
|
||||
.as_deref()
|
||||
.is_some_and(|path| executable_paths_match(&process.binary_path, path))
|
||||
});
|
||||
processes.first().map(|process| process.process_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn clear_lifetime_guard(&self) {
|
||||
@@ -1486,12 +1504,57 @@ impl CoreProcessState {
|
||||
self.child.lock().ok().and_then(|mut child| child.take())
|
||||
}
|
||||
|
||||
fn adopt_process_ids(&self, binary_path: &Path, process_ids: Vec<u32>) -> Result<(), String> {
|
||||
let mut adopted = self
|
||||
.adopted_processes
|
||||
.lock()
|
||||
.map_err(|_| "接管的内核进程状态锁已损坏".to_string())?;
|
||||
*adopted = process_ids
|
||||
.into_iter()
|
||||
.filter(|process_id| is_process_alive(*process_id))
|
||||
.map(|process_id| AdoptedCoreProcess {
|
||||
process_id,
|
||||
binary_path: binary_path.to_path_buf(),
|
||||
})
|
||||
.collect();
|
||||
adopted.sort_unstable_by_key(|process| process.process_id);
|
||||
adopted.dedup_by_key(|process| process.process_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_adopted_processes(&self) -> Result<(), String> {
|
||||
self.adopted_processes
|
||||
.lock()
|
||||
.map(|mut processes| processes.clear())
|
||||
.map_err(|_| "接管的内核进程状态锁已损坏".to_string())
|
||||
}
|
||||
|
||||
fn take_adopted_processes(&self) -> Vec<AdoptedCoreProcess> {
|
||||
self.adopted_processes
|
||||
.lock()
|
||||
.map(|mut processes| std::mem::take(&mut *processes))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn store_child(&self, child: Child) -> Result<u32, String> {
|
||||
let pid = child.id();
|
||||
self.clear_adopted_processes()?;
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let job = attach_child_to_windows_job(&child)?;
|
||||
let job = match attach_child_to_windows_job(&child) {
|
||||
Ok(job) => job,
|
||||
Err(error) => {
|
||||
let mut child = child;
|
||||
let cleanup_error = terminate_child(&mut child).err();
|
||||
return Err(match cleanup_error {
|
||||
Some(cleanup_error) => {
|
||||
format!("{error};清理未托管的内核进程也失败: {cleanup_error}")
|
||||
}
|
||||
None => error,
|
||||
});
|
||||
}
|
||||
};
|
||||
let Ok(mut managed_child) = self.child.lock() else {
|
||||
close_windows_handle(job);
|
||||
return Err("内核进程状态锁已损坏".to_string());
|
||||
@@ -1506,10 +1569,19 @@ impl CoreProcessState {
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
let mut managed_child = self
|
||||
.child
|
||||
.lock()
|
||||
.map_err(|_| "内核进程状态锁已损坏".to_string())?;
|
||||
let mut managed_child = match self.child.lock() {
|
||||
Ok(managed_child) => managed_child,
|
||||
Err(_) => {
|
||||
let mut child = child;
|
||||
let cleanup_error = terminate_child(&mut child).err();
|
||||
return Err(match cleanup_error {
|
||||
Some(cleanup_error) => format!(
|
||||
"内核进程状态锁已损坏;清理未托管的内核进程也失败: {cleanup_error}"
|
||||
),
|
||||
None => "内核进程状态锁已损坏".to_string(),
|
||||
});
|
||||
}
|
||||
};
|
||||
*managed_child = Some(child);
|
||||
}
|
||||
|
||||
@@ -1852,6 +1924,14 @@ fn main() {
|
||||
}
|
||||
}
|
||||
|
||||
let _instance_guard = match acquire_app_instance_guard() {
|
||||
Ok(guard) => guard,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let portable_update_ack = portable_update_ack_argument();
|
||||
let gui_config = match load_or_create_gui_config() {
|
||||
Ok(config) => config,
|
||||
@@ -2020,7 +2100,24 @@ fn main() {
|
||||
Err(error) => eprintln!("自动安装 CPA 离线内核失败: {error}"),
|
||||
}
|
||||
|
||||
if should_start_core_on_launch(&config) {
|
||||
let adopted_process_ids = match adopt_existing_core_processes(process_state.inner())
|
||||
{
|
||||
Ok(process_ids) => process_ids,
|
||||
Err(error) => {
|
||||
eprintln!("扫描并接管当前目录的 CPA 内核失败: {error}");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
if !adopted_process_ids.is_empty() {
|
||||
eprintln!(
|
||||
"已接管当前目录中运行的 CPA 内核: PID {}",
|
||||
adopted_process_ids
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
} else if should_start_core_on_launch(&config) {
|
||||
if let Err(error) = start_core_process_inner(process_state.inner(), &config) {
|
||||
eprintln!("自动启动 CPA 内核失败: {error}");
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ mod app_settings;
|
||||
mod app_update;
|
||||
mod core_config;
|
||||
mod core_runtime;
|
||||
mod instance_lock;
|
||||
mod model_aliases;
|
||||
mod platform;
|
||||
mod provider_health;
|
||||
|
||||
@@ -9,6 +9,122 @@ fn core_process_starting_state_tracks_automatic_launch() {
|
||||
assert!(!state.is_starting());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_path_matching_keeps_core_instances_directory_scoped() {
|
||||
let root = agent_test_home("core-process-path-scope");
|
||||
let first_dir = root.join("first").join("cpa-core");
|
||||
let second_dir = root.join("second").join("cpa-core");
|
||||
fs::create_dir_all(&first_dir).unwrap();
|
||||
fs::create_dir_all(&second_dir).unwrap();
|
||||
let first_binary = first_dir.join(core_binary_name());
|
||||
let second_binary = second_dir.join(core_binary_name());
|
||||
fs::write(&first_binary, b"first").unwrap();
|
||||
fs::write(&second_binary, b"second").unwrap();
|
||||
|
||||
assert!(executable_paths_match(&first_binary, &first_binary));
|
||||
assert!(!executable_paths_match(&first_binary, &second_binary));
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_process_discovery_sleep_helper() {
|
||||
if env::var_os("EASYCLIPROXYAPI_PROCESS_DISCOVERY_TEST_HELPER").is_some() {
|
||||
thread::sleep(Duration::from_secs(10));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_core_process_discovery_ignores_the_same_binary_name_in_another_directory() {
|
||||
let root = agent_test_home("running-core-process-scope");
|
||||
let first_dir = root.join("first").join("cpa-core");
|
||||
let second_dir = root.join("second").join("cpa-core");
|
||||
fs::create_dir_all(&first_dir).unwrap();
|
||||
fs::create_dir_all(&second_dir).unwrap();
|
||||
let first_binary = first_dir.join(core_binary_name());
|
||||
let second_binary = second_dir.join(core_binary_name());
|
||||
|
||||
let source_binary = env::current_exe().unwrap();
|
||||
fs::copy(&source_binary, &first_binary).unwrap();
|
||||
fs::copy(&source_binary, &second_binary).unwrap();
|
||||
let arguments = [
|
||||
"--exact",
|
||||
"tests::core_runtime::core_process_discovery_sleep_helper",
|
||||
"--nocapture",
|
||||
];
|
||||
let mut first = Command::new(&first_binary)
|
||||
.args(&arguments)
|
||||
.env("EASYCLIPROXYAPI_PROCESS_DISCOVERY_TEST_HELPER", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let mut second = Command::new(&second_binary)
|
||||
.args(&arguments)
|
||||
.env("EASYCLIPROXYAPI_PROCESS_DISCOVERY_TEST_HELPER", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
|
||||
let first_running = first.try_wait().unwrap().is_none();
|
||||
let second_running = second.try_wait().unwrap().is_none();
|
||||
let candidate_process_ids = find_candidate_core_process_ids();
|
||||
let first_actual_path = process_executable_path(first.id());
|
||||
let second_actual_path = process_executable_path(second.id());
|
||||
let first_matches = find_core_process_ids(&first_binary);
|
||||
let second_matches = find_core_process_ids(&second_binary);
|
||||
|
||||
let _ = first.kill();
|
||||
let _ = second.kill();
|
||||
let _ = first.wait();
|
||||
let _ = second.wait();
|
||||
assert_eq!(
|
||||
first_matches,
|
||||
vec![first.id()],
|
||||
"running={first_running}, candidate={}, actual={first_actual_path:?}, expected={first_binary:?}",
|
||||
candidate_process_ids.contains(&first.id())
|
||||
);
|
||||
assert_eq!(
|
||||
second_matches,
|
||||
vec![second.id()],
|
||||
"running={second_running}, candidate={}, actual={second_actual_path:?}, expected={second_binary:?}",
|
||||
candidate_process_ids.contains(&second.id())
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_process_executable_path_can_be_resolved() {
|
||||
let expected = env::current_exe().unwrap();
|
||||
let actual = process_executable_path(std::process::id()).unwrap();
|
||||
assert!(executable_paths_match(&expected, &actual));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn core_process_state_tracks_and_releases_adopted_processes() {
|
||||
let state = CoreProcessState::new(false);
|
||||
let binary_path = env::current_exe().unwrap();
|
||||
state
|
||||
.adopt_process_ids(&binary_path, vec![std::process::id(), std::process::id()])
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(state.managed_pid(), Some(std::process::id()));
|
||||
assert_eq!(
|
||||
state
|
||||
.take_adopted_processes()
|
||||
.into_iter()
|
||||
.map(|process| process.process_id)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![std::process::id()]
|
||||
);
|
||||
assert_eq!(state.managed_pid(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_core_install_remains_successful_when_restart_succeeds() {
|
||||
let result = combine_install_and_restart_results(Ok("installed"), Ok(()));
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
use super::support::*;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn app_instance_keys_are_stable_and_directory_scoped() {
|
||||
let first = agent_test_home("instance-key-first");
|
||||
let second = agent_test_home("instance-key-second");
|
||||
|
||||
assert_eq!(app_instance_key(&first), app_instance_key(&first));
|
||||
assert_ne!(app_instance_key(&first), app_instance_key(&second));
|
||||
|
||||
fs::remove_dir_all(first).unwrap();
|
||||
fs::remove_dir_all(second).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn app_instance_guard_rejects_a_second_copy_and_releases_on_drop() {
|
||||
let root = agent_test_home("instance-lock");
|
||||
let first = acquire_app_instance_guard_for(&root).unwrap();
|
||||
|
||||
let duplicate = acquire_app_instance_guard_for(&root);
|
||||
assert!(duplicate.is_err());
|
||||
|
||||
drop(first);
|
||||
assert!(acquire_app_instance_guard_for(&root).is_ok());
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
Reference in new issue
Block a user