# process.desktop：私有桌面运行进程

用于在不切换用户前台桌面的情况下，启动 GUI 程序、枚举窗口，再通过 HWND 进行后台自动化。纯 aardio 封装 Win32 API，不依赖 .NET。

这不是 Windows 任务视图的虚拟桌面，也不是安全沙箱。`dotNet.desktop` 管理任务视图；本库管理 `HDESK` 桌面对象。

## 快速上手

下面示例会在私有桌面启动指定程序，枚举其窗口，然后**强制终止本次启动的程序**。请替换成专用于自动化测试的 EXE，不要用来处理尚未保存的工作。

```aardio
import process.desktop;
import winex;

var desk,err = process.desktop();
if(!desk){ print(err); return; }

var p,err = desk.start("C:\path\your-app.exe");
if(!p){ desk.close(); print(err); return; }

// 类名、标题均可省略；传入 p 可限定进程，并在进程退出时提前结束等待。
var hwnd,tidOrErr = desk.wait(,,p,8000);
if(hwnd){
    for h,title,tid,pid in desk.each(,,p){
        print(h,title,tid,pid);
    }
    // 根据目标程序继续复用 winex：
    // var edit = winex.findEx(hwnd,,"Edit",,1011);
    // print(winex.getText(edit));
}
else print(tidOrErr);

// 业务完成或失败都应明确收尾；不需要强制终止时改用 desk.close()。
var cleaned,cleanupError = desk.dispose();
p.free();
if(!cleaned) print(cleanupError); // 失败时桌面仍可重试 dispose。
```

## 接口

| 接口 | 用途与返回值 |
|---|---|
| `process.desktop()` | 创建自动命名的私有桌面；失败返回 `null,错误信息,Win32错误码` |
| `desk.start(exe,args,startInfo)` | 在本桌面启动进程，返回现有 `process` 对象 |
| `desk.list()` | 顶层 HWND 快照数组；正常空结果是 `[]` |
| `desk.each(cls,title,processOrPid)` | 快照迭代器，每次返回 `hwnd,title,threadId,processId` |
| `desk.find(cls,title,processOrPid)` | 首个匹配窗口：`hwnd,threadId,processId`；未找到返回 `null` |
| `desk.wait(cls,title,processOrPid,timeout=8000)` | 有限时等待；成功返回值同 `find` |
| `desk.close()` | 关闭本库持有的桌面句柄和内部跟踪句柄，**不终止进程** |
| `desk.dispose(timeout=2000)` | 强制终止 `start` 直接启动的进程，总限时等待后调用 `close` |

`desk.name` 是自动生成的名称，`desk.path` 是 `窗口站名\桌面名`，`desk.handle` 是 HDESK 指针。这三个字段只读使用，不要修改或自行释放句柄。

### 启动

- 在**当前进程的窗口站**创建桌面，不硬编码 `WinSta0`。
- `args`、`startInfo` 沿用 `process` 的规则，可指定参数数组、工作目录、环境变量等。
- 强制覆盖 `startInfo.desktop`；不会修改调用方传入的参数表。
- `waitInputTimeout` 默认 `0`，避免启动阶段无限等待 GUI 就绪；`showWindow` 默认 `1`，但窗口只显示在私有桌面。显式传入的 `0` 等值会保留。
- 不修改安全描述符、不创建其他窗口站、不切换桌面、不迁移调用线程。

### 枚举、查找与等待

- `cls` 和 `title` 使用 aardio 模式匹配；省略不筛选，`title=""` 仅匹配空标题。
- 第三个参数可以是 `process` 对象或数值 PID。传入**对象**时，`wait` 能在进程退出或对象释放时提前结束；单独的数值 PID 只用于筛选。
- 包含隐藏窗口和有所有者的顶层窗口，因此也可能包含输入法等辅助窗口。子控件请通过 `winex.findEx`、`winex.enum` 查找。
- 每次 `each` 都枚举一次桌面、创建快照；不会持续跟踪后续新窗口。窗口可能随时销毁，不应长期缓存 HWND。
- 顶层标题通过 `GetWindowTextW` 读取外部窗口的系统缓存标题，不发送 `WM_GETTEXT`，可避免无响应进程拖住标题筛选。自绘内容不等于系统标题；控件正文仍用 `winex` 等方法读取。
- `wait` 默认等待 8000 毫秒，约每 50 毫秒查询一次；0 或负数只检查一次。超时返回 `null,"等待窗口超时",258`。
- `wait` 同步阻塞调用线程。GUI 程序应在工作线程中创建并使用桌面对象；不要把这个对象直接传到其他线程。
- `wait` 只等待窗口，不监控整个自动化任务的最长执行时间，也不会在超时时自动杀进程。

`list/each/find/wait` 出错时返回 `null,错误信息[,Win32错误码]`。直接用于 `for ... in` 时，迭代器创建失败会表现为没有迭代；需要区分错误时先取得迭代器：

```aardio
var iterator,err = desk.each("目标窗口类名",,p);
if(!iterator){ print(err); return; }
for hwnd,title,tid,pid in iterator {
    print(hwnd,title);
}
```

### 清理与所有权

- `close` 与 `dispose` 都可重复调用，成功返回 `true`，失败返回 `null,错误信息[,错误码]`。
- **先 `close` 就放弃了本库对此前进程的清理权**；之后再 `dispose` 不会补杀进程。要终止进程，应直接调用 `dispose`。
- `dispose` 使用独立持有的原生进程句柄，不根据 EXE 名称或重新打开 PID 来杀进程。即使调用方先 `p.free()`，清理仍然有效。
- `dispose` **只清理本对象 `start` 直接创建的进程**，不追踪孙进程，也不清理通过 `desk.path` 从外部启动的进程。需要进程树约束时应另行采用 Job Object 等机制。
- 返回给调用方的 `process` 对象独立存在，`dispose` 不会调用它的 `free`；仍可查询退出状态，然后自行 `p.free()`。
- 清理超时或失败时保留内部句柄，允许重试 `dispose`。若不再重试而直接 `close`，剩余进程不会被终止。
- 垃圾回收仅调用 `close`，**不会自动强杀进程**。请显式收尾；业务代码可能抛错时，可用一次 `call` 包住业务，将清理放在其后。
- 关闭 HDESK 不代表系统立刻销毁桌面：其他进程、线程或打开的句柄仍可能持有它。不要用 `raw.closeHandle` 代替 `CloseDesktop`。

## 边界

- 不提供桌面切换、屏幕截图、真实鼠标键盘注入、窗口站管理或任务视图 COM 接口。
- 适合可通过窗口消息、控件接口等完成的后台自动化。不承诺所有 GUI 框架、浏览器或 UI 自动化技术都能跨桌面工作。
- 无文件、注册表、网络或权限隔离；不是安全沙箱。
- 单实例程序可能转交给已有实例，程序也可能另起进程或显式使用其他桌面；这类程序需要单独适配。
- `EnumDesktopWindows` 按整个桌面枚举。已知目标 TID 时 `EnumThreadWindows` 也可能找到对应窗口，但只覆盖一个线程，不能代替桌面枚举。
