aardio 文档

process.job.limitKill — Job 生命周期管理

使用 Windows Job 管理一组进程及其按系统规则继承的后代。适用于外部编译器、脚本、命令行任务,以及需要防止根进程退出后遗留子进程的场景。

不依赖进程名称或父 PID 快照,不是安全沙箱,不会收管由已有服务或外部代理启动的进程。

共享对象与独立实例

import process.job.limitKill;

// 旧用法保留:绑定当前线程的共享 Job。
// p.assignToJobObject(process.job.limitKill);
// p.killOnExit();

// 单个任务应持有独立 Job,并主动清理。
var job,err,code = process.job.limitKill();
if(!job) return null,err,code;
return job.dispose(2000);

不要对共享的 process.job.limitKill 调用 close/dispose/terminate 来清理单个任务。 应使用带括号的 process.job.limitKill() 创建独立实例。

实例持有一个未命名、不可继承的 Job 句柄,默认启用 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE。GC 会调用 close,所以在任务存活期间必须保存 Job 对象引用;把临时 limitKill() 直接传给 assignToJobObject 而不保存引用,会使后续 GC 提前终止任务。

最小完整示例

下面运行一个退出代码为 7 的小命令,区分任务退出代码、等待超时与清理失败。

import process.job.limitKill;

var job,err,code = process.job.limitKill();
if(!job) return null,err,code;

var p,err,code,pending = job.start(
    string.expand("%ComSpec%"),
    "/d /c exit /b 7",
    {createNoWindow=true}
);
if(!p){
    job.close();
    return null,err,code,pending;
}

var ended,waitError,waitCode = job.wait(3000);
var exitCode = p.getExitCode();
var cleaned,cleanupError,cleanupCode = job.dispose(2000);
p.free();
if(!cleaned){
    job.close(); // 最后句柄关闭的兜底,不等于确认所有成员已退出。
    return null,cleanupError,cleanupCode;
}
if(!ended) return null,waitError,waitCode;
return {exitCode=exitCode;allExited=true};

wait 成功只表示 Job 的活动成员已归零,不表示程序业务成功或退出代码为 0。根进程的退出代码仍由 p.getExitCode() 获取。

生命周期 API

方法 合同
stats() 返回 {total;active;terminated} 的原生 accounting 快照。total 是累计加入数量;terminated 对应系统 TotalTerminatedProcesses,不可用来代替 active 判断退出。
list() 返回当前成员 PID 数组;空 Job 为 []。有界扩容,最多 65536 项;持续增长或超限返回错误 234。
isIn(p) 参数支持 PID、进程句柄、process、process.popen;成员为 true,非成员为 false;查询失败返回 null,不冒充非成员。
assignProcess(p) 将现有进程加入 Job,支持上述四种参数,成功 true。仅按 PID 打开的临时句柄由库释放;借用的句柄不释放。
start(exe,args,startInfo) 挂起创建 → 加入当前 Job → 恢复主线程,成功返回已运行的 process 对象。一个 Job 可以启动多个根进程。
terminate(exitCode=1) 请求终止整个 Job,不等待、不关闭句柄。
wait(timeout=2000) 有界等待 active=0,不终止进程、不关闭句柄。超时返回 null,错误信息,258。
close() 仅关闭本对象持有的 Job 句柄,重复调用安全。最后一个句柄关闭触发 KILL_ON_JOB_CLOSE,但不等待确认成员退出。
dispose(timeout=2000,exitCode=1) 终止 → 有界等待归零 → close。成功 true;失败保留句柄,可显式重试或 close 兜底。

其它失败一般返回 null,错误信息,Win32错误码,底层 process 创建失败可能只有错误信息。

timeout 为 0..2147483647 的整数毫秒,0 表示只检查一次。dispose 在同一预算内等待,不对每个 PID 分别重置期限。原生 API 自身执行时间仍遵循 Windows 语义。wait/dispose 同步阻塞且不处理窗口消息,GUI 请在工作线程使用。

PID 快照可能在返回后立即过期或发生 PID 复用,只用于显示和诊断;不要按 list() 的结果循环杀进程。终止应调用 Job API。

安全启动与失败回收

start 的参数与 process(exe,args,startInfo) 一致,但不接受数值 PID。它复制 startInfo,内部强制 suspended=true 和 waitInputTimeout=0,返回成功时已经恢复主线程。即使调用方指定 suspended=true,也不会返回一个仍挂起的进程。

绑定失败不会降级成无 Job 运行;恢复失败也不会放任挂起候选。库终止新候选并最多等待 2000ms,成功回收后释放其 process 对象,不影响同 Job 中原有的其它根进程。

极端情况下,回收尚未确认则返回:

null, 错误信息, 清理错误码, pendingProcess

第 4 返回值是尚须清理的 process 对象;调用方须继续终止/等待,最后 free(),或把这个所有权传给上层。错误信息保留启动失败阶段。不要忽略这个异常路径;单纯关闭进程句柄不会终止尚未加入 Job 的候选。

直接对已经运行的进程调用 assignProcess 不能补收它在加入之前已经创建的后代。需要从第一条用户代码起管理时使用 start,或自行挂起创建后使用旧绑定 API。

配合 process.desktop

import process.desktop;
import process.job.limitKill;

var desktop,err = process.desktop();
if(!desktop) return null,err;
var job,err = process.job.limitKill();
if(!job){ desktop.close(); return null,err; }

var p,err,code,pending = job.start(
    string.expand("%ComSpec%"),"/d /c exit /b 0",
    {desktop=desktop.path;createNoWindow=true}
);
if(!p){ job.close();desktop.close();return null,err,code,pending; }
var ended,waitError,waitCode = job.wait(3000);
var cleaned,cleanupError,cleanupCode = job.dispose(2000);
p.free();
if(!cleaned){
    // 此处把仍需处理的 Job/桌面交给调用方,不能冒称已经安全清理。
    return null,cleanupError,cleanupCode,{job=job;desktop=desktop};
}
var closed,closeError,closeCode = desktop.close();
if(!closed) return null,closeError,closeCode;
if(!ended) return null,waitError,waitCode;
return true;

桌面由 process.desktop 管理,Job 不拥有桌面。这里通过 startInfo.desktop 指定桌面,进程不是由 desktop.start 启动,因此不在 desktop.dispose 的直接进程跟踪列表中;必须先由 Job 清理整组进程,再释放桌面。

限制与兼容说明

Markdown 格式