使用 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() 获取。
| 方法 | 合同 |
|---|---|
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。
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 清理整组进程,再释放桌面。
limitKill()、共享对象、指针转换、p.assignToJobObject(job)、p.killOnExit() 保持原用法;assignProcess(process/popen) 现在正确返回成功值。limitKill(limitFlags) 现在实际转传 flags。可选 0x800(BREAKAWAY_OK)或 0x1000(SILENT_BREAKAWAY_OK),默认均关闭;启用后主动允许相应后代脱离管理,不能再宣称管理完整进程树。startInfo.creationFlag 的 CREATE_BREAKAWAY_FROM_JOB 是创建时脱离控制器原有 Job 的原生选项,与新 Job 的后代限制不是一回事;是否允许由 Windows 决定。job.handle,应迁移到 job.close();避免把已被关闭的句柄留在对象内,尤其不要随后触发 GC 二次关闭。