Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/process_session/model.rs
T
AIGameCreator App 0aaa191f8d 并行拆分客户端运行时与项目摘要模块
将 runtime_tools 拆为十五个职责模块并保留 Agent 可见性

将 Runner 拆为协议端点分发客户端与所有权模块

将进程会话拆为模型持久化生命周期 IO 恢复与测试模块

将项目摘要拆为十四个无环模块并保留一百一十二个导出

记录并行拆分边界与稳定树验收规则
2026-07-22 14:45:36 +08:00

453 lines
16 KiB
Rust

use super::*;
pub(super) const PROCESS_SESSION_SCHEMA_VERSION: &str = "3";
pub(super) const PROCESS_SESSION_TRANSCRIPT_SCHEMA_VERSION: &str = "2";
pub(super) const PROCESS_SESSION_CURSOR_VERSION: &str = "v1";
pub(super) const PROCESS_SESSION_MAX_PER_PROJECT: usize = 4;
pub(super) const PROCESS_SESSION_MAX_PER_AGENT: usize = 2;
pub(super) const PROCESS_SESSION_MAX_OUTPUT_BYTES: usize = 256 * 1024;
pub(super) const PROCESS_SESSION_MAX_PENDING_LINE_BYTES: usize = 16 * 1024;
pub(super) const PROCESS_SESSION_MAX_STDIN_BYTES: usize = 8 * 1024;
pub(super) const PROCESS_SESSION_DEFAULT_POLL_CHARS: usize = 8_000;
pub(super) const PROCESS_SESSION_MAX_POLL_CHARS: usize = 16_000;
pub(super) const PROCESS_SESSION_MAX_POLL_WAIT_MS: u64 = 30_000;
pub(super) const PROCESS_SESSION_RECORD_MAX_BYTES: usize = 32 * 1024;
pub(super) const PROCESS_SESSION_TRANSCRIPT_MAX_BYTES: usize = 320 * 1024;
pub(super) const PROCESS_SESSION_TERMINATE_GRACE_MS: u64 = 800;
#[cfg(target_os = "linux")]
pub(super) const PROCESS_SESSION_OWNER_PID_ENV: &str = "GENARRATIVE_PROCESS_SESSION_OWNER_PID";
#[cfg(target_os = "linux")]
pub(super) const PROCESS_SESSION_CHILD_MODE: &str = "--process-session-child";
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ProcessSessionIdentity {
pub(crate) project_id: String,
pub(crate) agent_id: String,
pub(crate) task_id: String,
pub(crate) conversation_session_id: String,
pub(crate) run_id: String,
pub(crate) start_action_id: String,
pub(crate) start_action_fingerprint: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(crate) struct ProcessSessionRecord {
pub(crate) schema_version: String,
pub(crate) project_id: String,
pub(crate) agent_id: String,
pub(crate) task_id: String,
pub(crate) conversation_session_id: String,
pub(crate) run_id: String,
pub(crate) start_action_id: String,
pub(crate) start_action_fingerprint: String,
pub(crate) process_id: String,
pub(crate) owner_boot_id: String,
pub(crate) command_id: String,
pub(crate) program: String,
pub(crate) cwd: String,
#[serde(default)]
pub(crate) sandbox_backend: String,
#[serde(default)]
pub(crate) sandbox_mode: String,
#[serde(default)]
pub(crate) network_access: String,
#[serde(default)]
pub(crate) sandbox_profile_version: String,
#[serde(default)]
pub(crate) sandbox_establishment: String,
#[serde(default)]
pub(crate) target_exec: String,
#[serde(default)]
pub(crate) launch_failure_kind: Option<String>,
#[serde(default)]
pub(crate) sandbox_ready_at: Option<u64>,
#[serde(default)]
pub(crate) exec_established_at: Option<u64>,
pub(crate) status: String,
pub(crate) exit_code: Option<i32>,
pub(crate) signal: Option<String>,
pub(crate) stdin_open: bool,
pub(crate) output_bytes: usize,
pub(crate) output_sha256: String,
pub(crate) output_ref: Option<String>,
pub(crate) source_fingerprint_before: String,
pub(crate) source_fingerprint_after: Option<String>,
pub(crate) source_changed: Option<bool>,
pub(crate) needs_reconciliation: bool,
pub(crate) started_at: u64,
pub(crate) terminal_at: Option<u64>,
pub(crate) updated_at: u64,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub(super) struct ProcessSessionTranscript {
pub(super) schema_version: String,
pub(super) project_id: String,
pub(super) agent_id: String,
pub(super) task_id: String,
pub(super) conversation_session_id: String,
pub(super) run_id: String,
pub(super) start_action_id: String,
pub(super) start_action_fingerprint: String,
pub(super) process_id: String,
pub(super) output: String,
pub(super) output_sha256: String,
pub(super) output_bytes: usize,
pub(super) updated_at: u64,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ProcessSessionPollResult {
pub(crate) process_id: String,
pub(crate) status: String,
pub(crate) output: String,
pub(crate) cursor: String,
pub(crate) next_cursor: String,
pub(crate) has_more: bool,
pub(crate) stdin_open: bool,
pub(crate) exit_code: Option<i32>,
pub(crate) signal: Option<String>,
pub(crate) output_bytes: usize,
pub(crate) output_sha256: String,
pub(crate) source_changed: Option<bool>,
pub(crate) needs_reconciliation: bool,
pub(crate) sandbox_backend: String,
pub(crate) sandbox_mode: String,
pub(crate) network_access: String,
pub(crate) sandbox_profile_version: String,
pub(crate) sandbox_establishment: String,
pub(crate) target_exec: String,
pub(crate) launch_failure_kind: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ProcessSessionStdinResult {
pub(crate) process_id: String,
pub(crate) bytes_written: usize,
pub(crate) content_sha256: String,
pub(crate) stdin_open: bool,
pub(crate) eof: bool,
pub(crate) sandbox_backend: String,
pub(crate) sandbox_mode: String,
pub(crate) network_access: String,
pub(crate) sandbox_profile_version: String,
}
#[derive(Debug)]
pub(super) struct ProcessOutputState {
pub(super) text: String,
pub(super) status: String,
pub(super) exit_code: Option<i32>,
pub(super) signal: Option<String>,
pub(super) stdin_open: bool,
pub(super) reader_finished: bool,
pub(super) output_limit_exceeded: bool,
pub(super) source_fingerprint_after: Option<String>,
pub(super) source_changed: Option<bool>,
pub(super) needs_reconciliation: bool,
pub(super) launch_failure_kind: Option<String>,
}
impl ProcessOutputState {
pub(super) fn running() -> Self {
Self {
text: String::new(),
status: "running".to_string(),
exit_code: None,
signal: None,
stdin_open: true,
reader_finished: false,
output_limit_exceeded: false,
source_fingerprint_after: None,
source_changed: None,
needs_reconciliation: false,
launch_failure_kind: None,
}
}
}
#[derive(Debug)]
pub(super) enum ProcessControl {
Terminate,
OutputLimit,
Shutdown,
}
pub(super) struct LiveProcessSession {
pub(super) root: PathBuf,
pub(super) identity: ProcessSessionIdentity,
pub(super) process_id: String,
pub(super) command_id: String,
pub(super) program: String,
pub(super) cwd: String,
pub(super) sandbox_backend: String,
pub(super) sandbox_mode: String,
pub(super) network_access: String,
pub(super) sandbox_profile_version: String,
pub(super) sandbox_establishment: String,
pub(super) target_exec: String,
pub(super) sandbox_ready_at: Option<u64>,
pub(super) exec_established_at: Option<u64>,
pub(super) source_fingerprint_before: String,
pub(super) started_at: u64,
pub(super) output: Mutex<ProcessOutputState>,
pub(super) output_changed: Condvar,
pub(super) writer: Mutex<Option<Box<dyn std::io::Write + Send>>>,
pub(super) master: Mutex<Option<Box<dyn MasterPty + Send>>>,
#[cfg(windows)]
pub(super) job: Mutex<Option<WindowsProcessJob>>,
pub(super) control: std::sync::mpsc::Sender<ProcessControl>,
}
#[cfg(windows)]
pub(super) struct WindowsProcessJob(windows_sys::Win32::Foundation::HANDLE);
#[cfg(windows)]
unsafe impl Send for WindowsProcessJob {}
#[cfg(windows)]
unsafe impl Sync for WindowsProcessJob {}
#[cfg(windows)]
impl WindowsProcessJob {
pub(super) fn assign(child: &dyn Child) -> Result<Self, String> {
use std::mem::size_of;
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let process = child
.as_raw_handle()
.ok_or_else(|| "command.start Windows child 缺少 process handle".to_string())?
as windows_sys::Win32::Foundation::HANDLE;
let handle = unsafe { CreateJobObjectW(std::ptr::null(), std::ptr::null()) };
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return Err(format!(
"创建 command.start Windows Job Object 失败:{}",
std::io::Error::last_os_error()
));
}
let mut information = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
information.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
let configured = unsafe {
SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
&information as *const _ as *const _,
size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
let assigned = configured != 0 && unsafe { AssignProcessToJobObject(handle, process) } != 0;
if !assigned {
let error = std::io::Error::last_os_error();
unsafe {
CloseHandle(handle);
}
return Err(format!(
"配置 command.start Windows Job Object 失败:{error}"
));
}
Ok(Self(handle))
}
pub(super) fn terminate(&self) -> Result<(), String> {
use windows_sys::Win32::System::JobObjects::TerminateJobObject;
if unsafe { TerminateJobObject(self.0, 1) } == 0 {
return Err(format!(
"终止 command.start Windows Job Object 失败:{}",
std::io::Error::last_os_error()
));
}
Ok(())
}
}
#[cfg(windows)]
impl Drop for WindowsProcessJob {
fn drop(&mut self) {
unsafe {
windows_sys::Win32::Foundation::CloseHandle(self.0);
}
}
}
impl std::fmt::Debug for LiveProcessSession {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("LiveProcessSession")
.field("process_id", &self.process_id)
.field("agent_id", &self.identity.agent_id)
.field("run_id", &self.identity.run_id)
.finish_non_exhaustive()
}
}
#[derive(Default)]
pub(super) struct ProcessSessionRegistry {
pub(super) sessions: HashMap<String, Arc<LiveProcessSession>>,
}
#[cfg(target_os = "linux")]
#[derive(Clone, Debug)]
pub(super) struct PendingProcessLaunch {
pub(super) root: PathBuf,
pub(super) agent_id: String,
pub(super) process_group_leader: Option<i32>,
pub(super) shutdown_requested: bool,
}
#[cfg(target_os = "linux")]
#[derive(Default)]
pub(super) struct PendingProcessLaunchRegistry {
pub(super) launches: HashMap<String, PendingProcessLaunch>,
}
#[cfg(target_os = "linux")]
pub(super) struct PendingProcessLaunchGuard {
process_id: String,
}
#[cfg(target_os = "linux")]
impl Drop for PendingProcessLaunchGuard {
fn drop(&mut self) {
if let Ok(mut registry) = pending_process_launch_registry().lock() {
registry.launches.remove(&self.process_id);
}
}
}
static PROCESS_SESSION_REGISTRY: OnceLock<Mutex<ProcessSessionRegistry>> = OnceLock::new();
static PROCESS_SESSION_BOOT_ID: OnceLock<String> = OnceLock::new();
#[cfg(target_os = "linux")]
static PENDING_PROCESS_LAUNCH_REGISTRY: OnceLock<Mutex<PendingProcessLaunchRegistry>> =
OnceLock::new();
pub(super) fn process_session_registry() -> &'static Mutex<ProcessSessionRegistry> {
PROCESS_SESSION_REGISTRY.get_or_init(|| Mutex::new(ProcessSessionRegistry::default()))
}
#[cfg(target_os = "linux")]
pub(super) fn pending_process_launch_registry() -> &'static Mutex<PendingProcessLaunchRegistry> {
PENDING_PROCESS_LAUNCH_REGISTRY
.get_or_init(|| Mutex::new(PendingProcessLaunchRegistry::default()))
}
#[cfg(target_os = "linux")]
pub(super) fn reserve_pending_process_launch(
root: &Path,
agent_id: &str,
process_id: &str,
) -> Result<PendingProcessLaunchGuard, String> {
let mut registry = pending_process_launch_registry()
.lock()
.map_err(|_| "pending process launch registry 锁已损坏".to_string())?;
if registry.launches.contains_key(process_id) {
return Err("command.start pending launch 身份冲突".to_string());
}
registry.launches.insert(
process_id.to_string(),
PendingProcessLaunch {
root: root.to_path_buf(),
agent_id: agent_id.to_string(),
process_group_leader: None,
shutdown_requested: false,
},
);
Ok(PendingProcessLaunchGuard {
process_id: process_id.to_string(),
})
}
#[cfg(target_os = "linux")]
pub(super) fn activate_pending_process_launch(
process_id: &str,
process_group_leader: i32,
) -> Result<(), String> {
if process_group_leader <= 1 {
return Err("command.start wrapper 进程组身份无效".to_string());
}
let mut registry = pending_process_launch_registry()
.lock()
.map_err(|_| "pending process launch registry 锁已损坏".to_string())?;
let launch = registry
.launches
.get_mut(process_id)
.ok_or_else(|| "command.start pending launch reservation 缺失".to_string())?;
launch.process_group_leader = Some(process_group_leader);
if launch.shutdown_requested {
unsafe {
libc::kill(-process_group_leader, libc::SIGKILL);
}
return Err("Runner shutdown 已取消 pending process launch".to_string());
}
Ok(())
}
pub(crate) fn process_session_boot_id() -> &'static str {
PROCESS_SESSION_BOOT_ID
.get_or_init(|| {
let mut digest = Sha256::new();
digest.update(std::process::id().to_le_bytes());
digest.update(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos()
.to_le_bytes(),
);
digest.update(unix_timestamp().to_le_bytes());
let value = format!("{:x}", digest.finalize());
format!("boot-{}", &value[..32])
})
.as_str()
}
pub(crate) fn initialize_process_session_boot_id(boot_id: &str) -> Result<(), String> {
let boot_id = boot_id.trim();
if boot_id.is_empty()
|| boot_id.chars().count() > 160
|| boot_id.chars().any(|character| character.is_control())
{
return Err("Agent Runner bootId 无效,无法初始化 process session owner".to_string());
}
match PROCESS_SESSION_BOOT_ID.set(boot_id.to_string()) {
Ok(()) => Ok(()),
Err(_) if PROCESS_SESSION_BOOT_ID.get().map(String::as_str) == Some(boot_id) => Ok(()),
Err(_) => Err("process session owner bootId 已被其他 Runner 初始化".to_string()),
}
}
#[cfg(target_os = "linux")]
pub(crate) fn is_process_session_child_mode(args: &[String]) -> bool {
args.first().map(String::as_str) == Some(PROCESS_SESSION_CHILD_MODE)
}
#[cfg(target_os = "linux")]
pub(crate) fn run_process_session_child(args: &[String]) -> Result<i32, String> {
if args != [PROCESS_SESSION_CHILD_MODE] {
return Err("process session child 参数无效".to_string());
}
let expected_parent = std::env::var(PROCESS_SESSION_OWNER_PID_ENV)
.map_err(|_| "process session child 缺少 owner pid".to_string())?
.parse::<libc::pid_t>()
.map_err(|_| "process session child owner pid 无效".to_string())?;
if expected_parent <= 1 {
return Err("process session child owner pid 无效".to_string());
}
if unsafe { libc::getppid() } != expected_parent {
return Err("process session owner 在 child containment 生效前已退出".to_string());
}
unsafe {
libc::signal(libc::SIGHUP, libc::SIG_IGN);
}
std::env::remove_var(PROCESS_SESSION_OWNER_PID_ENV);
run_process_session_bridge_child(expected_parent)
}