quickwin 1.3.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -2
- package/README.md +19 -23
- package/examples/PdfCanvas.js +8 -37
- package/examples/PdfCanvas.tsx +11 -41
- package/examples/exec_server.js +20054 -1423
- package/examples/exec_server.tsx +489 -0
- package/examples/exec_server_worker.js +383 -33
- package/examples/exec_server_worker.ts +408 -30
- package/examples/pdf_preview2.js +25 -82
- package/examples/pdf_preview2.ts +27 -84
- package/examples/pdf_viewer.js +1318 -1178
- package/examples/pdf_viewer.tsx +16 -67
- package/examples/setres.js +22 -47
- package/examples/setres.ts +23 -57
- package/examples/test_gallery.js +1313 -1074
- package/examples/test_react_lazy.js +1284 -1056
- package/examples/test_react_listbox.js +1283 -1055
- package/examples/test_root_window.js +1279 -1051
- package/examples/test_tab.js +1284 -1056
- package/lib/abort.js +70 -0
- package/lib/abort.ts +69 -0
- package/lib/certs.js +214 -0
- package/lib/certs.ts +224 -0
- package/lib/exec-log.js +15 -0
- package/lib/exec-log.ts +30 -0
- package/lib/fetch.js +37 -4
- package/lib/fetch.ts +35 -4
- package/lib/ffi/bind.js +352 -0
- package/lib/ffi/bind.ts +442 -0
- package/lib/ffi/ctype.js +168 -0
- package/lib/ffi/ctype.ts +331 -0
- package/lib/ffi/index.js +6 -0
- package/lib/ffi/index.ts +6 -0
- package/lib/ffi/struct.js +412 -0
- package/lib/ffi/struct.ts +664 -0
- package/lib/http-common.js +43 -5
- package/lib/http-common.ts +48 -6
- package/lib/http-server.js +187 -29
- package/lib/http-server.ts +177 -26
- package/lib/react-qw/components/DateTimePicker.js +18 -37
- package/lib/react-qw/components/Input.js +3 -10
- package/lib/react-qw/components/Link.js +6 -19
- package/lib/react-qw/components/ListView.js +49 -154
- package/lib/react-qw/components/PathPicker.js +28 -88
- package/lib/react-qw/components/ScrollView.js +15 -8
- package/lib/react-qw/components/Slider.js +2 -1
- package/lib/react-qw/components/Tab.js +6 -22
- package/lib/react-qw/components/Tooltip.js +10 -21
- package/lib/react-qw/components/TreeView.js +8 -31
- package/lib/react-qw/index.js +4 -2
- package/lib/react-qw/index.ts +4 -2
- package/lib/react-qw/nmhdr.js +3 -24
- package/lib/react-qw/nmhdr.ts +5 -30
- package/lib/react-qw/props.js +7 -2
- package/lib/react-qw/props.ts +7 -3
- package/lib/react-qw/reconciler.js +23 -13
- package/lib/react-qw/reconciler.ts +22 -13
- package/lib/text-measure.js +18 -23
- package/lib/text-measure.ts +20 -25
- package/lib/websocket.js +12 -2
- package/lib/websocket.ts +15 -2
- package/lib/windows/comctl32.js +18 -0
- package/lib/windows/comctl32.ts +20 -0
- package/lib/windows/comdlg32.js +7 -0
- package/lib/windows/comdlg32.ts +9 -0
- package/lib/windows/gdi32.js +90 -0
- package/lib/windows/gdi32.ts +97 -0
- package/lib/windows/kernel32.js +66 -0
- package/lib/windows/kernel32.ts +76 -0
- package/lib/windows/ntdll.js +8 -0
- package/lib/windows/ntdll.ts +10 -0
- package/lib/windows/ole32.js +7 -0
- package/lib/windows/ole32.ts +9 -0
- package/lib/windows/shell32.js +11 -0
- package/lib/windows/shell32.ts +14 -0
- package/lib/windows/structs.js +384 -0
- package/lib/windows/structs.ts +422 -0
- package/lib/windows/user32.js +469 -0
- package/lib/windows/user32.ts +502 -0
- package/package.json +1 -1
- package/quickwin.d.ts +22 -37
- package/quickwin_const.d.ts +1 -0
- package/qwin-nowasm-x86.exe +0 -0
- package/qwin-nowasm.exe +0 -0
- package/qwin-x86.exe +0 -0
- package/qwin.exe +0 -0
- package/test/bench_ffi_callraw.js +236 -0
- package/test/bench_ffi_callraw.ts +236 -0
- package/test/bench_ffi_overhead.js +139 -0
- package/test/bench_ffi_overhead.ts +141 -0
- package/test/bench_sendmessage.js +104 -0
- package/test/bench_sendmessage.ts +100 -0
- package/test/bench_sendmessage_nonnull.js +187 -0
- package/test/bench_sendmessage_nonnull.ts +188 -0
- package/test/run.js +12 -4
- package/test/run.ts +13 -5
- package/test/test_basic_fetch.js +8 -0
- package/test/test_basic_fetch.ts +9 -0
- package/test/test_ffi.js +25 -43
- package/test/test_ffi.ts +33 -52
- package/test/test_ffi_abi.js +235 -0
- package/test/test_ffi_abi.ts +218 -0
- package/test/test_ffi_bind.js +538 -8
- package/test/test_ffi_bind.ts +559 -8
- package/test/test_ffi_struct.js +632 -88
- package/test/test_ffi_struct.ts +666 -91
- package/test/test_ffi_struct_layout.js +495 -0
- package/test/test_ffi_struct_layout.ts +543 -0
- package/test/test_ffi_user32.js +103 -0
- package/test/test_ffi_user32.ts +113 -0
- package/test/test_ffi_windows.js +101 -0
- package/test/test_ffi_windows.ts +113 -0
- package/test/test_helper.js +8 -1
- package/test/test_helper.ts +9 -1
- package/test/test_http_import.js +18 -0
- package/test/test_http_import.ts +19 -0
- package/test/test_http_server.js +73 -1
- package/test/test_http_server.ts +75 -1
- package/test/test_ipv6.js +2 -2
- package/test/test_ipv6.ts +2 -2
- package/test/test_net_fetch.js +70 -8
- package/test/test_net_fetch.ts +69 -7
- package/test/test_net_websocket.js +1 -1
- package/test/test_net_websocket.ts +1 -1
- package/test/test_react_complex.js +1294 -1066
- package/test/test_react_counter.js +1284 -1056
- package/test/test_react_flex.js +1304 -1069
- package/test/test_react_hidden.js +1287 -1059
- package/test/test_react_listview_link.js +1284 -1056
- package/test/test_react_notify.js +18534 -0
- package/test/test_react_render.js +1284 -1056
- package/test/test_wolfssl_read.js +3 -2
- package/test/test_wolfssl_read.ts +3 -1
- package/test/test_worker_net.js +35 -17
- package/test/test_worker_net.ts +27 -15
- package/tsconfig.json +1 -1
- package/examples/exec_server.ts +0 -80
- package/lib/ffi-bind.js +0 -85
- package/lib/ffi-bind.ts +0 -170
- package/lib/ffi-struct.js +0 -224
- package/lib/ffi-struct.ts +0 -239
- package/lib/win-common-structs.js +0 -39
- package/lib/win-common-structs.ts +0 -41
|
@@ -1,45 +1,395 @@
|
|
|
1
1
|
import * as os from 'os';
|
|
2
2
|
import * as std from 'std';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
import { closure, WCHAR } from '../lib/ffi/bind.js';
|
|
4
|
+
import { NULL, PtrArrayBuffer } from '../lib/ffi/ctype.js';
|
|
5
|
+
import { struct } from '../lib/ffi/struct.js';
|
|
6
|
+
import { CloseHandle, CreatePipe, CreateProcess, GetExitCodeProcess, GetLastError, GetSystemDirectory, ReadFile, SetHandleInformation, WaitForSingleObject, } from '../lib/windows/kernel32.js';
|
|
7
|
+
import { ClientToScreen, EnumWindows, GetClassName, GetClientRect, GetDC, GetSystemMetrics, GetWindowRect, GetWindowText, IsIconic, IsWindowVisible, PrintWindow, ReleaseDC, } from '../lib/windows/user32.js';
|
|
8
|
+
import { BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDIBits, SelectObject, } from '../lib/windows/gdi32.js';
|
|
9
|
+
import { RtlGetVersion } from '../lib/windows/ntdll.js';
|
|
10
|
+
import { BITMAPFILEHEADER, BITMAPINFOHEADER, POINT, PROCESS_INFORMATION, RECT, STARTUPINFOW, OSVERSIONINFO_148, } from '../lib/windows/structs.js';
|
|
11
|
+
// worker 没有可观测的输出通道:绑定失败会静默中断 worker 脚本,主线程永远等不到消息。
|
|
12
|
+
// 失败原因落盘 diag.log 以便定位(典型:DLL 位数与进程不匹配 -> ERROR_BAD_EXE_FORMAT 193)。
|
|
13
|
+
const diagFile = (stage) => {
|
|
14
|
+
try {
|
|
15
|
+
const f = std.open('diag.log', 'a');
|
|
16
|
+
if (f) {
|
|
17
|
+
f.puts(`${Date.now()} ${stage}\n`);
|
|
18
|
+
f.close();
|
|
19
|
+
}
|
|
11
20
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
catch (ex) { /* best effort */ }
|
|
22
|
+
};
|
|
23
|
+
const HANDLE_FLAG_INHERIT = 0x1;
|
|
24
|
+
const STARTF_USESTDHANDLES = 0x100;
|
|
25
|
+
const WAIT_INFINITE = 0xffffffff;
|
|
26
|
+
// STARTUPINFOW / PROCESS_INFORMATION 的布局随进程位宽(x86 68B / x64 104B),由 struct 按 os.arch 推。
|
|
27
|
+
// os.arch 是编译期的进程指针宽度(quickjs-libc.c OS_ARCH),WOW64 下的 32 位进程得 ia32 布局——
|
|
28
|
+
// 本地 win7 VM 上的 exec_server 就是 32 位进程跑在 64 位系统上。
|
|
29
|
+
const parent = os.Worker.parent;
|
|
30
|
+
// GetSystemDirectoryW 拼 cmd.exe 全路径。
|
|
31
|
+
// 探针实测(XP/Win7 均如此):CreateProcessW 的 lpCommandLine 第一个 token 若是
|
|
32
|
+
// 裸 "cmd.exe",PATH 解析在本环境失败(err 267 ERROR_DIRECTORY / 123 ERROR_INVALID_NAME),
|
|
33
|
+
// 用全路径即正常。cmd 内部再解析 ping/dir 等用自己的 PATH,无此问题。
|
|
34
|
+
let sysDir = 'C:\\Windows\\System32';
|
|
35
|
+
try {
|
|
36
|
+
const out = WCHAR.alloc(256);
|
|
37
|
+
const n = GetSystemDirectory(out, 256);
|
|
38
|
+
const s = n > 0 ? WCHAR.decode(out) : '';
|
|
39
|
+
if (s)
|
|
40
|
+
sysDir = s;
|
|
41
|
+
}
|
|
42
|
+
catch (ex) {
|
|
43
|
+
diagFile('SYS-DIR-FAIL: ' + String(ex));
|
|
44
|
+
}
|
|
45
|
+
// win11(NT 10.x)上用 `/u` 让 cmd 内置输出直接写 UTF-16LE,绕开 ACP(Tiny11 缺
|
|
46
|
+
// c_*.nls 且 ACP=1252,中文会退化成 `?`)。win7/XP 的 ACP=936 本来就无损,保持原样。
|
|
47
|
+
// 版本缓冲用 OSVERSIONINFO_148(148B 特制布局,标准 OSVERSIONINFOW 会越界写坏 worker 堆,
|
|
48
|
+
// 定义与缘由见 lib/windows/structs.ts)。RtlGetVersion 绑定随 ntdll 模块加载(与
|
|
49
|
+
// kernel32/user32/gdi32 同款 fail-loud);下方 try/catch 只兜运行期失败落 VER-FAIL 诊断。
|
|
50
|
+
let IS_WIN11 = false;
|
|
51
|
+
try {
|
|
52
|
+
const vi = OSVERSIONINFO_148.encode({ dwOSVersionInfoSize: OSVERSIONINFO_148.size });
|
|
53
|
+
if (RtlGetVersion(vi) === 0) {
|
|
54
|
+
IS_WIN11 = OSVERSIONINFO_148.decode(vi).dwMajorVersion >= 10;
|
|
20
55
|
}
|
|
56
|
+
}
|
|
57
|
+
catch (ex) {
|
|
58
|
+
diagFile('VER-FAIL: ' + String(ex));
|
|
59
|
+
}
|
|
60
|
+
// 用 CreateProcessW + CreatePipe 直接捕获子进程 stdout(替代 winpty)。
|
|
61
|
+
// 纯捕获场景(/exec 从不交互),管道方案比 winpty 少一层 agent 中转:
|
|
62
|
+
// - 流式粒度更好:探针实测 XP 上逐行实时到达(winpty 攒批 ~4s)
|
|
63
|
+
// - 无 winpty.dll / winpty-agent.exe 依赖,纯 kernel32
|
|
64
|
+
// 编码:无统一代码页转换,各程序输出原生字节(qwin= UTF-8、系统命令= GBK)。
|
|
65
|
+
// 同步阻塞读循环(worker 线程自转),主线程事件循环不受影响。
|
|
66
|
+
// CreatePipe 的两个 HANDLE 出参槽:一个结构两个指针字段,读回走 decode
|
|
67
|
+
// CreatePipe 成功后两端句柄必非零(失败已在下方 throw)——字段标 '!',decode 直出 Ptr
|
|
68
|
+
const PIPE_HANDLES = /* @__PURE__ */ struct('PIPE_HANDLES', { hRead: '<HFILE>ptr!', hWrite: '<HFILE>ptr!' });
|
|
69
|
+
function runCmd(id, cmd) {
|
|
70
|
+
// —— 管道 + 继承设置(读端不可继承,否则 EOF 永不触发)——
|
|
71
|
+
const pipe = PIPE_HANDLES.alloc();
|
|
72
|
+
if (!CreatePipe(pipe.ptr, pipe.ptr + PIPE_HANDLES.offsetOf('hWrite'), NULL, 0)) {
|
|
73
|
+
throw new Error('CreatePipe err=' + GetLastError());
|
|
74
|
+
}
|
|
75
|
+
const { hRead, hWrite } = PIPE_HANDLES.decode(pipe);
|
|
76
|
+
SetHandleInformation(hWrite, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
|
77
|
+
SetHandleInformation(hRead, HANDLE_FLAG_INHERIT, 0);
|
|
78
|
+
// —— STARTUPINFO(写端作为子进程 stdout/stderr)——
|
|
79
|
+
const pi = PROCESS_INFORMATION.alloc();
|
|
80
|
+
const u = IS_WIN11 ? '/u ' : '';
|
|
81
|
+
const cmdline = `${sysDir}\\cmd.exe ${u}/c ${cmd}`;
|
|
82
|
+
if (!CreateProcess(NULL, cmdline, NULL, NULL, 1, 0, NULL, NULL, { cb: STARTUPINFOW.size, dwFlags: STARTF_USESTDHANDLES, hStdOutput: hWrite, hStdError: hWrite }, pi.ptr)) {
|
|
83
|
+
CloseHandle(hRead);
|
|
84
|
+
CloseHandle(hWrite);
|
|
85
|
+
throw new Error('CreateProcessW err=' + GetLastError() + ' cmd=' + cmdline);
|
|
86
|
+
}
|
|
87
|
+
const { hProcess, hThread, dwProcessId } = PROCESS_INFORMATION.decode(pi);
|
|
88
|
+
CloseHandle(hThread);
|
|
89
|
+
CloseHandle(hWrite);
|
|
90
|
+
parent.postMessage({ type: 'info', id, pid: dwProcessId });
|
|
91
|
+
// —— 同步阻塞读循环(子进程树全退出、写端全关 -> EOF -> 收尾)——
|
|
92
|
+
const bufB = new PtrArrayBuffer(4096);
|
|
93
|
+
const buf = new Uint8Array(bufB);
|
|
94
|
+
const nReadB = new PtrArrayBuffer(4);
|
|
95
|
+
const nRead = new Uint32Array(nReadB);
|
|
96
|
+
for (;;) {
|
|
97
|
+
nRead[0] = 0;
|
|
98
|
+
if (!ReadFile(hRead, bufB, 4096, nReadB, NULL))
|
|
99
|
+
break;
|
|
100
|
+
const n = nRead[0];
|
|
101
|
+
if (n === 0)
|
|
102
|
+
break;
|
|
103
|
+
parent.postMessage({ type: 'data', id, chunk: buf.slice(0, n) });
|
|
104
|
+
}
|
|
105
|
+
WaitForSingleObject(hProcess, WAIT_INFINITE);
|
|
106
|
+
const ecB = new PtrArrayBuffer(4);
|
|
107
|
+
const ec = new Int32Array(ecB);
|
|
108
|
+
GetExitCodeProcess(hProcess, ecB);
|
|
109
|
+
const code = ec[0];
|
|
110
|
+
CloseHandle(hRead);
|
|
111
|
+
CloseHandle(hProcess);
|
|
112
|
+
return code;
|
|
113
|
+
}
|
|
114
|
+
// —— 截屏:全屏 BitBlt → 32bpp 顶向下 → BMP 包装(无压缩)——
|
|
115
|
+
// 不 import 'gui':那个模块的初始化依赖主线程 GUI 循环,exec_server 没有窗口;
|
|
116
|
+
// 这里跟 lib/text-measure.ts 一样直接用 bind 绑 user32/gdi32。
|
|
117
|
+
// 行宽:32bpp 下每行 w*4 字节,天然 4 字节对齐,与 BMP 的行填充规则一致,
|
|
118
|
+
// 所以 GetDIBits 拿到的像素可以原样当 BMP 数据体。
|
|
119
|
+
const SRCCOPY = 0x00cc0020;
|
|
120
|
+
const DIB_RGB_COLORS = 0;
|
|
121
|
+
const SCREENSHOT_CHUNK = 65536;
|
|
122
|
+
// 单次捕获像素上限(≈160MB 像素缓冲),挡住畸形 rect 造成的巨量分配
|
|
123
|
+
const MAX_CAPTURE_PX = 40000000;
|
|
124
|
+
const WIDE_BUF_CHARS = 512;
|
|
125
|
+
const MAX_WINDOWS = 20000;
|
|
126
|
+
// BMP 文件 = 文件头(14B, pack 2) + 信息头(40B) + 像素
|
|
127
|
+
const BMP_HEADERS_SIZE = BITMAPFILEHEADER.size + BITMAPINFOHEADER.size;
|
|
128
|
+
function bmpHeader(size, w, h) {
|
|
129
|
+
const fileHdr = BITMAPFILEHEADER.encode({
|
|
130
|
+
bfType: 0x4d42, // 'BM'
|
|
131
|
+
bfSize: BMP_HEADERS_SIZE + size, // 文件总长 = 头 + 像素
|
|
132
|
+
bfOffBits: BMP_HEADERS_SIZE, // 像素数据偏移
|
|
133
|
+
});
|
|
134
|
+
const infoHdr = BITMAPINFOHEADER.encode({
|
|
135
|
+
biSize: BITMAPINFOHEADER.size,
|
|
136
|
+
biWidth: w,
|
|
137
|
+
biHeight: -h, // 负 = 顶向下
|
|
138
|
+
biPlanes: 1,
|
|
139
|
+
biBitCount: 32,
|
|
140
|
+
biCompression: 0, // BI_RGB
|
|
141
|
+
biSizeImage: size,
|
|
142
|
+
biXPelsPerMeter: 2835, // 72dpi
|
|
143
|
+
biYPelsPerMeter: 2835,
|
|
144
|
+
});
|
|
145
|
+
const out = new Uint8Array(BMP_HEADERS_SIZE);
|
|
146
|
+
out.set(new Uint8Array(fileHdr), 0);
|
|
147
|
+
out.set(new Uint8Array(infoHdr), BITMAPFILEHEADER.size);
|
|
21
148
|
return out;
|
|
22
149
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
150
|
+
// 取 hdc 当前选中位图的全部像素,按 BMP 分块发出。返回 GetDIBits 是否全量成功。
|
|
151
|
+
function emitDib(id, hdc, hbm, w, h) {
|
|
152
|
+
const pxB = new PtrArrayBuffer(w * h * 4);
|
|
153
|
+
const px = new Uint8Array(pxB);
|
|
154
|
+
const bmi = BITMAPINFOHEADER.encode({
|
|
155
|
+
biSize: BITMAPINFOHEADER.size,
|
|
156
|
+
biWidth: w,
|
|
157
|
+
biHeight: -h, // 负 = 顶向下
|
|
158
|
+
biPlanes: 1,
|
|
159
|
+
biBitCount: 32, // biCompression 缺省 0 = BI_RGB
|
|
160
|
+
biSizeImage: w * h * 4,
|
|
161
|
+
});
|
|
162
|
+
const got = GetDIBits(hdc, hbm, 0, h, pxB, bmi.ptr, DIB_RGB_COLORS);
|
|
163
|
+
if (got !== h)
|
|
164
|
+
return false;
|
|
165
|
+
const size = px.byteLength;
|
|
166
|
+
parent.postMessage({ type: 'data', id, chunk: bmpHeader(size, w, h) });
|
|
167
|
+
for (let i = 0; i < size; i += SCREENSHOT_CHUNK) {
|
|
168
|
+
parent.postMessage({ type: 'data', id, chunk: px.slice(i, Math.min(i + SCREENSHOT_CHUNK, size)) });
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
// PrintWindow 到整窗临时 DC,再用 BitBlt 裁出子矩形到 hdcOut。临时 DC 用完立即归还。
|
|
173
|
+
// 裁切在 GDI 里做(第二块 DC + BitBlt),不在 JS 里逐像素拷:既省一份整帧缓冲,
|
|
174
|
+
// 也不会把客户区偏移和 BitBlt 的源坐标重复应用。
|
|
175
|
+
function printAndCrop(hdcScreen, hwnd, frameW, frameH, hdcOut, cropX, cropY, outW, outH) {
|
|
176
|
+
const hdcTmp = CreateCompatibleDC(hdcScreen);
|
|
177
|
+
const hbmTmp = hdcTmp ? CreateCompatibleBitmap(hdcScreen, frameW, frameH) : 0;
|
|
178
|
+
if (!hdcTmp || !hbmTmp) {
|
|
179
|
+
if (hdcTmp)
|
|
180
|
+
DeleteDC(hdcTmp);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
const oldTmp = SelectObject(hdcTmp, hbmTmp);
|
|
184
|
+
// PrintWindow 从 (0,0) 填整张位图,所以必须给整窗尺寸;nFlags 传 0(XP/Win7 无 PW_* 语义)
|
|
185
|
+
const printed = PrintWindow(hwnd, hdcTmp, 0) !== 0;
|
|
186
|
+
const cropped = printed && BitBlt(hdcOut, 0, 0, outW, outH, hdcTmp, cropX, cropY, SRCCOPY) !== 0;
|
|
187
|
+
SelectObject(hdcTmp, oldTmp);
|
|
188
|
+
DeleteObject(hbmTmp);
|
|
189
|
+
DeleteDC(hdcTmp);
|
|
190
|
+
return cropped;
|
|
191
|
+
}
|
|
192
|
+
// 抓一张 [outW x outH] 的位图。只做两件事:填内存 DC → GetDIBits 取出 → 发 data 帧。
|
|
193
|
+
// hwnd 为 null:srcX/srcY 是屏幕坐标,BitBlt 直接截出结果,没有裁切这一步;
|
|
194
|
+
// 非 null:PrintWindow 让窗口自行重绘(可截被遮挡、最小化的窗口),再裁出窗口相对偏移
|
|
195
|
+
// (cropX,cropY) 处的子矩形。两条路径的坐标语义不同,所以 crop 只对 print 有意义。
|
|
196
|
+
// 只返回尺寸,像素走 parent.postMessage 分块发出(复用 /exec 的 data 帧)。
|
|
197
|
+
// 注意:BitBlt 自 GetDC(NULL) 截的是「调用线程所在 session 的输入桌面」,
|
|
198
|
+
// 在 sshd / service(session 0)里跑会得到全黑图且**不报错**——调用方要校验像素内容。
|
|
199
|
+
function captureRect(id, frameW, frameH, srcX, srcY, cropX, cropY, outW, outH, hwnd) {
|
|
200
|
+
// 用否定式判断:NaN 会让 `> 0` 与 `> 上限` 同时为假,正写会漏过
|
|
201
|
+
if (!(outW > 0 && outH > 0 && outW * outH <= MAX_CAPTURE_PX)) {
|
|
202
|
+
throw new Error('capture size invalid w=' + outW + ' h=' + outH);
|
|
203
|
+
}
|
|
204
|
+
if (hwnd !== null
|
|
205
|
+
&& !(cropX >= 0 && cropY >= 0 && cropX + outW <= frameW && cropY + outH <= frameH)) {
|
|
206
|
+
throw new Error('capture crop out of range crop=(' + cropX + ',' + cropY + ',' + outW + 'x' + outH
|
|
207
|
+
+ ') frame=' + frameW + 'x' + frameH);
|
|
208
|
+
}
|
|
209
|
+
const hdcScreen = GetDC(NULL);
|
|
210
|
+
if (!hdcScreen) {
|
|
211
|
+
throw new Error('GetDC(NULL)=0:无输入桌面(确认在交互 session 启动,而非 sshd/service)');
|
|
212
|
+
}
|
|
213
|
+
// 必须传屏幕 DC 建位图:CreateCompatibleBitmap 按「当前选中对象」决定位图格式,
|
|
214
|
+
// 传 hdcOut 时它选中的是 1×1 单色图 → 建出 1×1 位图,BitBlt 被裁剪到 1 像素
|
|
215
|
+
// (症状:blit=1、getdibits=h 都成功,但缓冲区几乎全 0)。
|
|
216
|
+
const hdcOut = CreateCompatibleDC(hdcScreen);
|
|
217
|
+
const hbmOut = hdcOut ? CreateCompatibleBitmap(hdcScreen, outW, outH) : 0;
|
|
218
|
+
if (!hdcOut || !hbmOut) {
|
|
219
|
+
if (hdcOut)
|
|
220
|
+
DeleteDC(hdcOut);
|
|
221
|
+
ReleaseDC(NULL, hdcScreen);
|
|
222
|
+
throw new Error('CreateCompatibleDC/Bitmap failed');
|
|
223
|
+
}
|
|
224
|
+
const oldOut = SelectObject(hdcOut, hbmOut);
|
|
225
|
+
const ok = hwnd === null
|
|
226
|
+
? BitBlt(hdcOut, 0, 0, outW, outH, hdcScreen, srcX, srcY, SRCCOPY) !== 0
|
|
227
|
+
: printAndCrop(hdcScreen, hwnd, frameW, frameH, hdcOut, cropX, cropY, outW, outH);
|
|
228
|
+
SelectObject(hdcOut, oldOut);
|
|
229
|
+
const complete = ok && emitDib(id, hdcOut, hbmOut, outW, outH);
|
|
230
|
+
DeleteObject(hbmOut);
|
|
231
|
+
DeleteDC(hdcOut);
|
|
232
|
+
ReleaseDC(NULL, hdcScreen);
|
|
233
|
+
if (!ok) {
|
|
234
|
+
throw new Error('capture failed out=' + outW + 'x' + outH
|
|
235
|
+
+ (hwnd === null ? ' at=(' + srcX + ',' + srcY + ')'
|
|
236
|
+
: ' hwnd=' + hwnd + ' crop=(' + cropX + ',' + cropY + ') frame=' + frameW + 'x' + frameH));
|
|
237
|
+
}
|
|
238
|
+
if (!complete)
|
|
239
|
+
throw new Error('GetDIBits short read out=' + outW + 'x' + outH);
|
|
240
|
+
return { width: outW, height: outH };
|
|
241
|
+
}
|
|
242
|
+
// 抓主屏全屏
|
|
243
|
+
function captureScreen(id) {
|
|
244
|
+
const w = GetSystemMetrics(0);
|
|
245
|
+
const h = GetSystemMetrics(1);
|
|
246
|
+
if (w <= 0 || h <= 0)
|
|
247
|
+
throw new Error('GetSystemMetrics w=' + w + ' h=' + h);
|
|
248
|
+
return captureRect(id, w, h, 0, 0, 0, 0, w, h, null);
|
|
249
|
+
}
|
|
250
|
+
// 抓单个窗口。area=client 只抓客户区;mode=print 让窗口自行重绘(窗口可被遮挡、可最小化)。
|
|
251
|
+
function captureWindow(id, hwnd, area, mode) {
|
|
252
|
+
if (mode === 'screen' && IsIconic(hwnd) !== 0) {
|
|
253
|
+
throw new Error('window minimized:mode=screen 只会截到它下方的桌面,改用 mode=print');
|
|
254
|
+
}
|
|
255
|
+
const wRect = RECT.alloc();
|
|
256
|
+
if (!GetWindowRect(hwnd, wRect.ptr)) {
|
|
257
|
+
throw new Error('GetWindowRect(hwnd=' + hwnd + ')=0:窗口不存在或已销毁');
|
|
258
|
+
}
|
|
259
|
+
const { left: wl, top: wt, right: wr, bottom: wbot } = RECT.decode(wRect);
|
|
260
|
+
const ww = wr - wl;
|
|
261
|
+
const wh = wbot - wt;
|
|
262
|
+
let cropX = 0;
|
|
263
|
+
let cropY = 0;
|
|
264
|
+
let cw = ww;
|
|
265
|
+
let ch = wh;
|
|
266
|
+
if (area === 'client') {
|
|
267
|
+
const cRect = RECT.alloc();
|
|
268
|
+
if (!GetClientRect(hwnd, cRect.ptr))
|
|
269
|
+
throw new Error('GetClientRect(hwnd=' + hwnd + ')=0');
|
|
270
|
+
// 客户区坐标以客户区左上角为原点,left/top 恒为 0,不是窗口→客户的偏移;
|
|
271
|
+
// 边框和标题栏的位移必须用 ClientToScreen(0,0) 单独查。尺寸取差值(同理不假设 left/top)。
|
|
272
|
+
const { left: cl, top: ct, right: cr, bottom: cbot } = RECT.decode(cRect);
|
|
273
|
+
cw = cr - cl;
|
|
274
|
+
ch = cbot - ct;
|
|
275
|
+
const origin = POINT.encode({ x: 0, y: 0 });
|
|
276
|
+
if (!ClientToScreen(hwnd, origin.ptr))
|
|
277
|
+
throw new Error('ClientToScreen(hwnd=' + hwnd + ')=0');
|
|
278
|
+
const org = POINT.decode(origin);
|
|
279
|
+
cropX = org.x - wl;
|
|
280
|
+
cropY = org.y - wt;
|
|
281
|
+
}
|
|
282
|
+
// screen:srcX/srcY 给绝对屏幕坐标,截出的就是输出本身,crop 无意义(传 0 即可,反正被忽略);
|
|
283
|
+
// print:PrintWindow 只能从 (0,0) 画整窗,客户区靠 crop 从整窗位图里裁出来。
|
|
284
|
+
// 两条路径只走一种坐标,所以客户区偏移不会被应用两次。
|
|
285
|
+
let sx = mode === 'print' ? 0 : wl + cropX;
|
|
286
|
+
let sy = mode === 'print' ? 0 : wt + cropY;
|
|
287
|
+
if (mode === 'screen') {
|
|
288
|
+
// clamp 捕获区域到虚拟屏幕内:窗口超出屏幕时 BitBlt 从屏幕外只能截到黑区,
|
|
289
|
+
// 直接把发出的 BMP 裁到窗口在屏幕内的可见部分(dims 也随之收缩)。
|
|
290
|
+
const vsX = GetSystemMetrics(76); // SM_XVIRTUALSCREEN
|
|
291
|
+
const vsY = GetSystemMetrics(77); // SM_YVIRTUALSCREEN
|
|
292
|
+
const vsW = GetSystemMetrics(78); // SM_CXVIRTUALSCREEN
|
|
293
|
+
const vsH = GetSystemMetrics(79); // SM_CYVIRTUALSCREEN
|
|
294
|
+
const ax = Math.max(sx, vsX);
|
|
295
|
+
const ay = Math.max(sy, vsY);
|
|
296
|
+
const ax2 = Math.min(sx + cw, vsX + vsW);
|
|
297
|
+
const ay2 = Math.min(sy + ch, vsY + vsH);
|
|
298
|
+
if (ax2 <= ax || ay2 <= ay) {
|
|
299
|
+
throw new Error('capture area outside virtual screen at=(' + sx + ',' + sy + ',' + cw
|
|
300
|
+
+ 'x' + ch + ')');
|
|
301
|
+
}
|
|
302
|
+
sx = ax;
|
|
303
|
+
sy = ay;
|
|
304
|
+
cw = ax2 - ax;
|
|
305
|
+
ch = ay2 - ay;
|
|
306
|
+
}
|
|
307
|
+
return captureRect(id, ww, wh, sx, sy, cropX, cropY, cw, ch, mode === 'print' ? hwnd : null);
|
|
308
|
+
}
|
|
309
|
+
// 顶层窗口列表(不递归子控件)。EnumWindows 先拍窗口快照再回调:不像 FindWindowEx 链式遍历
|
|
310
|
+
// 那样,在「下一个窗口恰好被销毁」时拿到失效句柄,下一轮返回 NULL 把列表悄悄截断。
|
|
311
|
+
// 回调只收集句柄:回调内抛异常会被 closure 吞成返回 0(枚举静默提前结束),所以不在回调里查属性。
|
|
312
|
+
// 快照里的窗口在逐个查询时可能已销毁,GetWindowRect 返回 0 即跳过。
|
|
313
|
+
// 输出顺序是 EnumWindows 的顺序(与 FindWindowEx 链的顺序不同,集合相同;API 未承诺顺序)。
|
|
314
|
+
function listWindows(id) {
|
|
315
|
+
const handles = [];
|
|
316
|
+
const collect = closure('<HWND>ptr <>ptr -> i32', (h) => {
|
|
317
|
+
if (h)
|
|
318
|
+
handles.push(h);
|
|
319
|
+
return handles.length < MAX_WINDOWS ? 1 : 0; // 返回 0 = 停止枚举(到上限)
|
|
320
|
+
});
|
|
31
321
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
322
|
+
EnumWindows(collect.ptr, NULL);
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
collect.dispose();
|
|
326
|
+
}
|
|
327
|
+
const wRect = RECT.alloc();
|
|
328
|
+
const tb = WCHAR.alloc(WIDE_BUF_CHARS);
|
|
329
|
+
const cb = WCHAR.alloc(WIDE_BUF_CHARS);
|
|
330
|
+
const out = [];
|
|
331
|
+
for (const hwnd of handles) {
|
|
332
|
+
if (GetWindowRect(hwnd, wRect.ptr)) {
|
|
333
|
+
const rc = RECT.decode(wRect);
|
|
334
|
+
const tl = GetWindowText(hwnd, tb, WIDE_BUF_CHARS);
|
|
335
|
+
const cl = GetClassName(hwnd, cb, WIDE_BUF_CHARS);
|
|
336
|
+
out.push({
|
|
337
|
+
hwnd,
|
|
338
|
+
// 缓冲跨窗口复用:长度 0 时 API 不保证写回终止符,不能 decode 残留内容
|
|
339
|
+
title: tl > 0 ? WCHAR.decode(tb) : '',
|
|
340
|
+
className: cl > 0 ? WCHAR.decode(cb) : '',
|
|
341
|
+
rect: { left: rc.left, top: rc.top, right: rc.right, bottom: rc.bottom },
|
|
342
|
+
visible: IsWindowVisible(hwnd) !== 0,
|
|
343
|
+
minimized: IsIconic(hwnd) !== 0
|
|
344
|
+
});
|
|
35
345
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
346
|
+
}
|
|
347
|
+
parent.postMessage({ type: 'result', id, code: 0, error: null, windows: out });
|
|
348
|
+
}
|
|
349
|
+
function handleRequest(msg) {
|
|
350
|
+
if (msg.type === 'run') {
|
|
351
|
+
if (typeof msg.cmd !== 'string')
|
|
352
|
+
return;
|
|
353
|
+
let code = -1;
|
|
354
|
+
let error = null;
|
|
355
|
+
try {
|
|
356
|
+
code = runCmd(msg.id, msg.cmd);
|
|
357
|
+
}
|
|
358
|
+
catch (ex) {
|
|
359
|
+
error = String(ex);
|
|
360
|
+
}
|
|
361
|
+
parent.postMessage({ type: 'result', id: msg.id, code, error });
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (msg.type === 'windows') {
|
|
365
|
+
try {
|
|
366
|
+
listWindows(msg.id);
|
|
39
367
|
}
|
|
368
|
+
catch (ex) {
|
|
369
|
+
parent.postMessage({ type: 'result', id: msg.id, code: -1, error: String(ex) });
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
40
372
|
}
|
|
41
|
-
|
|
42
|
-
|
|
373
|
+
if (msg.type === 'screenshot') {
|
|
374
|
+
try {
|
|
375
|
+
// JSON 来的句柄是普通 number,在消息边界一次性标 HWND 品牌(有效性由 Win32 判定)
|
|
376
|
+
const hwnd = (msg.hwnd ?? null);
|
|
377
|
+
const { width, height } = hwnd === null
|
|
378
|
+
? captureScreen(msg.id)
|
|
379
|
+
: captureWindow(msg.id, hwnd, msg.area ?? 'window', msg.mode ?? 'screen');
|
|
380
|
+
parent.postMessage({ type: 'result', id: msg.id, code: 0, error: null, width, height });
|
|
381
|
+
}
|
|
382
|
+
catch (ex) {
|
|
383
|
+
parent.postMessage({ type: 'result', id: msg.id, code: -1, error: String(ex) });
|
|
384
|
+
}
|
|
43
385
|
}
|
|
44
|
-
|
|
386
|
+
}
|
|
387
|
+
parent.onmessage = (e) => {
|
|
388
|
+
handleRequest(e.data);
|
|
389
|
+
// 父侧每个请求起一个新 worker,一次只处理一个请求,处理完立刻释放自己的 port:
|
|
390
|
+
// port_list 清空后 js_os_poll 返回 -1(quickjs-libc.c:2506)→ 本线程退出,
|
|
391
|
+
// JSRuntime 随之释放。不设 null 的话线程常驻(实测 win7 30 次 /screenshot 线程 4→35)。
|
|
392
|
+
// 注意父侧 `worker.onmessage = null` 只释放父侧 port,对本线程无效——必须在这里设。
|
|
393
|
+
// 消息已在 js_post_message_pipe 进队列,父侧读的是同一份 refcount 共享管道,线程退出不影响。
|
|
394
|
+
parent.onmessage = null;
|
|
45
395
|
};
|