feat: 加固运行期可靠性并补齐调度与观测接口

- 运行句柄按 run_id 归属,修复取消后重跑时闸门被旧运行删除的竞态
- 调度循环改用取消令牌退出,启动时对账遗留运行,校验 rows_affected
- 三类任务统一参与调度,新增 /api/v1/stats 与 /api/v1/schedule
- SQLite 启用 WAL 与 busy_timeout,日志清理跳过活跃文件,单行上限 256 KiB
- 前端缓存管理令牌并在 403 时重试一次,仪表盘改用统计接口
This commit is contained in:
2026-10-26 00:38:16 +08:00
parent 773196d689
commit 815258c19b
19 changed files with 990 additions and 93 deletions
+55 -2
View File
@@ -79,7 +79,7 @@ pub fn prepare_script_environment(
.to_string_lossy()
.into_owned(),
);
environment.insert("PYTHONUNBUFFERED".to_owned(), "1".to_owned());
apply_stdio_defaults(&mut environment);
return Ok(PreparedPython {
interpreter: python,
virtual_env: Some(venv.clone()),
@@ -93,7 +93,7 @@ pub fn prepare_script_environment(
});
}
environment.insert("PYTHONUNBUFFERED".to_owned(), "1".to_owned());
apply_stdio_defaults(&mut environment);
Ok(PreparedPython {
interpreter: interpreter_path,
virtual_env: None,
@@ -102,6 +102,17 @@ pub fn prepare_script_environment(
})
}
/// Python 的标准流默认跟随系统代码页(简体中文为 GBK),一旦打印 emoji 或生僻字符
/// 就会 `UnicodeEncodeError` 直接失败。这里默认改用 UTF-8,日志侧本来就优先按 UTF-8 解码。
/// 用户在任务里显式设置的 `PYTHONIOENCODING` 仍然优先。
fn apply_stdio_defaults(environment: &mut BTreeMap<String, String>) {
// 实时日志依赖标准流不缓冲,这一项始终强制。
environment.insert("PYTHONUNBUFFERED".to_owned(), "1".to_owned());
environment
.entry("PYTHONIOENCODING".to_owned())
.or_insert_with(|| "utf-8".to_owned());
}
/// 虚拟环境的来源,用于在界面上说明"为什么"关联到了这个环境。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
@@ -630,6 +641,48 @@ mod tests {
assert!(prepared.remove_environment.contains(&"VIRTUAL_ENV_PROMPT"));
}
#[test]
fn injects_utf8_stdio_for_python_tasks() {
let temp = TempDir::new().unwrap();
let prepared = prepare_script_environment(
temp.path().to_str().unwrap(),
"python.exe",
None,
&BTreeMap::new(),
)
.unwrap();
assert_eq!(
prepared.environment.get("PYTHONIOENCODING"),
Some(&"utf-8".to_owned())
);
assert_eq!(
prepared.environment.get("PYTHONUNBUFFERED"),
Some(&"1".to_owned())
);
}
#[test]
fn keeps_user_pythonioencoding_override() {
let temp = TempDir::new().unwrap();
let mut user_env = BTreeMap::new();
user_env.insert("PYTHONIOENCODING".to_owned(), "gbk".to_owned());
let prepared = prepare_script_environment(
temp.path().to_str().unwrap(),
"python.exe",
None,
&user_env,
)
.unwrap();
assert_eq!(
prepared.environment.get("PYTHONIOENCODING"),
Some(&"gbk".to_owned())
);
}
fn probe(working_dir: &str, interpreter: &str, venv: Option<&str>) -> VenvProbeResponse {
probe_venv(&VenvProbeRequest {
working_dir: working_dir.to_owned(),