use std::sync::Mutex;
use std::sync::mpsc;
static JOBS: Mutex<Vec<mpsc::Receiver<Result<String, String>>>> = Mutex::new(Vec::new());
static MESSAGE: Mutex<String> = Mutex::new(String::new());
pub fn active_session(paths: &actl_core::state::SignalPaths) -> Option<String> {
let records = paths.dir.join("records");
let entries = std::fs::read_dir(records).ok()?;
for entry in entries.flatten() {
let id = entry.file_name().into_string().ok()?;
let Ok(raw) = std::fs::read(entry.path().join("state.json")) else {
continue;
};
let Ok(state) = serde_json::from_slice::<serde_json::Value>(&raw) else {
continue;
};
if state["status"].as_str() != Some("recording") {
continue;
}
let pid = state["pid"].as_u64().unwrap_or_default() as u32;
if pid != 0 && alive(pid) {
return Some(id);
}
}
None
}
fn alive(pid: u32) -> bool {
use windows::Win32::Foundation::{CloseHandle, WAIT_OBJECT_0};
use windows::Win32::System::Threading::*;
unsafe {
match OpenProcess(PROCESS_SYNCHRONIZE, false, pid) {
Ok(handle) => {
let alive = WaitForSingleObject(handle, 0) != WAIT_OBJECT_0;
let _ = CloseHandle(handle);
alive
}
Err(error) => error.code() != windows::core::HRESULT::from_win32(87),
}
}
}
pub fn request(start: bool) -> Result<(), String> {
let (tx, receiver) = mpsc::channel();
std::thread::Builder::new()
.name("record-ui".into())
.spawn(move || {
let _ = tx.send(execute(start));
})
.map_err(|_| "INTERNAL:无法启动录制操作线程".to_string())?;
if let Ok(mut jobs) = JOBS.lock() {
jobs.push(receiver);
}
if let Ok(mut message) = MESSAGE.lock() {
*message = if start {
"正在开始录制…".into()
} else {
"正在停止录制并编译草稿…".into()
};
}
Ok(())
}
pub fn message() -> String {
if let Ok(mut jobs) = JOBS.lock() {
jobs.retain(|receiver| match receiver.try_recv() {
Ok(result) => {
if let Ok(mut message) = MESSAGE.lock() {
*message = result.unwrap_or_else(|error| error);
}
false
}
Err(mpsc::TryRecvError::Disconnected) => {
if let Ok(mut message) = MESSAGE.lock() {
*message = "INTERNAL:录制操作线程已退出".into();
}
false
}
Err(mpsc::TryRecvError::Empty) => true,
});
}
MESSAGE.lock().map(|s| s.clone()).unwrap_or_default()
}
fn execute(start: bool) -> Result<String, String> {
use std::os::windows::process::CommandExt;
let exe = std::env::current_exe()
.map_err(|_| "INTERNAL:无法定位程序".to_string())?
.with_file_name("actl.exe");
let output = std::process::Command::new(exe)
.args([if start { "record-start" } else { "record-stop" }])
.stdin(std::process::Stdio::null())
.creation_flags(0x08000000) .output()
.map_err(|_| "INTERNAL:无法启动录制命令".to_string())?;
let body: serde_json::Value = serde_json::from_slice(&output.stdout)
.map_err(|_| "INTERNAL:录制命令输出不可解析".to_string())?;
if body["ok"].as_bool() != Some(true) {
let code = body["error"]["code"].as_str().unwrap_or("INTERNAL");
let detail = body["error"]["message"].as_str().unwrap_or_default();
return Err(format!("{code}:{detail}"));
}
Ok(if start {
"录制已开始(上限 10 分钟,右键菜单可停止)".into()
} else {
let data = &body["data"];
format!(
"录制已完成:{} 事件 → {} 步 + {} TODO(草稿 {})",
data["events"].as_u64().unwrap_or_default(),
data["steps"].as_u64().unwrap_or_default(),
data["todos"].as_u64().unwrap_or_default(),
data["draft"].as_str().unwrap_or_default(),
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use actl_core::state::SignalPaths;
use serde_json::json;
fn sandbox() -> SignalPaths {
SignalPaths::at(std::env::temp_dir().join(actl_core::snapshot::new_snapshot_id()))
}
#[test]
fn active_session_requires_recording_status_and_live_pid() {
let paths = sandbox();
let dir = paths.dir.join("records").join("rec-x");
std::fs::create_dir_all(&dir).unwrap();
let live_pid = std::process::id();
std::fs::write(
dir.join("state.json"),
serde_json::to_vec(&json!({
"id": "rec-x", "status": "recording", "pid": live_pid,
}))
.unwrap(),
)
.unwrap();
assert_eq!(active_session(&paths).as_deref(), Some("rec-x"));
std::fs::write(
dir.join("state.json"),
serde_json::to_vec(&json!({
"id": "rec-x", "status": "completed", "pid": live_pid,
}))
.unwrap(),
)
.unwrap();
assert_eq!(active_session(&paths), None);
std::fs::write(
dir.join("state.json"),
serde_json::to_vec(&json!({
"id": "rec-x", "status": "recording", "pid": 999999,
}))
.unwrap(),
)
.unwrap();
assert_eq!(active_session(&paths), None);
}
#[test]
fn active_session_is_none_without_records_dir() {
let paths = sandbox();
assert_eq!(active_session(&paths), None);
}
}