构建命令的前后置同步 IO 挪到阻塞线程池

export/draft/xhs_minitool/build.rs:把找脚本、判权限、清旧产物抽成同步的 prepare,连同收尾判产物的 symlink_metadata 一起走 offload。
export/draft/xhs_minitool/commands.rs:offload 放宽到 pub(super),供同级 build 模块复用。
This commit is contained in:
2026-10-06 21:35:21 +08:00
parent 00b8d12d98
commit 22ad218dc7
2 changed files with 45 additions and 18 deletions
@@ -10,12 +10,13 @@ use std::path::Path;
use crate::project::run_project_verification_with_commit_at;
use super::commands::offload;
use super::dto::XHSMiniToolExportRunResult;
use super::error::{
ArtifactMissing, CommandDenied, CommandFailed, ExportUnavailable, XHSMiniToolExportBuildError,
};
use super::layout::{artifact_path, artifact_relative_path, SCRIPT_NAME};
use super::script;
use super::script::{self, NpmScript};
/// 权限点位复用既有命令口径:这就是在跑项目命令,不新增 `GAME_CREATION_APP_COMMANDS` 条目。
const PERMISSION_COMMAND_ID: &str = "command.exec";
@@ -27,6 +28,45 @@ const OUTPUT_TAIL_MAX_CHARS: usize = 4_000;
pub(crate) async fn run(
root: &Path,
) -> Result<XHSMiniToolExportRunResult, XHSMiniToolExportBuildError> {
// 找脚本、读权限策略、清上一次产物都是同步文件 IO:与读/存命令同一口径挪到阻塞线程池,
// 不占 async worker。
let root = root.to_path_buf();
let script = offload({
let root = root.clone();
move || prepare(&root)
})
.await?;
let result = run_project_verification_with_commit_at(
&root,
SCRIPT_NAME,
&script.command,
TIMEOUT_SECONDS,
script.cwd_relative,
// 导出不推进项目 revision,也不改任何正式状态:产物只是 `.export/` 下的一个文件。
|| Ok(()),
)
.await
.map_err(ExportUnavailable::new)?;
// 收尾判产物同样是同步 IO(symlink_metadata),一样搬去阻塞线程池。
offload(move || {
conclude(
&root,
result.exit_code,
result.timed_out,
&result.output,
)
})
.await
}
/// 构建前置(同步):找到脚本、确认权限点位没被拒、清掉上一次的产物。
///
/// 产物判据只看「文件非空」,所以先清掉上一次的 zip:脚本这次什么都没写时,残留的旧 zip 会
/// 让 no-op 脚本被判成功,用户上传的其实是上一轮的产物。删不掉(权限等)就 fail closed——
/// 留着旧产物继续判成功,比这次构建失败更糟。
fn prepare(root: &Path) -> Result<NpmScript, XHSMiniToolExportBuildError> {
let Some(script) = script::find(root) else {
return Err(script::missing_error());
};
@@ -42,9 +82,6 @@ pub(crate) async fn run(
}
})?;
// 产物判据只看「文件非空」,所以先清掉上一次的 zip:脚本这次什么都没写时,残留的旧 zip 会
// 让 no-op 脚本被判成功,用户上传的其实是上一轮的产物。删不掉(权限等)就 fail closed——
// 留着旧产物继续判成功,比这次构建失败更糟。
let artifact = artifact_path(root);
match std::fs::remove_file(&artifact) {
Ok(()) => {}
@@ -59,19 +96,7 @@ pub(crate) async fn run(
}
}
let result = run_project_verification_with_commit_at(
root,
SCRIPT_NAME,
&script.command,
TIMEOUT_SECONDS,
script.cwd_relative,
// 导出不推进项目 revision,也不改任何正式状态:产物只是 `.export/` 下的一个文件。
|| Ok(()),
)
.await
.map_err(ExportUnavailable::new)?;
conclude(root, result.exit_code, result.timed_out, &result.output)
Ok(script)
}
/// 把宿主的构建结果收成导出结果:先判成功,再确认产物真的在。
@@ -128,7 +128,9 @@ fn project_root(project_path: &str) -> Result<PathBuf, ExportUnavailable> {
/// 读命令每 2 秒被轮询一次,注册表 + `package.json` 探测虽然小,但直接在 `async fn` 里做同步
/// 文件 IO 会占着 worker 线程,拖慢同一 runtime 上的其它异步任务(构建子进程、SpacetimeDB
/// 客户端)。任务 panic 时 `JoinError` 收口成 `ExportUnavailable`,走各命令既有的失败表。
async fn offload<T, E, F>(job: F) -> Result<T, E>
///
/// 构建命令(`build::run`)的前后置同步 IO 也复用这里,所以对同级的 `build` 模块可见。
pub(super) async fn offload<T, E, F>(job: F) -> Result<T, E>
where
T: Send + 'static,
E: From<ExportUnavailable> + Send + 'static,