quickwin 1.1.4 → 1.2.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 +7 -3
- package/README.md +7 -3
- package/examples/exec_server.js +3535 -34
- package/examples/exec_server.ts +71 -26
- package/examples/exec_server_worker.js +45 -0
- package/examples/exec_server_worker.ts +39 -0
- package/examples/setres.js +104 -0
- package/examples/setres.ts +120 -0
- package/package.json +1 -1
- package/quickwin.d.ts +1 -1
- 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/run.js +3 -1
- package/test/run.ts +3 -1
- package/test/test_worker_concurrent.js +45 -0
- package/test/test_worker_concurrent.ts +56 -0
- package/test/test_worker_data_url.js +46 -0
- package/test/test_worker_data_url.ts +51 -0
package/examples/exec_server.ts
CHANGED
|
@@ -1,35 +1,80 @@
|
|
|
1
1
|
import { createServer } from '../lib/http-server.js'
|
|
2
|
-
import * as
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
|
|
4
|
+
// WORKER_DATA_URL 由 esbuild --define 在 build.ts 中注入(base64 data: URL)
|
|
5
|
+
declare const WORKER_DATA_URL: string
|
|
6
|
+
|
|
7
|
+
const EXEC_TIMEOUT_MS = 60_000
|
|
8
|
+
|
|
9
|
+
interface ExecResult {
|
|
10
|
+
out: Uint8Array
|
|
11
|
+
code: number
|
|
12
|
+
error: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function runInWorker(cmd: string): Promise<ExecResult> {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const worker = new os.Worker(WORKER_DATA_URL)
|
|
18
|
+
const id = 1
|
|
19
|
+
let settled = false
|
|
20
|
+
let timer: ReturnType<typeof os.setTimeout>
|
|
21
|
+
|
|
22
|
+
const finish = (fn: () => void) => {
|
|
23
|
+
if (settled) return
|
|
24
|
+
settled = true
|
|
25
|
+
os.clearTimeout(timer)
|
|
26
|
+
worker.onmessage = null
|
|
27
|
+
fn()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
timer = os.setTimeout(() => {
|
|
31
|
+
finish(() => reject(new Error('exec timeout')))
|
|
32
|
+
}, EXEC_TIMEOUT_MS)
|
|
33
|
+
|
|
34
|
+
worker.onmessage = (e) => {
|
|
35
|
+
const msg = e.data as { type: string; id: number; out?: Uint8Array; code?: number; error?: string | null }
|
|
36
|
+
if (msg.type !== 'result' || msg.id !== id) return
|
|
37
|
+
finish(() => resolve({ out: msg.out ?? new Uint8Array(0), code: msg.code ?? -1, error: msg.error ?? null }))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
worker.postMessage({ type: 'run', id, cmd })
|
|
42
|
+
} catch (ex) {
|
|
43
|
+
finish(() => reject(ex instanceof Error ? ex : new Error(String(ex))))
|
|
44
|
+
}
|
|
45
|
+
})
|
|
18
46
|
}
|
|
19
47
|
|
|
20
48
|
const server = createServer(async (req) => {
|
|
21
|
-
|
|
49
|
+
const path = new URL(req.url).pathname
|
|
50
|
+
if (req.method === 'GET' && (path === '/' || path === '/health')) {
|
|
51
|
+
return new Response('ok', { headers: { 'Content-Type': 'text/plain' } })
|
|
52
|
+
}
|
|
53
|
+
if (req.method === 'POST' && path === '/exec') {
|
|
22
54
|
const body = await req.json() as { cmd: string }
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
try {
|
|
56
|
+
const { out, code, error } = await runInWorker(body.cmd)
|
|
57
|
+
if (error) {
|
|
58
|
+
return new Response(JSON.stringify({ error }), {
|
|
59
|
+
status: 500,
|
|
60
|
+
headers: { 'Content-Type': 'application/json' }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return new Response(out, {
|
|
64
|
+
headers: {
|
|
65
|
+
'Content-Type': 'application/octet-stream',
|
|
66
|
+
'X-Exit-Code': String(code),
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
} catch (ex) {
|
|
70
|
+
return new Response(JSON.stringify({ error: String(ex) }), {
|
|
71
|
+
status: 504,
|
|
72
|
+
headers: { 'Content-Type': 'application/json' }
|
|
73
|
+
})
|
|
74
|
+
}
|
|
30
75
|
}
|
|
31
76
|
return new Response('Not Found', { status: 404 })
|
|
32
77
|
})
|
|
33
78
|
|
|
34
|
-
server.listen(8080)
|
|
35
|
-
console.log('
|
|
79
|
+
const rc = server.listen(8080, '0.0.0.0')
|
|
80
|
+
console.log('listen rc=' + rc + ' addr=' + JSON.stringify(server.address()))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as std from 'std';
|
|
3
|
+
function readBytes(file) {
|
|
4
|
+
const chunks = [];
|
|
5
|
+
const buf = new Uint8Array(4096);
|
|
6
|
+
while (true) {
|
|
7
|
+
const n = file.read(buf.buffer, 0, 4096);
|
|
8
|
+
if (n <= 0)
|
|
9
|
+
break;
|
|
10
|
+
chunks.push(buf.slice(0, n));
|
|
11
|
+
}
|
|
12
|
+
let total = 0;
|
|
13
|
+
for (const c of chunks)
|
|
14
|
+
total += c.length;
|
|
15
|
+
const out = new Uint8Array(total);
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const c of chunks) {
|
|
18
|
+
out.set(c, offset);
|
|
19
|
+
offset += c.length;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
const parent = os.Worker.parent;
|
|
24
|
+
parent.onmessage = (e) => {
|
|
25
|
+
const msg = e.data;
|
|
26
|
+
if (msg.type !== 'run' || typeof msg.cmd !== 'string')
|
|
27
|
+
return;
|
|
28
|
+
let out = new Uint8Array(0);
|
|
29
|
+
let code = -1;
|
|
30
|
+
let error = null;
|
|
31
|
+
try {
|
|
32
|
+
const file = std.popen(msg.cmd, 'r');
|
|
33
|
+
if (!file) {
|
|
34
|
+
error = 'popen failed';
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
out = readBytes(file);
|
|
38
|
+
code = file.close();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (ex) {
|
|
42
|
+
error = String(ex);
|
|
43
|
+
}
|
|
44
|
+
parent.postMessage({ type: 'result', id: msg.id, out, code, error });
|
|
45
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as os from 'os'
|
|
2
|
+
import * as std from 'std'
|
|
3
|
+
|
|
4
|
+
function readBytes(file: std.FILE): Uint8Array {
|
|
5
|
+
const chunks: Uint8Array[] = []
|
|
6
|
+
const buf = new Uint8Array(4096)
|
|
7
|
+
while (true) {
|
|
8
|
+
const n = file.read(buf.buffer, 0, 4096)
|
|
9
|
+
if (n <= 0) break
|
|
10
|
+
chunks.push(buf.slice(0, n))
|
|
11
|
+
}
|
|
12
|
+
let total = 0
|
|
13
|
+
for (const c of chunks) total += c.length
|
|
14
|
+
const out = new Uint8Array(total)
|
|
15
|
+
let offset = 0
|
|
16
|
+
for (const c of chunks) { out.set(c, offset); offset += c.length }
|
|
17
|
+
return out
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const parent = os.Worker.parent
|
|
21
|
+
parent.onmessage = (e) => {
|
|
22
|
+
const msg = e.data as { type: string; id: number; cmd?: string }
|
|
23
|
+
if (msg.type !== 'run' || typeof msg.cmd !== 'string') return
|
|
24
|
+
let out: Uint8Array = new Uint8Array(0)
|
|
25
|
+
let code = -1
|
|
26
|
+
let error: string | null = null
|
|
27
|
+
try {
|
|
28
|
+
const file = std.popen(msg.cmd, 'r')
|
|
29
|
+
if (!file) {
|
|
30
|
+
error = 'popen failed'
|
|
31
|
+
} else {
|
|
32
|
+
out = readBytes(file)
|
|
33
|
+
code = file.close()
|
|
34
|
+
}
|
|
35
|
+
} catch (ex) {
|
|
36
|
+
error = String(ex)
|
|
37
|
+
}
|
|
38
|
+
parent.postMessage({ type: 'result', id: msg.id, out, code, error })
|
|
39
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as std from 'std';
|
|
2
|
+
import * as gui from 'gui';
|
|
3
|
+
import * as win from 'win';
|
|
4
|
+
import * as ffi from 'ffi';
|
|
5
|
+
const user32 = win.LoadLibrary('user32.dll');
|
|
6
|
+
if (!user32) {
|
|
7
|
+
print('LoadLibrary user32 failed');
|
|
8
|
+
std.exit(1);
|
|
9
|
+
}
|
|
10
|
+
function loadProc(lib, name) {
|
|
11
|
+
const ptr = win.GetProcAddress(lib, name);
|
|
12
|
+
if (!ptr) {
|
|
13
|
+
print('GetProcAddress ' + name + ' failed');
|
|
14
|
+
std.exit(1);
|
|
15
|
+
}
|
|
16
|
+
return ptr;
|
|
17
|
+
}
|
|
18
|
+
const EnumDisplaySettingsA = loadProc(user32, 'EnumDisplaySettingsA');
|
|
19
|
+
const ChangeDisplaySettingsA = loadProc(user32, 'ChangeDisplaySettingsA');
|
|
20
|
+
/** DEVMODEA used by EnumDisplaySettings; driver reports actual dmSize (often 124). */
|
|
21
|
+
const DEVMODE_SIZE = 220;
|
|
22
|
+
const DM_PELSWIDTH = 0x00800000;
|
|
23
|
+
const DM_PELSHEIGHT = 0x01000000;
|
|
24
|
+
const ENUM_CURRENT_SETTINGS = -1;
|
|
25
|
+
const CDS_UPDATEREGISTRY = 0x00000001;
|
|
26
|
+
const CDS_TEST = 0x00000002;
|
|
27
|
+
const DISP_CHANGE = {
|
|
28
|
+
0: 'SUCCESSFUL',
|
|
29
|
+
1: 'RESTART',
|
|
30
|
+
[-1]: 'FAILED',
|
|
31
|
+
[-2]: 'BADMODE',
|
|
32
|
+
[-3]: 'NOTUPDATED',
|
|
33
|
+
[-4]: 'BADFLAGS',
|
|
34
|
+
[-5]: 'BADPARAM',
|
|
35
|
+
[-6]: 'BADDUALVIEW',
|
|
36
|
+
};
|
|
37
|
+
function newDevMode() {
|
|
38
|
+
const buf = new ArrayBuffer(DEVMODE_SIZE);
|
|
39
|
+
new DataView(buf).setUint16(36, DEVMODE_SIZE, true);
|
|
40
|
+
return buf;
|
|
41
|
+
}
|
|
42
|
+
function enumMode(modeNum, buf) {
|
|
43
|
+
return ffi.ffiCall(EnumDisplaySettingsA, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_SINT32, ffi.FFI_TYPE_POINTER], [null, modeNum, buf], ffi.FFI_TYPE_SINT32);
|
|
44
|
+
}
|
|
45
|
+
function getPels(dv) {
|
|
46
|
+
return [dv.getUint32(108, true), dv.getUint32(112, true), dv.getUint32(120, true)];
|
|
47
|
+
}
|
|
48
|
+
function applyFromCurrent(w, h, flags, label) {
|
|
49
|
+
const dm = newDevMode();
|
|
50
|
+
if (!enumMode(ENUM_CURRENT_SETTINGS, dm)) {
|
|
51
|
+
print(label + ': EnumDisplaySettings CURRENT failed');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const dv = new DataView(dm);
|
|
55
|
+
const before = getPels(dv);
|
|
56
|
+
const size = dv.getUint16(36, true);
|
|
57
|
+
const extra = dv.getUint16(38, true);
|
|
58
|
+
const fields = dv.getUint32(40, true);
|
|
59
|
+
const bpp = dv.getUint32(104, true);
|
|
60
|
+
const freq = dv.getUint32(120, true);
|
|
61
|
+
print(label + ': base size=' + size + ' extra=' + extra + ' fields=0x' + fields.toString(16) +
|
|
62
|
+
' ' + before[0] + 'x' + before[1] + '@' + freq + ' bpp=' + bpp);
|
|
63
|
+
dv.setUint32(40, fields | DM_PELSWIDTH | DM_PELSHEIGHT, true);
|
|
64
|
+
dv.setUint32(108, w, true);
|
|
65
|
+
dv.setUint32(112, h, true);
|
|
66
|
+
const ret = ffi.ffiCall(ChangeDisplaySettingsA, [ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_UINT32], [dm, flags], ffi.FFI_TYPE_SINT32);
|
|
67
|
+
print(label + ': ChangeDisplaySettings ' + w + 'x' + h +
|
|
68
|
+
' flags=0x' + flags.toString(16) + ' ret=' + ret + ' (' + (DISP_CHANGE[ret] ?? '?') + ')');
|
|
69
|
+
return ret;
|
|
70
|
+
}
|
|
71
|
+
const targetW = Number(scriptArgs[1] ?? '1024');
|
|
72
|
+
const targetH = Number(scriptArgs[2] ?? '768');
|
|
73
|
+
if (!Number.isFinite(targetW) || !Number.isFinite(targetH) || targetW <= 0 || targetH <= 0) {
|
|
74
|
+
print('usage: qwin.exe setres.js [width] [height]');
|
|
75
|
+
std.exit(1);
|
|
76
|
+
}
|
|
77
|
+
print('before GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()));
|
|
78
|
+
const probe = newDevMode();
|
|
79
|
+
const modes32 = [];
|
|
80
|
+
for (let i = 0; i < 200; i++) {
|
|
81
|
+
if (!enumMode(i, probe))
|
|
82
|
+
break;
|
|
83
|
+
const dv = new DataView(probe);
|
|
84
|
+
if (dv.getUint32(104, true) !== 32)
|
|
85
|
+
continue;
|
|
86
|
+
const p = getPels(dv);
|
|
87
|
+
modes32.push(p[0] + 'x' + p[1] + '@' + p[2]);
|
|
88
|
+
}
|
|
89
|
+
print('32bpp modes sample: ' + modes32.slice(0, 25).join(', ') + (modes32.length > 25 ? ' ...' : ''));
|
|
90
|
+
const testRet = applyFromCurrent(targetW, targetH, CDS_TEST, 'TEST');
|
|
91
|
+
if (testRet === 0) {
|
|
92
|
+
applyFromCurrent(targetW, targetH, 0, 'DYNAMIC');
|
|
93
|
+
print('after dynamic GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()));
|
|
94
|
+
applyFromCurrent(targetW, targetH, CDS_UPDATEREGISTRY, 'REGISTRY');
|
|
95
|
+
print('after registry GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()));
|
|
96
|
+
}
|
|
97
|
+
else if (testRet !== null) {
|
|
98
|
+
print('target rejected; try 800x600');
|
|
99
|
+
const t2 = applyFromCurrent(800, 600, CDS_TEST, 'TEST800');
|
|
100
|
+
if (t2 === 0) {
|
|
101
|
+
applyFromCurrent(800, 600, 0, 'DYNAMIC800');
|
|
102
|
+
print('after 800x600 GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import * as std from 'std'
|
|
2
|
+
import * as gui from 'gui'
|
|
3
|
+
import * as win from 'win'
|
|
4
|
+
import * as ffi from 'ffi'
|
|
5
|
+
|
|
6
|
+
const user32 = win.LoadLibrary('user32.dll')
|
|
7
|
+
if (!user32) { print('LoadLibrary user32 failed'); std.exit(1) }
|
|
8
|
+
|
|
9
|
+
function loadProc(lib: win.HMODULE, name: string): number {
|
|
10
|
+
const ptr = win.GetProcAddress(lib, name)
|
|
11
|
+
if (!ptr) { print('GetProcAddress ' + name + ' failed'); std.exit(1) }
|
|
12
|
+
return ptr
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const EnumDisplaySettingsA = loadProc(user32, 'EnumDisplaySettingsA')
|
|
16
|
+
const ChangeDisplaySettingsA = loadProc(user32, 'ChangeDisplaySettingsA')
|
|
17
|
+
|
|
18
|
+
/** DEVMODEA used by EnumDisplaySettings; driver reports actual dmSize (often 124). */
|
|
19
|
+
const DEVMODE_SIZE = 220
|
|
20
|
+
const DM_PELSWIDTH = 0x00800000
|
|
21
|
+
const DM_PELSHEIGHT = 0x01000000
|
|
22
|
+
const ENUM_CURRENT_SETTINGS = -1
|
|
23
|
+
const CDS_UPDATEREGISTRY = 0x00000001
|
|
24
|
+
const CDS_TEST = 0x00000002
|
|
25
|
+
|
|
26
|
+
const DISP_CHANGE: Record<number, string> = {
|
|
27
|
+
0: 'SUCCESSFUL',
|
|
28
|
+
1: 'RESTART',
|
|
29
|
+
[-1]: 'FAILED',
|
|
30
|
+
[-2]: 'BADMODE',
|
|
31
|
+
[-3]: 'NOTUPDATED',
|
|
32
|
+
[-4]: 'BADFLAGS',
|
|
33
|
+
[-5]: 'BADPARAM',
|
|
34
|
+
[-6]: 'BADDUALVIEW',
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function newDevMode(): ArrayBuffer {
|
|
38
|
+
const buf = new ArrayBuffer(DEVMODE_SIZE)
|
|
39
|
+
new DataView(buf).setUint16(36, DEVMODE_SIZE, true)
|
|
40
|
+
return buf
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function enumMode(modeNum: number, buf: ArrayBuffer): number {
|
|
44
|
+
return ffi.ffiCall(
|
|
45
|
+
EnumDisplaySettingsA,
|
|
46
|
+
[ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_SINT32, ffi.FFI_TYPE_POINTER],
|
|
47
|
+
[null, modeNum, buf],
|
|
48
|
+
ffi.FFI_TYPE_SINT32,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getPels(dv: DataView): [number, number, number] {
|
|
53
|
+
return [dv.getUint32(108, true), dv.getUint32(112, true), dv.getUint32(120, true)]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function applyFromCurrent(w: number, h: number, flags: number, label: string): number | null {
|
|
57
|
+
const dm = newDevMode()
|
|
58
|
+
if (!enumMode(ENUM_CURRENT_SETTINGS, dm)) {
|
|
59
|
+
print(label + ': EnumDisplaySettings CURRENT failed')
|
|
60
|
+
return null
|
|
61
|
+
}
|
|
62
|
+
const dv = new DataView(dm)
|
|
63
|
+
const before = getPels(dv)
|
|
64
|
+
const size = dv.getUint16(36, true)
|
|
65
|
+
const extra = dv.getUint16(38, true)
|
|
66
|
+
const fields = dv.getUint32(40, true)
|
|
67
|
+
const bpp = dv.getUint32(104, true)
|
|
68
|
+
const freq = dv.getUint32(120, true)
|
|
69
|
+
print(label + ': base size=' + size + ' extra=' + extra + ' fields=0x' + fields.toString(16) +
|
|
70
|
+
' ' + before[0] + 'x' + before[1] + '@' + freq + ' bpp=' + bpp)
|
|
71
|
+
|
|
72
|
+
dv.setUint32(40, fields | DM_PELSWIDTH | DM_PELSHEIGHT, true)
|
|
73
|
+
dv.setUint32(108, w, true)
|
|
74
|
+
dv.setUint32(112, h, true)
|
|
75
|
+
|
|
76
|
+
const ret = ffi.ffiCall(
|
|
77
|
+
ChangeDisplaySettingsA,
|
|
78
|
+
[ffi.FFI_TYPE_POINTER, ffi.FFI_TYPE_UINT32],
|
|
79
|
+
[dm, flags],
|
|
80
|
+
ffi.FFI_TYPE_SINT32,
|
|
81
|
+
)
|
|
82
|
+
print(label + ': ChangeDisplaySettings ' + w + 'x' + h +
|
|
83
|
+
' flags=0x' + flags.toString(16) + ' ret=' + ret + ' (' + (DISP_CHANGE[ret] ?? '?') + ')')
|
|
84
|
+
return ret
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const targetW = Number(scriptArgs[1] ?? '1024')
|
|
88
|
+
const targetH = Number(scriptArgs[2] ?? '768')
|
|
89
|
+
if (!Number.isFinite(targetW) || !Number.isFinite(targetH) || targetW <= 0 || targetH <= 0) {
|
|
90
|
+
print('usage: qwin.exe setres.js [width] [height]')
|
|
91
|
+
std.exit(1)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
print('before GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()))
|
|
95
|
+
|
|
96
|
+
const probe = newDevMode()
|
|
97
|
+
const modes32: string[] = []
|
|
98
|
+
for (let i = 0; i < 200; i++) {
|
|
99
|
+
if (!enumMode(i, probe)) break
|
|
100
|
+
const dv = new DataView(probe)
|
|
101
|
+
if (dv.getUint32(104, true) !== 32) continue
|
|
102
|
+
const p = getPels(dv)
|
|
103
|
+
modes32.push(p[0] + 'x' + p[1] + '@' + p[2])
|
|
104
|
+
}
|
|
105
|
+
print('32bpp modes sample: ' + modes32.slice(0, 25).join(', ') + (modes32.length > 25 ? ' ...' : ''))
|
|
106
|
+
|
|
107
|
+
const testRet = applyFromCurrent(targetW, targetH, CDS_TEST, 'TEST')
|
|
108
|
+
if (testRet === 0) {
|
|
109
|
+
applyFromCurrent(targetW, targetH, 0, 'DYNAMIC')
|
|
110
|
+
print('after dynamic GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()))
|
|
111
|
+
applyFromCurrent(targetW, targetH, CDS_UPDATEREGISTRY, 'REGISTRY')
|
|
112
|
+
print('after registry GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()))
|
|
113
|
+
} else if (testRet !== null) {
|
|
114
|
+
print('target rejected; try 800x600')
|
|
115
|
+
const t2 = applyFromCurrent(800, 600, CDS_TEST, 'TEST800')
|
|
116
|
+
if (t2 === 0) {
|
|
117
|
+
applyFromCurrent(800, 600, 0, 'DYNAMIC800')
|
|
118
|
+
print('after 800x600 GetScreenSize: ' + JSON.stringify(gui.GetScreenSize()))
|
|
119
|
+
}
|
|
120
|
+
}
|
package/package.json
CHANGED
package/quickwin.d.ts
CHANGED
|
@@ -330,7 +330,7 @@ declare module "os" {
|
|
|
330
330
|
const S_IFREG: number;
|
|
331
331
|
|
|
332
332
|
class Worker {
|
|
333
|
-
/** Constructor to create a new thread (worker) with an API close to the `WebWorkers`. `module_filename` is a
|
|
333
|
+
/** Constructor to create a new thread (worker) with an API close to the `WebWorkers`. `module_filename` is a file path, `http(s)://` URL, or `data:text/javascript;base64,...` URL executed in the newly created thread. */
|
|
334
334
|
constructor(module_filename: string);
|
|
335
335
|
/** Send a message to the corresponding worker. `msg` is cloned in the destination worker using an algorithm similar to the `HTML` structured clone algorithm. */
|
|
336
336
|
postMessage(msg: unknown): void;
|
package/qwin-nowasm-x86.exe
CHANGED
|
Binary file
|
package/qwin-nowasm.exe
CHANGED
|
Binary file
|
package/qwin-x86.exe
CHANGED
|
Binary file
|
package/qwin.exe
CHANGED
|
Binary file
|
package/test/run.js
CHANGED
|
@@ -34,7 +34,9 @@ const suiteDefs = [
|
|
|
34
34
|
{ name: 'worker', file: './test_worker.js', tags: [] },
|
|
35
35
|
{ name: 'worker-wasm', file: './test_worker_wasm_concurrent.js', tags: ['wasm'] },
|
|
36
36
|
{ name: 'worker-http', file: './test_worker_net.js', tags: ['net'] },
|
|
37
|
-
{ name: 'worker-fetch', file: './test_worker_fetch.js', tags: ['net'] }
|
|
37
|
+
{ name: 'worker-fetch', file: './test_worker_fetch.js', tags: ['net'] },
|
|
38
|
+
{ name: 'worker-concurrent', file: './test_worker_concurrent.js', tags: [] },
|
|
39
|
+
{ name: 'worker-data-url', file: './test_worker_data_url.js', tags: [] }
|
|
38
40
|
];
|
|
39
41
|
async function main() {
|
|
40
42
|
const filter = scriptArgs.length > 1 ? scriptArgs[1] : '';
|
package/test/run.ts
CHANGED
|
@@ -38,7 +38,9 @@ const suiteDefs = [
|
|
|
38
38
|
{ name: 'worker', file: './test_worker.js', tags: [] },
|
|
39
39
|
{ name: 'worker-wasm', file: './test_worker_wasm_concurrent.js', tags: ['wasm'] },
|
|
40
40
|
{ name: 'worker-http', file: './test_worker_net.js', tags: ['net'] },
|
|
41
|
-
{ name: 'worker-fetch', file: './test_worker_fetch.js', tags: ['net'] }
|
|
41
|
+
{ name: 'worker-fetch', file: './test_worker_fetch.js', tags: ['net'] },
|
|
42
|
+
{ name: 'worker-concurrent', file: './test_worker_concurrent.js', tags: [] },
|
|
43
|
+
{ name: 'worker-data-url', file: './test_worker_data_url.js', tags: [] }
|
|
42
44
|
]
|
|
43
45
|
|
|
44
46
|
async function main(): Promise<void> {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
import * as std from 'std';
|
|
3
|
+
function runRound() {
|
|
4
|
+
const ids = [1, 2];
|
|
5
|
+
return Promise.all(ids.map((id) => new Promise((resolve, reject) => {
|
|
6
|
+
const worker = new os.Worker('./worker_module.js');
|
|
7
|
+
const timer = os.setTimeout(() => {
|
|
8
|
+
worker.onmessage = null;
|
|
9
|
+
reject(new Error(`worker ${id} timeout`));
|
|
10
|
+
}, 5000);
|
|
11
|
+
worker.onmessage = (e) => {
|
|
12
|
+
os.clearTimeout(timer);
|
|
13
|
+
worker.postMessage({ type: 'done' });
|
|
14
|
+
worker.onmessage = null;
|
|
15
|
+
resolve(e.data);
|
|
16
|
+
};
|
|
17
|
+
worker.postMessage({ type: 'start' });
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
20
|
+
export const suite = {
|
|
21
|
+
name: 'worker-concurrent',
|
|
22
|
+
run: async (t) => {
|
|
23
|
+
t.section('dual-worker-init');
|
|
24
|
+
const N = 20;
|
|
25
|
+
let ok = 0;
|
|
26
|
+
let lastErr = null;
|
|
27
|
+
for (let i = 0; i < N; i++) {
|
|
28
|
+
try {
|
|
29
|
+
const results = await runRound();
|
|
30
|
+
if (results.length === 2 &&
|
|
31
|
+
results.every((r) => r.type === 'result' && r.value === 42)) {
|
|
32
|
+
ok++;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
lastErr = e;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (ok !== N && lastErr) {
|
|
41
|
+
std.printf('round error: %s\n', String(lastErr));
|
|
42
|
+
}
|
|
43
|
+
t.check('concurrent dual-worker rounds', N, ok);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Tester } from './test_helper.js'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
import * as std from 'std'
|
|
4
|
+
|
|
5
|
+
interface RoundResults {
|
|
6
|
+
type: string
|
|
7
|
+
value: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function runRound(): Promise<RoundResults[]> {
|
|
11
|
+
const ids = [1, 2]
|
|
12
|
+
return Promise.all(ids.map((id) =>
|
|
13
|
+
new Promise<RoundResults>((resolve, reject) => {
|
|
14
|
+
const worker: any = new os.Worker('./worker_module.js')
|
|
15
|
+
const timer = os.setTimeout(() => {
|
|
16
|
+
worker.onmessage = null
|
|
17
|
+
reject(new Error(`worker ${id} timeout`))
|
|
18
|
+
}, 5000)
|
|
19
|
+
worker.onmessage = (e: any) => {
|
|
20
|
+
os.clearTimeout(timer)
|
|
21
|
+
worker.postMessage({ type: 'done' })
|
|
22
|
+
worker.onmessage = null
|
|
23
|
+
resolve(e.data)
|
|
24
|
+
}
|
|
25
|
+
worker.postMessage({ type: 'start' })
|
|
26
|
+
})
|
|
27
|
+
))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const suite = {
|
|
31
|
+
name: 'worker-concurrent',
|
|
32
|
+
run: async (t: Tester) => {
|
|
33
|
+
t.section('dual-worker-init')
|
|
34
|
+
const N = 20
|
|
35
|
+
let ok = 0
|
|
36
|
+
let lastErr: unknown = null
|
|
37
|
+
for (let i = 0; i < N; i++) {
|
|
38
|
+
try {
|
|
39
|
+
const results = await runRound()
|
|
40
|
+
if (
|
|
41
|
+
results.length === 2 &&
|
|
42
|
+
results.every((r) => r.type === 'result' && r.value === 42)
|
|
43
|
+
) {
|
|
44
|
+
ok++
|
|
45
|
+
}
|
|
46
|
+
} catch (e) {
|
|
47
|
+
lastErr = e
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (ok !== N && lastErr) {
|
|
52
|
+
std.printf('round error: %s\n', String(lastErr))
|
|
53
|
+
}
|
|
54
|
+
t.check('concurrent dual-worker rounds', N, ok)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as os from 'os';
|
|
2
|
+
// base64 of worker body (see comment). Split manually to keep lines short.
|
|
3
|
+
const B64_WORKER = 'aW1wb3J0ICogYXMgb3MgZnJvbSAnb3MnCmNvbnN0IHBhcmVudCA9IG9zLldvcmtlci5wYXJl' +
|
|
4
|
+
'bnQKcGFyZW50Lm9ubWVzc2FnZSA9IChlKSA9PiB7CiAgICBjb25zdCBkID0gZS5kYXRhCiAg' +
|
|
5
|
+
'ICBpZiAoZC50eXBlID09PSAnc3RhcnQnKSBwYXJlbnQucG9zdE1lc3NhZ2UoeyB0eXBlOiAn' +
|
|
6
|
+
'cmVzdWx0JywgdmFsdWU6IDQyIH0pCiAgICBlbHNlIGlmIChkLnR5cGUgPT09ICdkb25lJykg' +
|
|
7
|
+
'cGFyZW50Lm9ubWVzc2FnZSA9IG51bGwKfQo=';
|
|
8
|
+
const DATA_WORKER = 'data:text/javascript;base64,' + B64_WORKER;
|
|
9
|
+
// export const answer = 42\n
|
|
10
|
+
const DATA_MOD = 'data:text/javascript;base64,ZXhwb3J0IGNvbnN0IGFuc3dlciA9IDQyCg==';
|
|
11
|
+
export const suite = {
|
|
12
|
+
name: 'worker-data-url',
|
|
13
|
+
run: async (t) => {
|
|
14
|
+
t.section('base64-worker-entry');
|
|
15
|
+
let worker;
|
|
16
|
+
try {
|
|
17
|
+
const { data: result } = await new Promise((resolve, reject) => {
|
|
18
|
+
worker = new os.Worker(DATA_WORKER);
|
|
19
|
+
const timer = os.setTimeout(() => reject(new Error('worker timeout')), 5000);
|
|
20
|
+
worker.onmessage = (e) => {
|
|
21
|
+
os.clearTimeout(timer);
|
|
22
|
+
resolve({ data: e.data });
|
|
23
|
+
};
|
|
24
|
+
worker.postMessage({ type: 'start' });
|
|
25
|
+
});
|
|
26
|
+
t.check('message type', 'result', result.type);
|
|
27
|
+
t.check('imported value', 42, result.value);
|
|
28
|
+
worker.postMessage({ type: 'done' });
|
|
29
|
+
worker.onmessage = null;
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
t.checkTrue('data url worker failed: ' + e, false);
|
|
33
|
+
if (worker) {
|
|
34
|
+
worker.onmessage = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
t.section('dynamic-import-data-url');
|
|
38
|
+
try {
|
|
39
|
+
const mod = await import(DATA_MOD);
|
|
40
|
+
t.check('import(data:) answer', 42, mod.answer);
|
|
41
|
+
}
|
|
42
|
+
catch (e) {
|
|
43
|
+
t.checkTrue('import(data:) failed: ' + e, false);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Tester } from './test_helper.js'
|
|
2
|
+
import * as os from 'os'
|
|
3
|
+
|
|
4
|
+
// base64 of worker body (see comment). Split manually to keep lines short.
|
|
5
|
+
const B64_WORKER =
|
|
6
|
+
'aW1wb3J0ICogYXMgb3MgZnJvbSAnb3MnCmNvbnN0IHBhcmVudCA9IG9zLldvcmtlci5wYXJl' +
|
|
7
|
+
'bnQKcGFyZW50Lm9ubWVzc2FnZSA9IChlKSA9PiB7CiAgICBjb25zdCBkID0gZS5kYXRhCiAg' +
|
|
8
|
+
'ICBpZiAoZC50eXBlID09PSAnc3RhcnQnKSBwYXJlbnQucG9zdE1lc3NhZ2UoeyB0eXBlOiAn' +
|
|
9
|
+
'cmVzdWx0JywgdmFsdWU6IDQyIH0pCiAgICBlbHNlIGlmIChkLnR5cGUgPT09ICdkb25lJykg' +
|
|
10
|
+
'cGFyZW50Lm9ubWVzc2FnZSA9IG51bGwKfQo='
|
|
11
|
+
|
|
12
|
+
const DATA_WORKER = 'data:text/javascript;base64,' + B64_WORKER
|
|
13
|
+
|
|
14
|
+
// export const answer = 42\n
|
|
15
|
+
const DATA_MOD = 'data:text/javascript;base64,ZXhwb3J0IGNvbnN0IGFuc3dlciA9IDQyCg=='
|
|
16
|
+
|
|
17
|
+
export const suite = {
|
|
18
|
+
name: 'worker-data-url',
|
|
19
|
+
run: async (t: Tester) => {
|
|
20
|
+
t.section('base64-worker-entry')
|
|
21
|
+
let worker: any
|
|
22
|
+
try {
|
|
23
|
+
const { data: result } = await new Promise<any>((resolve, reject) => {
|
|
24
|
+
worker = new os.Worker(DATA_WORKER)
|
|
25
|
+
const timer = os.setTimeout(() => reject(new Error('worker timeout')), 5000)
|
|
26
|
+
worker.onmessage = (e: any) => {
|
|
27
|
+
os.clearTimeout(timer)
|
|
28
|
+
resolve({ data: e.data })
|
|
29
|
+
}
|
|
30
|
+
worker.postMessage({ type: 'start' })
|
|
31
|
+
})
|
|
32
|
+
t.check('message type', 'result', result.type)
|
|
33
|
+
t.check('imported value', 42, result.value)
|
|
34
|
+
worker.postMessage({ type: 'done' })
|
|
35
|
+
worker.onmessage = null
|
|
36
|
+
} catch (e) {
|
|
37
|
+
t.checkTrue('data url worker failed: ' + e, false)
|
|
38
|
+
if (worker) {
|
|
39
|
+
worker.onmessage = null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
t.section('dynamic-import-data-url')
|
|
44
|
+
try {
|
|
45
|
+
const mod = await import(DATA_MOD)
|
|
46
|
+
t.check('import(data:) answer', 42, (mod as any).answer)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
t.checkTrue('import(data:) failed: ' + e, false)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|