Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/process_session/io.rs
T
kdletters 15c227cebf 修复AGC Windows运行与全量测试稳定性
完善Codex CLI、App Server、ConPTY与进程会话在Windows下的发现、启动、恢复和退出行为
修复Agent Runtime、Provider重试、项目写锁及工具交接账本的并发与跨测试串线问题
补齐配置目录、路径脱敏、原子写入、浏览器探测和本地Provider smoke的跨平台兼容
增强Goal Contract、自动策略、资源生成及运行态恢复的契约和回归测试
更新AI游戏创作智能体App技术文档中的Windows稳定性说明
验证AGC开发态、Release打包、打包后GUI运行及完整agc:check门禁
2026-08-13 13:36:21 +08:00

371 lines
14 KiB
Rust

use super::*;
pub(super) fn live_process_session(
process_id: &str,
) -> Result<Option<Arc<LiveProcessSession>>, String> {
validate_process_id(process_id)?;
Ok(process_session_registry()
.lock()
.map_err(|_| "process session registry 锁已损坏".to_string())?
.sessions
.get(process_id)
.cloned())
}
pub(super) fn poll_result_from_output(
process_id: &str,
output: &str,
state: &ProcessOutputState,
sandbox_backend: &str,
sandbox_mode: &str,
network_access: &str,
sandbox_profile_version: &str,
sandbox_establishment: &str,
target_exec: &str,
launch_failure_kind: Option<&str>,
cursor: Option<&str>,
max_chars: usize,
) -> Result<ProcessSessionPollResult, String> {
let offset = parse_process_session_cursor(process_id, cursor, output)?;
let end = output[offset..]
.char_indices()
.nth(max_chars)
.map(|(index, _)| offset + index)
.unwrap_or(output.len());
let next_cursor = process_session_cursor(process_id, end);
Ok(ProcessSessionPollResult {
process_id: process_id.to_string(),
status: state.status.clone(),
output: output[offset..end].to_string(),
cursor: process_session_cursor(process_id, offset),
next_cursor,
has_more: end < output.len(),
stdin_open: state.stdin_open,
exit_code: state.exit_code,
signal: state.signal.clone(),
output_bytes: output.len(),
output_sha256: format!("{:x}", Sha256::digest(output.as_bytes())),
source_changed: state.source_changed,
needs_reconciliation: state.needs_reconciliation,
sandbox_backend: sandbox_backend.to_string(),
sandbox_mode: sandbox_mode.to_string(),
network_access: network_access.to_string(),
sandbox_profile_version: sandbox_profile_version.to_string(),
sandbox_establishment: sandbox_establishment.to_string(),
target_exec: target_exec.to_string(),
launch_failure_kind: launch_failure_kind.map(str::to_string),
})
}
pub(crate) fn poll_process_session_at(
root: &Path,
identity: &ProcessSessionIdentity,
process_id: &str,
cursor: Option<&str>,
max_chars: Option<usize>,
wait_ms: Option<u64>,
) -> Result<ProcessSessionPollResult, String> {
validate_process_session_identity(identity)?;
let max_chars = max_chars
.unwrap_or(PROCESS_SESSION_DEFAULT_POLL_CHARS)
.min(PROCESS_SESSION_MAX_POLL_CHARS);
let wait_ms = wait_ms.unwrap_or(0).min(PROCESS_SESSION_MAX_POLL_WAIT_MS);
if let Some(live) = live_process_session(process_id)? {
if live.root != root || live.identity != *identity {
return Err("process session 不属于当前 Agent run".to_string());
}
let mut output = live
.output
.lock()
.map_err(|_| "process session output 锁已损坏".to_string())?;
let initial_offset = parse_process_session_cursor(process_id, cursor, &output.text)?;
if wait_ms > 0 && initial_offset == output.text.len() && output.status == "running" {
let waited = live
.output_changed
.wait_timeout(output, Duration::from_millis(wait_ms))
.map_err(|_| "process session output 锁已损坏".to_string())?;
output = waited.0;
}
return poll_result_from_output(
process_id,
&output.text,
&output,
&live.sandbox_backend,
&live.sandbox_mode,
&live.network_access,
&live.sandbox_profile_version,
&live.sandbox_establishment,
&live.target_exec,
output.launch_failure_kind.as_deref(),
cursor,
max_chars,
);
}
let mut record = read_process_session_record(root, process_id)?
.ok_or_else(|| "process session 不存在".to_string())?;
validate_process_session_access(&record, identity)?;
if matches!(
record.status.as_str(),
"prepared" | "launching" | "running" | "terminating"
) && record.owner_boot_id != process_session_boot_id()
{
reconcile_stale_active_process_session(&mut record);
write_process_session_record(root, &record)?;
}
let transcript = if let Some(output_ref) = record.output_ref.as_deref() {
read_agent_runtime_json_sidecar_with_max_bytes::<ProcessSessionTranscript>(
root,
output_ref,
"Agent Runtime process transcript",
PROCESS_SESSION_TRANSCRIPT_MAX_BYTES,
)?
} else {
None
};
if let Some(transcript) = &transcript {
if let Err(error) = validate_process_session_transcript(transcript, &record) {
record.status = "needs-reconciliation".to_string();
record.stdin_open = false;
record.needs_reconciliation = true;
record.terminal_at = Some(unix_timestamp());
record.updated_at = unix_timestamp();
let _ = write_process_session_record(root, &record);
return Err(error);
}
}
let output = transcript
.as_ref()
.map(|value| value.output.as_str())
.unwrap_or_default();
let state = ProcessOutputState {
text: output.to_string(),
status: record.status,
exit_code: record.exit_code,
signal: record.signal,
stdin_open: record.stdin_open,
reader_finished: true,
output_limit_exceeded: false,
source_fingerprint_after: record.source_fingerprint_after,
source_changed: record.source_changed,
needs_reconciliation: record.needs_reconciliation,
launch_failure_kind: record.launch_failure_kind.clone(),
};
poll_result_from_output(
process_id,
output,
&state,
&record.sandbox_backend,
&record.sandbox_mode,
&record.network_access,
&record.sandbox_profile_version,
&record.sandbox_establishment,
&record.target_exec,
record.launch_failure_kind.as_deref(),
cursor,
max_chars,
)
}
pub(crate) fn write_process_session_stdin_at(
root: &Path,
identity: &ProcessSessionIdentity,
process_id: &str,
data: &str,
append_newline: bool,
eof: bool,
) -> Result<ProcessSessionStdinResult, String> {
write_process_session_stdin_at_with_after_write(
root,
identity,
process_id,
data,
append_newline,
eof,
|_| {},
)
}
pub(super) fn write_process_session_stdin_at_with_after_write<F>(
root: &Path,
identity: &ProcessSessionIdentity,
process_id: &str,
data: &str,
append_newline: bool,
eof: bool,
after_write: F,
) -> Result<ProcessSessionStdinResult, String>
where
F: FnOnce(&LiveProcessSession),
{
validate_process_session_identity(identity)?;
let live = live_process_session(process_id)?
.ok_or_else(|| "process session 不在当前 Runner 中运行".to_string())?;
if live.root != root || live.identity != *identity {
return Err("process session 不属于当前 Agent run".to_string());
}
let mut bytes = data.as_bytes().to_vec();
if append_newline {
#[cfg(windows)]
bytes.extend_from_slice(b"\r\n");
#[cfg(not(windows))]
bytes.push(b'\n');
}
if bytes.len() > PROCESS_SESSION_MAX_STDIN_BYTES {
return Err(format!(
"command.stdin 单次最多写入 {PROCESS_SESSION_MAX_STDIN_BYTES} 字节"
));
}
if bytes.iter().any(|byte| *byte == 0) {
return Err("command.stdin 不接受 NUL 或二进制正文".to_string());
}
let content_sha256 = format!("{:x}", Sha256::digest(&bytes));
let mut writer = live
.writer
.lock()
.map_err(|_| "process session stdin 锁已损坏".to_string())?;
if live
.output
.lock()
.map_err(|_| "process session output 锁已损坏".to_string())?
.status
!= "running"
{
return Err("process session 已进入终态".to_string());
}
if !bytes.is_empty() {
let stream = writer
.as_mut()
.ok_or_else(|| "process session stdin 已关闭".to_string())?;
stream
.write_all(&bytes)
.and_then(|()| stream.flush())
.map_err(|error| format!("写入 process session stdin 失败:{error}"))?;
}
if eof {
writer.take();
}
drop(writer);
after_write(&live);
let mut output = live
.output
.lock()
.map_err(|_| "process session output 锁已损坏".to_string())?;
if eof || output.status != "running" {
output.stdin_open = false;
}
let record = process_session_record_from_live(&live, &output);
if let Err(error) = write_process_session_record(root, &record) {
output.status = "needs-reconciliation".to_string();
output.needs_reconciliation = true;
output.stdin_open = false;
let reconciliation = process_session_record_from_live(&live, &output);
let _ = write_process_session_record(root, &reconciliation);
let _ = live.control.send(ProcessControl::Terminate);
return Err(format!(
"command.stdin 已写入但状态无法落盘,需要人工核对:{error}"
));
}
Ok(ProcessSessionStdinResult {
process_id: process_id.to_string(),
bytes_written: data.len() + usize::from(append_newline),
content_sha256,
stdin_open: output.stdin_open,
eof,
sandbox_backend: live.sandbox_backend.clone(),
sandbox_mode: live.sandbox_mode.clone(),
network_access: live.network_access.clone(),
sandbox_profile_version: live.sandbox_profile_version.clone(),
})
}
pub(crate) fn terminate_process_session_at(
root: &Path,
identity: &ProcessSessionIdentity,
process_id: &str,
cursor: Option<&str>,
) -> Result<ProcessSessionPollResult, String> {
validate_process_session_identity(identity)?;
if let Some(live) = live_process_session(process_id)? {
if live.root != root || live.identity != *identity {
return Err("process session 不属于当前 Agent run".to_string());
}
let running = live
.output
.lock()
.map_err(|_| "process session output 锁已损坏".to_string())?
.status
== "running";
if running {
live.control
.send(ProcessControl::Terminate)
.map_err(|_| "process session 监督线程已结束".to_string())?;
let mut output = live
.output
.lock()
.map_err(|_| "process session output 锁已损坏".to_string())?;
let deadline = std::time::Instant::now()
+ Duration::from_millis(PROCESS_SESSION_TERMINATE_GRACE_MS + 1_500);
while output.status == "running" && std::time::Instant::now() < deadline {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
let waited = live
.output_changed
.wait_timeout(output, remaining.min(Duration::from_millis(100)))
.map_err(|_| "process session output 锁已损坏".to_string())?;
output = waited.0;
}
}
let mut result =
poll_process_session_at(root, identity, process_id, cursor, Some(1), Some(0))?;
let cursor_offset = result
.cursor
.rsplit_once(':')
.and_then(|(_, offset)| offset.parse::<usize>().ok())
.ok_or_else(|| "command.terminate 返回了无效 cursor".to_string())?;
result.output.clear();
result.next_cursor = result.cursor.clone();
result.has_more = cursor_offset < result.output_bytes;
return Ok(result);
}
let mut result = poll_process_session_at(root, identity, process_id, cursor, Some(1), Some(0))?;
let cursor_offset = result
.cursor
.rsplit_once(':')
.and_then(|(_, offset)| offset.parse::<usize>().ok())
.ok_or_else(|| "command.terminate 返回了无效 cursor".to_string())?;
result.output.clear();
result.next_cursor = result.cursor.clone();
result.has_more = cursor_offset < result.output_bytes;
Ok(result)
}
pub(crate) fn mark_process_session_start_audit_failure_at(
root: &Path,
process_id: &str,
error: &str,
) -> Result<(), String> {
if let Some(live) = live_process_session(process_id)? {
if live.root != root {
return Err("process session 不属于当前项目".to_string());
}
mark_process_session_reconciliation(
&live,
&format!("command.start audit persistence failed: {error}"),
);
if let Ok(mut output) = live.output.lock() {
output.launch_failure_kind = Some("start-audit-failed".to_string());
}
let _ = live.control.send(ProcessControl::Terminate);
}
let mut record = read_process_session_record(root, process_id)?
.ok_or_else(|| "command.start audit 失败后 process record 缺失".to_string())?;
record.status = "needs-reconciliation".to_string();
record.stdin_open = false;
record.needs_reconciliation = true;
record.launch_failure_kind = Some("start-audit-failed".to_string());
record.signal = Some(redact_agent_runtime_project_paths(root, error, 240));
record.terminal_at = Some(unix_timestamp());
record.updated_at = unix_timestamp();
write_process_session_record(root, &record)
}