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

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

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

## 共享对象与独立实例

```aardio
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 的小命令，区分任务退出代码、等待超时与清理失败。

```aardio
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 中原有的其它根进程。

极端情况下，回收尚未确认则返回：

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

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

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

## 配合 process.desktop

```aardio
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 二次关闭。
- 不要将当前控制器进程加入一个随后会 terminate/dispose 的 Job，否则会终止自己。
- 不要跨线程传递或复制 Job 对象、擅自复制句柄、并发修改主线程挂起计数。外部 DuplicateHandle 会推迟“最后句柄关闭”效果；close 成功不等于整个 Job 退出。
- Job 嵌套能力和安全限制依 Windows 版本而异；Windows 7 等旧系统可能拒绝已属另一 Job 的进程，库不会隐藏失败或提供按 PID 补杀降级。
- 本实现使用指针宽度处理原生布局；当前真实验收环境为 x86 aardio / Windows Server 2012 R2。未宣称已验证 x64 控制器、旧 Windows、外部 broker 或敌意进程隔离。
