quickwin 2.3.0 → 2.3.1
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/constants.ts +7 -0
- package/enum_map.d.ts +1 -0
- package/examples/execd/main.js +160 -32
- package/lib/http-server.js +50 -25
- package/lib/http-server.ts +51 -23
- package/package.json +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/constants.ts
CHANGED
|
@@ -961,6 +961,13 @@ export const enum ProcessCreationFlag {
|
|
|
961
961
|
NO_WINDOW = 134217728, // 0x8000000
|
|
962
962
|
}
|
|
963
963
|
|
|
964
|
+
// File
|
|
965
|
+
export const enum MoveFileFlag {
|
|
966
|
+
REPLACE_EXISTING = 1, // 0x1
|
|
967
|
+
COPY_ALLOWED = 2, // 0x2
|
|
968
|
+
WRITE_THROUGH = 8, // 0x8
|
|
969
|
+
}
|
|
970
|
+
|
|
964
971
|
// Error codes
|
|
965
972
|
export const enum ErrorCode {
|
|
966
973
|
FILE_NOT_FOUND = 2, // 0x2
|
package/enum_map.d.ts
CHANGED
|
@@ -108,6 +108,7 @@ interface EnumMap {
|
|
|
108
108
|
IDI: import('./constants').IDI
|
|
109
109
|
DrawTextFlag: import('./constants').DrawTextFlag
|
|
110
110
|
ProcessCreationFlag: import('./constants').ProcessCreationFlag
|
|
111
|
+
MoveFileFlag: import('./constants').MoveFileFlag
|
|
111
112
|
ErrorCode: import('./constants').ErrorCode
|
|
112
113
|
PowerBroadcast: import('./constants').PowerBroadcast
|
|
113
114
|
DevMode: import('./constants').DevMode
|
package/examples/execd/main.js
CHANGED
|
@@ -18535,6 +18535,8 @@ function concatU8(parts) {
|
|
|
18535
18535
|
return out;
|
|
18536
18536
|
}
|
|
18537
18537
|
function toArrayBuffer(u8) {
|
|
18538
|
+
if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength)
|
|
18539
|
+
return u8.buffer;
|
|
18538
18540
|
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
|
18539
18541
|
}
|
|
18540
18542
|
function findCrLfCrLf(buf) {
|
|
@@ -18694,6 +18696,7 @@ var HttpServer = class {
|
|
|
18694
18696
|
const c2 = {
|
|
18695
18697
|
sock: ac.handle,
|
|
18696
18698
|
parts: [],
|
|
18699
|
+
partsLen: 0,
|
|
18697
18700
|
pending: null,
|
|
18698
18701
|
current: null,
|
|
18699
18702
|
outQueue: null,
|
|
@@ -18732,7 +18735,9 @@ function onRead(c2) {
|
|
|
18732
18735
|
const d2 = sock.recv(c2.sock, 8192);
|
|
18733
18736
|
if (!d2)
|
|
18734
18737
|
break;
|
|
18735
|
-
|
|
18738
|
+
const u8 = new Uint8Array(d2);
|
|
18739
|
+
c2.parts.push(u8);
|
|
18740
|
+
c2.partsLen += u8.length;
|
|
18736
18741
|
}
|
|
18737
18742
|
if (!c2.closed)
|
|
18738
18743
|
tryProcess(c2);
|
|
@@ -18822,11 +18827,17 @@ async function writeStreamingBody(c2, body, chunked, trailer) {
|
|
|
18822
18827
|
if (done)
|
|
18823
18828
|
break;
|
|
18824
18829
|
if (value && value.byteLength > 0) {
|
|
18825
|
-
const
|
|
18826
|
-
|
|
18827
|
-
|
|
18828
|
-
|
|
18829
|
-
|
|
18830
|
+
const MAX_SEND = 65536;
|
|
18831
|
+
for (let off = 0; off < value.byteLength; off += MAX_SEND) {
|
|
18832
|
+
const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength));
|
|
18833
|
+
const frame = chunked ? encodeChunkFrame(piece, enc) : piece;
|
|
18834
|
+
if (!queueSend(c2, frame)) {
|
|
18835
|
+
await new Promise((resolve) => {
|
|
18836
|
+
c2.resumeRead = resolve;
|
|
18837
|
+
});
|
|
18838
|
+
}
|
|
18839
|
+
if (c2.closed)
|
|
18840
|
+
break;
|
|
18830
18841
|
}
|
|
18831
18842
|
}
|
|
18832
18843
|
}
|
|
@@ -18867,36 +18878,36 @@ function whole(c2) {
|
|
|
18867
18878
|
function trimParts(c2, buf, consumed) {
|
|
18868
18879
|
if (consumed >= buf.length) {
|
|
18869
18880
|
c2.parts = [];
|
|
18881
|
+
c2.partsLen = 0;
|
|
18870
18882
|
} else {
|
|
18871
18883
|
c2.parts = [buf.subarray(consumed)];
|
|
18884
|
+
c2.partsLen = buf.length - consumed;
|
|
18872
18885
|
}
|
|
18873
18886
|
}
|
|
18874
18887
|
function tryProcess(c2) {
|
|
18875
18888
|
if (c2.closed || c2.current)
|
|
18876
18889
|
return;
|
|
18877
|
-
const buf = whole(c2);
|
|
18878
18890
|
if (c2.pending) {
|
|
18879
18891
|
const bodyStart2 = c2.pending.bodyStart;
|
|
18880
18892
|
const parsed2 = c2.pending.parsed;
|
|
18881
|
-
|
|
18882
|
-
|
|
18883
|
-
|
|
18884
|
-
const dec = decodeChunked(buf, bodyStart2);
|
|
18885
|
-
if (!dec)
|
|
18886
|
-
return;
|
|
18887
|
-
body = dec.data;
|
|
18888
|
-
end = dec.end;
|
|
18889
|
-
} else {
|
|
18890
|
-
const needed = parsed2.contentLength;
|
|
18891
|
-
if (bodyStart2 + needed > buf.length)
|
|
18893
|
+
if (!parsed2.chunked) {
|
|
18894
|
+
const end = bodyStart2 + parsed2.contentLength;
|
|
18895
|
+
if (end > c2.partsLen)
|
|
18892
18896
|
return;
|
|
18893
|
-
|
|
18894
|
-
|
|
18897
|
+
const buf3 = whole(c2);
|
|
18898
|
+
c2.pending = null;
|
|
18899
|
+
dispatchRequest(c2, parsed2, buf3.subarray(bodyStart2, end), end, buf3);
|
|
18900
|
+
return;
|
|
18895
18901
|
}
|
|
18902
|
+
const buf2 = whole(c2);
|
|
18903
|
+
const dec = decodeChunked(buf2, bodyStart2);
|
|
18904
|
+
if (!dec)
|
|
18905
|
+
return;
|
|
18896
18906
|
c2.pending = null;
|
|
18897
|
-
dispatchRequest(c2, parsed2,
|
|
18907
|
+
dispatchRequest(c2, parsed2, dec.data, dec.end, buf2);
|
|
18898
18908
|
return;
|
|
18899
18909
|
}
|
|
18910
|
+
const buf = whole(c2);
|
|
18900
18911
|
const headerEnd = findCrLfCrLf(buf);
|
|
18901
18912
|
if (headerEnd < 0)
|
|
18902
18913
|
return;
|
|
@@ -18914,18 +18925,18 @@ function tryProcess(c2) {
|
|
|
18914
18925
|
c2.pending = { parsed, bodyStart };
|
|
18915
18926
|
return;
|
|
18916
18927
|
}
|
|
18917
|
-
dispatchRequest(c2, parsed, dec.data, dec.end);
|
|
18928
|
+
dispatchRequest(c2, parsed, dec.data, dec.end, buf);
|
|
18918
18929
|
} else if (parsed.contentLength > 0) {
|
|
18919
18930
|
if (bodyStart + parsed.contentLength > buf.length) {
|
|
18920
18931
|
c2.pending = { parsed, bodyStart };
|
|
18921
18932
|
return;
|
|
18922
18933
|
}
|
|
18923
|
-
dispatchRequest(c2, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength);
|
|
18934
|
+
dispatchRequest(c2, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf);
|
|
18924
18935
|
} else {
|
|
18925
|
-
dispatchRequest(c2, parsed, new Uint8Array(0), bodyStart);
|
|
18936
|
+
dispatchRequest(c2, parsed, new Uint8Array(0), bodyStart, buf);
|
|
18926
18937
|
}
|
|
18927
18938
|
}
|
|
18928
|
-
function dispatchRequest(c2, parsed, body, end) {
|
|
18939
|
+
function dispatchRequest(c2, parsed, body, end, wholeBuf) {
|
|
18929
18940
|
const connHeader = (parsed.headers["connection"] || "").toLowerCase();
|
|
18930
18941
|
if (parsed.version === "HTTP/1.1")
|
|
18931
18942
|
c2.keepAlive = connHeader.indexOf("close") < 0;
|
|
@@ -18948,7 +18959,7 @@ function dispatchRequest(c2, parsed, body, end) {
|
|
|
18948
18959
|
});
|
|
18949
18960
|
c2.ac = ac;
|
|
18950
18961
|
c2.current = { req };
|
|
18951
|
-
trimParts(c2, whole(c2), end);
|
|
18962
|
+
trimParts(c2, wholeBuf ?? whole(c2), end);
|
|
18952
18963
|
void handleRequest(c2);
|
|
18953
18964
|
}
|
|
18954
18965
|
async function handleRequest(c2) {
|
|
@@ -22022,10 +22033,6 @@ function createRoot(container) {
|
|
|
22022
22033
|
// _build/examples/execd/main.js
|
|
22023
22034
|
var import_react18 = __toESM(require_react(), 1);
|
|
22024
22035
|
|
|
22025
|
-
// _build/examples/execd/tray.js
|
|
22026
|
-
import * as os6 from "os";
|
|
22027
|
-
import * as gui18 from "gui";
|
|
22028
|
-
|
|
22029
22036
|
// _build/lib/windows/kernel32.js
|
|
22030
22037
|
var b7 = (name, sig, encoders) => bind2("kernel32.dll", name, sig, encoders);
|
|
22031
22038
|
var CloseHandle = /* @__PURE__ */ b7("CloseHandle", "<>ptr -> i32");
|
|
@@ -22034,9 +22041,13 @@ var GetCurrentProcess = /* @__PURE__ */ b7("GetCurrentProcess", " -> <HPROCESS>p
|
|
|
22034
22041
|
var VirtualAllocEx = /* @__PURE__ */ b7("VirtualAllocEx", "<HPROCESS>ptr <>ptr <>ptr u32 u32 -> <>ptr");
|
|
22035
22042
|
var VirtualFreeEx = /* @__PURE__ */ b7("VirtualFreeEx", "<HPROCESS>ptr <>ptr <>ptr u32 -> i32");
|
|
22036
22043
|
var ReadProcessMemory = /* @__PURE__ */ b7("ReadProcessMemory", "<HPROCESS>ptr <>ptr <BYTE>ptr <>ptr <>ptr -> i32");
|
|
22044
|
+
var MoveFileEx = /* @__PURE__ */ b7("MoveFileExW", "<WCHAR>ptr! <WCHAR>ptr! u32 -> i32");
|
|
22045
|
+
var GetLastError = /* @__PURE__ */ b7("GetLastError", " -> u32");
|
|
22037
22046
|
var IsWow64Process = /* @__PURE__ */ b7("IsWow64Process", "<HPROCESS>ptr <BYTE>ptr -> i32");
|
|
22038
22047
|
|
|
22039
22048
|
// _build/examples/execd/tray.js
|
|
22049
|
+
import * as os6 from "os";
|
|
22050
|
+
import * as gui18 from "gui";
|
|
22040
22051
|
var TB_GETBUTTON = 1047;
|
|
22041
22052
|
var TB_BUTTONCOUNT = 1048;
|
|
22042
22053
|
var TB_GETITEMRECT = 1053;
|
|
@@ -22744,7 +22755,14 @@ API\uFF08\u914D\u7F6E token \u540E\u9700 Authorization: Bearer <token>\uFF0C\u67
|
|
|
22744
22755
|
POST /input body = JSON \u6570\u7EC4\uFF08\u6279\u91CF\u6CE8\u5165\uFF1Bx/y \u7269\u7406\u50CF\u7D20\uFF09\uFF1A
|
|
22745
22756
|
{"t":"move","x":,"y":} | {"t":"down|up","btn":"left|right|middle|x1|x2","x":,"y":?} |
|
|
22746
22757
|
{"t":"key","key":"a|enter|0x41","up":true?} | {"t":"sleep","ms":0..5000}
|
|
22747
|
-
\u2192 JSON {sent,expected}\uFF1B\u6309 sleep \u5207\u6BB5\uFF0C\u6BB5\u5185\u4E00\u6B21 SendInput \u539F\u5B50\u6CE8\u5165
|
|
22758
|
+
\u2192 JSON {sent,expected}\uFF1B\u6309 sleep \u5207\u6BB5\uFF0C\u6BB5\u5185\u4E00\u6B21 SendInput \u539F\u5B50\u6CE8\u5165
|
|
22759
|
+
POST /upload ?path=<\u76EE\u6807\u8DEF\u5F84>&mkdir=0 body=\u539F\u59CB\u5B57\u8282\uFF08\u975E\u7A7A\uFF09\u2192 JSON {bytes,path}
|
|
22760
|
+
\u9ED8\u8BA4\u9010\u5C42\u5EFA\u7236\u76EE\u5F55\uFF08mkdir=0 \u4E25\u683C\u8981\u6C42\u7236\u76EE\u5F55\u5DF2\u5B58\u5728\uFF0C\u9632\u8DEF\u5F84 typo\uFF09\uFF1B
|
|
22761
|
+
\u539F\u5B50\u5199\uFF1A\u5148\u5199 <path>.uploading \u518D MoveFileEx \u8986\u76D6\uFF0C\u770B\u4E0D\u5230\u534A\u622A\u6587\u4EF6\u3002
|
|
22762
|
+
\u65E0 SMB \u73AF\u5883\u7684\u4F20\u6587\u4EF6\u901A\u9053\uFF0C\u8DEF\u5F84\u5168\u653E\u5F00\u30028MB \u5206\u5757\u5199\u76D8\uFF08CRT \u5355\u5757\u5927\u5199
|
|
22763
|
+
\u5728 XP \u7F51\u7EDC\u76D8 ~64MiB \u8FB9\u754C\u4F1A\u88AB\u6574\u5757\u62D2\u7EDD\uFF09\uFF0CXP/win7 \u5B9E\u6D4B 100MB+ OK
|
|
22764
|
+
GET /download ?path=<\u6587\u4EF6\u8DEF\u5F84> \u2192 \u539F\u59CB\u5B57\u8282\u6D41\uFF08application/octet-stream\uFF0CX-Bytes \u5934\uFF1B
|
|
22765
|
+
\u5206\u5757\u8BFB\u5230 EOF \u65E0\u5927\u5C0F\u4E0A\u9650\uFF09\u2014\u2014type \u547D\u4EE4\u7684\u4E8C\u8FDB\u5236\u5B89\u5168\u7248`;
|
|
22748
22766
|
function usageResponse() {
|
|
22749
22767
|
return new Response(USAGE, { headers: { "Content-Type": "text/plain; charset=utf-8" } });
|
|
22750
22768
|
}
|
|
@@ -22809,10 +22827,23 @@ function vkOf(key) {
|
|
|
22809
22827
|
}
|
|
22810
22828
|
throw new Error("bad key: " + JSON.stringify(key) + " (a-z/0-9|name|0..0xFFFF)");
|
|
22811
22829
|
}
|
|
22830
|
+
function mkdirParents(dest) {
|
|
22831
|
+
const segs = dest.replace(/\//g, "\\").split("\\");
|
|
22832
|
+
let acc = "";
|
|
22833
|
+
for (let i2 = 0; i2 < segs.length - 1; i2++) {
|
|
22834
|
+
acc = acc === "" ? segs[i2] : acc + "\\" + segs[i2];
|
|
22835
|
+
if (acc === "" || /^[A-Za-z]:$/.test(acc))
|
|
22836
|
+
continue;
|
|
22837
|
+
try {
|
|
22838
|
+
os7.mkdir(acc);
|
|
22839
|
+
} catch {
|
|
22840
|
+
}
|
|
22841
|
+
}
|
|
22842
|
+
}
|
|
22812
22843
|
var server = createServer(async (req) => {
|
|
22813
22844
|
const t0 = Date.now();
|
|
22814
22845
|
const path = new URL(req.url).pathname;
|
|
22815
|
-
const isApi = req.method === "POST" && (path === "/exec" || path === "/set_resolution" || path === "/input") || req.method === "GET" && (path === "/window_list" || path === "/screenshot" || path === "/tray_list");
|
|
22846
|
+
const isApi = req.method === "POST" && (path === "/exec" || path === "/set_resolution" || path === "/input" || path === "/upload") || req.method === "GET" && (path === "/window_list" || path === "/screenshot" || path === "/tray_list" || path === "/download");
|
|
22816
22847
|
if (!isApi)
|
|
22817
22848
|
return usageResponse();
|
|
22818
22849
|
const denied = authCheck(req);
|
|
@@ -23031,6 +23062,103 @@ var server = createServer(async (req) => {
|
|
|
23031
23062
|
return jsonError(504, String(ex));
|
|
23032
23063
|
}
|
|
23033
23064
|
}
|
|
23065
|
+
if (req.method === "POST" && path === "/upload") {
|
|
23066
|
+
const bad = (msg) => {
|
|
23067
|
+
logReq(t0, "POST /upload", 400, msg);
|
|
23068
|
+
return jsonError(400, msg);
|
|
23069
|
+
};
|
|
23070
|
+
const params = new URL(req.url).searchParams;
|
|
23071
|
+
const dest = params.get("path");
|
|
23072
|
+
if (!dest)
|
|
23073
|
+
return bad("path=<\u76EE\u6807\u8DEF\u5F84>");
|
|
23074
|
+
let bytes;
|
|
23075
|
+
try {
|
|
23076
|
+
bytes = new Uint8Array(await req.arrayBuffer());
|
|
23077
|
+
} catch (ex) {
|
|
23078
|
+
return bad("bad body: " + String(ex));
|
|
23079
|
+
}
|
|
23080
|
+
if (bytes.length === 0)
|
|
23081
|
+
return bad("empty body");
|
|
23082
|
+
try {
|
|
23083
|
+
if (params.get("mkdir") !== "0")
|
|
23084
|
+
mkdirParents(dest);
|
|
23085
|
+
const tmp = dest + ".uploading";
|
|
23086
|
+
const f2 = std2.open(tmp, "wb");
|
|
23087
|
+
if (!f2)
|
|
23088
|
+
throw new Error("open for write failed: " + tmp);
|
|
23089
|
+
let w2 = 0;
|
|
23090
|
+
try {
|
|
23091
|
+
const CHUNK = 8 * 1024 * 1024;
|
|
23092
|
+
for (let off = 0; off < bytes.length; off += CHUNK) {
|
|
23093
|
+
const n2 = f2.write(bytes.buffer, off, Math.min(CHUNK, bytes.length - off));
|
|
23094
|
+
if (n2 <= 0)
|
|
23095
|
+
break;
|
|
23096
|
+
w2 += n2;
|
|
23097
|
+
}
|
|
23098
|
+
} finally {
|
|
23099
|
+
f2.close();
|
|
23100
|
+
}
|
|
23101
|
+
if (w2 !== bytes.length)
|
|
23102
|
+
throw new Error("short write: " + w2 + "/" + bytes.length);
|
|
23103
|
+
if (!MoveFileEx(
|
|
23104
|
+
tmp,
|
|
23105
|
+
dest,
|
|
23106
|
+
1
|
|
23107
|
+
/* MoveFileFlag.REPLACE_EXISTING */
|
|
23108
|
+
))
|
|
23109
|
+
throw new Error("MoveFileEx failed (GetLastError " + GetLastError() + "): " + tmp + " -> " + dest);
|
|
23110
|
+
} catch (ex) {
|
|
23111
|
+
logReq(t0, "POST /upload", 500, String(ex));
|
|
23112
|
+
return jsonError(500, String(ex));
|
|
23113
|
+
}
|
|
23114
|
+
logReq(t0, "POST /upload", 200, bytes.length + "B -> " + dest);
|
|
23115
|
+
return new Response(JSON.stringify({ bytes: bytes.length, path: dest }), { headers: { "Content-Type": "application/json" } });
|
|
23116
|
+
}
|
|
23117
|
+
if (req.method === "GET" && path === "/download") {
|
|
23118
|
+
const p2 = new URL(req.url).searchParams.get("path");
|
|
23119
|
+
if (!p2) {
|
|
23120
|
+
logReq(t0, "GET /download", 400, "path=<\u6587\u4EF6\u8DEF\u5F84>");
|
|
23121
|
+
return jsonError(400, "path=<\u6587\u4EF6\u8DEF\u5F84>");
|
|
23122
|
+
}
|
|
23123
|
+
let f2 = null;
|
|
23124
|
+
try {
|
|
23125
|
+
f2 = std2.open(p2, "rb");
|
|
23126
|
+
} catch (ex) {
|
|
23127
|
+
logReq(t0, "GET /download", 500, String(ex));
|
|
23128
|
+
return jsonError(500, String(ex));
|
|
23129
|
+
}
|
|
23130
|
+
if (f2 === null) {
|
|
23131
|
+
logReq(t0, "GET /download", 404, p2);
|
|
23132
|
+
return jsonError(404, "cannot open: " + p2);
|
|
23133
|
+
}
|
|
23134
|
+
try {
|
|
23135
|
+
const chunks = [];
|
|
23136
|
+
const buf = new Uint8Array(64 * 1024);
|
|
23137
|
+
let total = 0;
|
|
23138
|
+
for (; ; ) {
|
|
23139
|
+
const n2 = f2.read(buf.buffer, 0, buf.length);
|
|
23140
|
+
if (n2 <= 0)
|
|
23141
|
+
break;
|
|
23142
|
+
chunks.push(n2 === buf.length ? buf.slice() : buf.slice(0, n2));
|
|
23143
|
+
total += n2;
|
|
23144
|
+
}
|
|
23145
|
+
const all = new Uint8Array(total);
|
|
23146
|
+
let off = 0;
|
|
23147
|
+
for (const c2 of chunks) {
|
|
23148
|
+
all.set(c2, off);
|
|
23149
|
+
off += c2.length;
|
|
23150
|
+
}
|
|
23151
|
+
logReq(t0, "GET /download", 200, total + "B <- " + p2);
|
|
23152
|
+
return new Response(all, {
|
|
23153
|
+
headers: { "Content-Type": "application/octet-stream", "X-Bytes": String(total) }
|
|
23154
|
+
});
|
|
23155
|
+
} catch (ex) {
|
|
23156
|
+
logReq(t0, "GET /download", 500, String(ex));
|
|
23157
|
+
return jsonError(500, String(ex));
|
|
23158
|
+
} finally {
|
|
23159
|
+
f2.close();
|
|
23160
|
+
}
|
|
23161
|
+
}
|
|
23034
23162
|
return usageResponse();
|
|
23035
23163
|
});
|
|
23036
23164
|
var rc = server.listen(8080, "0.0.0.0");
|
package/lib/http-server.js
CHANGED
|
@@ -24,6 +24,11 @@ function concatU8(parts) {
|
|
|
24
24
|
return out;
|
|
25
25
|
}
|
|
26
26
|
function toArrayBuffer(u8) {
|
|
27
|
+
// 已经是完整 buffer(byteOffset=0、无尾部余量)时零复制返回——发送大
|
|
28
|
+
// body 的主路径,避免每片 send 前白拷一份。视图场景仍 slice 出独立副本
|
|
29
|
+
//(FFI 持有的 buffer 生命周期不受 view 约束)。
|
|
30
|
+
if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength)
|
|
31
|
+
return u8.buffer;
|
|
27
32
|
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
|
|
28
33
|
}
|
|
29
34
|
function findCrLfCrLf(buf) {
|
|
@@ -190,6 +195,7 @@ export class HttpServer {
|
|
|
190
195
|
const c = {
|
|
191
196
|
sock: ac.handle,
|
|
192
197
|
parts: [],
|
|
198
|
+
partsLen: 0,
|
|
193
199
|
pending: null,
|
|
194
200
|
current: null,
|
|
195
201
|
outQueue: null,
|
|
@@ -229,7 +235,9 @@ function onRead(c) {
|
|
|
229
235
|
const d = sock.recv(c.sock, 8192);
|
|
230
236
|
if (!d)
|
|
231
237
|
break;
|
|
232
|
-
|
|
238
|
+
const u8 = new Uint8Array(d);
|
|
239
|
+
c.parts.push(u8);
|
|
240
|
+
c.partsLen += u8.length;
|
|
233
241
|
}
|
|
234
242
|
if (!c.closed)
|
|
235
243
|
tryProcess(c);
|
|
@@ -334,9 +342,19 @@ async function writeStreamingBody(c, body, chunked, trailer) {
|
|
|
334
342
|
if (done)
|
|
335
343
|
break;
|
|
336
344
|
if (value && value.byteLength > 0) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
345
|
+
// 切成 ≤64KB 再入 queueSend:单发 200MB 级大 chunk 时
|
|
346
|
+
// toArrayBuffer 会全量 slice 复制,partial drain 又每次复制
|
|
347
|
+
// 剩余(O(n²)),XP 32 位 1GB 上直接把进程水位顶穿导致断连;
|
|
348
|
+
// 小片让每次复制/发送恒为常数大小,backpressure 语义不变。
|
|
349
|
+
const MAX_SEND = 65536;
|
|
350
|
+
for (let off = 0; off < value.byteLength; off += MAX_SEND) {
|
|
351
|
+
const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength));
|
|
352
|
+
const frame = chunked ? encodeChunkFrame(piece, enc) : piece;
|
|
353
|
+
if (!queueSend(c, frame)) {
|
|
354
|
+
await new Promise((resolve) => { c.resumeRead = resolve; });
|
|
355
|
+
}
|
|
356
|
+
if (c.closed)
|
|
357
|
+
break;
|
|
340
358
|
}
|
|
341
359
|
}
|
|
342
360
|
}
|
|
@@ -383,38 +401,43 @@ function whole(c) {
|
|
|
383
401
|
function trimParts(c, buf, consumed) {
|
|
384
402
|
if (consumed >= buf.length) {
|
|
385
403
|
c.parts = [];
|
|
404
|
+
c.partsLen = 0;
|
|
386
405
|
}
|
|
387
406
|
else {
|
|
388
407
|
c.parts = [buf.subarray(consumed)];
|
|
408
|
+
c.partsLen = buf.length - consumed;
|
|
389
409
|
}
|
|
390
410
|
}
|
|
391
411
|
function tryProcess(c) {
|
|
392
412
|
if (c.closed || c.current)
|
|
393
413
|
return;
|
|
394
|
-
const buf = whole(c);
|
|
395
414
|
if (c.pending) {
|
|
396
415
|
const bodyStart = c.pending.bodyStart;
|
|
397
416
|
const parsed = c.pending.parsed;
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
const
|
|
402
|
-
if (
|
|
403
|
-
return;
|
|
404
|
-
body = dec.data;
|
|
405
|
-
end = dec.end;
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
const needed = parsed.contentLength;
|
|
409
|
-
if (bodyStart + needed > buf.length)
|
|
417
|
+
if (!parsed.chunked) {
|
|
418
|
+
// O(1) length check while the body is still arriving — only
|
|
419
|
+
// concatenate once, when the last byte is in.
|
|
420
|
+
const end = bodyStart + parsed.contentLength;
|
|
421
|
+
if (end > c.partsLen)
|
|
410
422
|
return;
|
|
411
|
-
|
|
412
|
-
|
|
423
|
+
const buf = whole(c);
|
|
424
|
+
c.pending = null;
|
|
425
|
+
dispatchRequest(c, parsed, buf.subarray(bodyStart, end), end, buf);
|
|
426
|
+
return;
|
|
413
427
|
}
|
|
428
|
+
// chunked: no total length up front, so a decode attempt still needs
|
|
429
|
+
// the concatenated view (re-tried per read event as before).
|
|
430
|
+
const buf = whole(c);
|
|
431
|
+
const dec = decodeChunked(buf, bodyStart);
|
|
432
|
+
if (!dec)
|
|
433
|
+
return;
|
|
414
434
|
c.pending = null;
|
|
415
|
-
dispatchRequest(c, parsed,
|
|
435
|
+
dispatchRequest(c, parsed, dec.data, dec.end, buf);
|
|
416
436
|
return;
|
|
417
437
|
}
|
|
438
|
+
// Header phase: bodies are checked against `partsLen` above, so a full
|
|
439
|
+
// concatenation is only needed here — once per request, not per read.
|
|
440
|
+
const buf = whole(c);
|
|
418
441
|
const headerEnd = findCrLfCrLf(buf);
|
|
419
442
|
if (headerEnd < 0)
|
|
420
443
|
return;
|
|
@@ -432,20 +455,20 @@ function tryProcess(c) {
|
|
|
432
455
|
c.pending = { parsed, bodyStart };
|
|
433
456
|
return;
|
|
434
457
|
}
|
|
435
|
-
dispatchRequest(c, parsed, dec.data, dec.end);
|
|
458
|
+
dispatchRequest(c, parsed, dec.data, dec.end, buf);
|
|
436
459
|
}
|
|
437
460
|
else if (parsed.contentLength > 0) {
|
|
438
461
|
if (bodyStart + parsed.contentLength > buf.length) {
|
|
439
462
|
c.pending = { parsed, bodyStart };
|
|
440
463
|
return;
|
|
441
464
|
}
|
|
442
|
-
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength);
|
|
465
|
+
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf);
|
|
443
466
|
}
|
|
444
467
|
else {
|
|
445
|
-
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart);
|
|
468
|
+
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart, buf);
|
|
446
469
|
}
|
|
447
470
|
}
|
|
448
|
-
function dispatchRequest(c, parsed, body, end) {
|
|
471
|
+
function dispatchRequest(c, parsed, body, end, wholeBuf) {
|
|
449
472
|
const connHeader = (parsed.headers['connection'] || '').toLowerCase();
|
|
450
473
|
if (parsed.version === 'HTTP/1.1')
|
|
451
474
|
c.keepAlive = connHeader.indexOf('close') < 0;
|
|
@@ -468,7 +491,9 @@ function dispatchRequest(c, parsed, body, end) {
|
|
|
468
491
|
});
|
|
469
492
|
c.ac = ac;
|
|
470
493
|
c.current = { req };
|
|
471
|
-
|
|
494
|
+
// `wholeBuf` is the concatenation the caller already produced — reuse it
|
|
495
|
+
// instead of concatenating the very same parts a second time.
|
|
496
|
+
trimParts(c, wholeBuf ?? whole(c), end);
|
|
472
497
|
void handleRequest(c);
|
|
473
498
|
}
|
|
474
499
|
async function handleRequest(c) {
|
package/lib/http-server.ts
CHANGED
|
@@ -35,6 +35,10 @@ interface ActiveReq {
|
|
|
35
35
|
interface Conn {
|
|
36
36
|
sock: SockHandle
|
|
37
37
|
parts: Uint8Array[]
|
|
38
|
+
/** Cached total length of `parts` — lets the pending-body wait check
|
|
39
|
+
* "have we received enough yet" in O(1) instead of re-concatenating
|
|
40
|
+
* every received byte on each read event (O(n²) for large bodies). */
|
|
41
|
+
partsLen: number
|
|
38
42
|
pending: PendingReq | null
|
|
39
43
|
current: ActiveReq | null
|
|
40
44
|
outQueue: Uint8Array | null
|
|
@@ -69,6 +73,10 @@ function concatU8(parts: Uint8Array[]): Uint8Array {
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
function toArrayBuffer(u8: Uint8Array): ArrayBuffer {
|
|
76
|
+
// 已经是完整 buffer(byteOffset=0、无尾部余量)时零复制返回——发送大
|
|
77
|
+
// body 的主路径,避免每片 send 前白拷一份。视图场景仍 slice 出独立副本
|
|
78
|
+
//(FFI 持有的 buffer 生命周期不受 view 约束)。
|
|
79
|
+
if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength) return u8.buffer as ArrayBuffer
|
|
72
80
|
return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength) as ArrayBuffer
|
|
73
81
|
}
|
|
74
82
|
|
|
@@ -213,6 +221,7 @@ export class HttpServer {
|
|
|
213
221
|
const c: Conn = {
|
|
214
222
|
sock: ac.handle,
|
|
215
223
|
parts: [],
|
|
224
|
+
partsLen: 0,
|
|
216
225
|
pending: null,
|
|
217
226
|
current: null,
|
|
218
227
|
outQueue: null,
|
|
@@ -251,7 +260,9 @@ function onRead(c: Conn): void {
|
|
|
251
260
|
while (true) {
|
|
252
261
|
const d = sock.recv(c.sock, 8192)
|
|
253
262
|
if (!d) break
|
|
254
|
-
|
|
263
|
+
const u8 = new Uint8Array(d)
|
|
264
|
+
c.parts.push(u8)
|
|
265
|
+
c.partsLen += u8.length
|
|
255
266
|
}
|
|
256
267
|
if (!c.closed) tryProcess(c)
|
|
257
268
|
}
|
|
@@ -350,9 +361,18 @@ async function writeStreamingBody(c: Conn, body: ReadableStream<Uint8Array> | nu
|
|
|
350
361
|
const { done, value } = await reader.read()
|
|
351
362
|
if (done) break
|
|
352
363
|
if (value && value.byteLength > 0) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
364
|
+
// 切成 ≤64KB 再入 queueSend:单发 200MB 级大 chunk 时
|
|
365
|
+
// toArrayBuffer 会全量 slice 复制,partial drain 又每次复制
|
|
366
|
+
// 剩余(O(n²)),XP 32 位 1GB 上直接把进程水位顶穿导致断连;
|
|
367
|
+
// 小片让每次复制/发送恒为常数大小,backpressure 语义不变。
|
|
368
|
+
const MAX_SEND = 65536
|
|
369
|
+
for (let off = 0; off < value.byteLength; off += MAX_SEND) {
|
|
370
|
+
const piece = value.byteLength <= MAX_SEND ? value : value.subarray(off, Math.min(off + MAX_SEND, value.byteLength))
|
|
371
|
+
const frame = chunked ? encodeChunkFrame(piece, enc) : piece
|
|
372
|
+
if (!queueSend(c, frame)) {
|
|
373
|
+
await new Promise<void>((resolve) => { c.resumeRead = resolve })
|
|
374
|
+
}
|
|
375
|
+
if (c.closed) break
|
|
356
376
|
}
|
|
357
377
|
}
|
|
358
378
|
}
|
|
@@ -396,36 +416,42 @@ function whole(c: Conn): Uint8Array {
|
|
|
396
416
|
function trimParts(c: Conn, buf: Uint8Array, consumed: number): void {
|
|
397
417
|
if (consumed >= buf.length) {
|
|
398
418
|
c.parts = []
|
|
419
|
+
c.partsLen = 0
|
|
399
420
|
} else {
|
|
400
421
|
c.parts = [buf.subarray(consumed)]
|
|
422
|
+
c.partsLen = buf.length - consumed
|
|
401
423
|
}
|
|
402
424
|
}
|
|
403
425
|
|
|
404
426
|
function tryProcess(c: Conn): void {
|
|
405
427
|
if (c.closed || c.current) return
|
|
406
|
-
const buf = whole(c)
|
|
407
428
|
|
|
408
429
|
if (c.pending) {
|
|
409
430
|
const bodyStart = c.pending.bodyStart
|
|
410
431
|
const parsed = c.pending.parsed
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
if (
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
if (bodyStart + needed > buf.length) return
|
|
421
|
-
body = buf.subarray(bodyStart, bodyStart + needed)
|
|
422
|
-
end = bodyStart + needed
|
|
432
|
+
if (!parsed.chunked) {
|
|
433
|
+
// O(1) length check while the body is still arriving — only
|
|
434
|
+
// concatenate once, when the last byte is in.
|
|
435
|
+
const end = bodyStart + parsed.contentLength
|
|
436
|
+
if (end > c.partsLen) return
|
|
437
|
+
const buf = whole(c)
|
|
438
|
+
c.pending = null
|
|
439
|
+
dispatchRequest(c, parsed, buf.subarray(bodyStart, end), end, buf)
|
|
440
|
+
return
|
|
423
441
|
}
|
|
442
|
+
// chunked: no total length up front, so a decode attempt still needs
|
|
443
|
+
// the concatenated view (re-tried per read event as before).
|
|
444
|
+
const buf = whole(c)
|
|
445
|
+
const dec = decodeChunked(buf, bodyStart)
|
|
446
|
+
if (!dec) return
|
|
424
447
|
c.pending = null
|
|
425
|
-
dispatchRequest(c, parsed,
|
|
448
|
+
dispatchRequest(c, parsed, dec.data, dec.end, buf)
|
|
426
449
|
return
|
|
427
450
|
}
|
|
428
451
|
|
|
452
|
+
// Header phase: bodies are checked against `partsLen` above, so a full
|
|
453
|
+
// concatenation is only needed here — once per request, not per read.
|
|
454
|
+
const buf = whole(c)
|
|
429
455
|
const headerEnd = findCrLfCrLf(buf)
|
|
430
456
|
if (headerEnd < 0) return
|
|
431
457
|
const absEnd = headerEnd + 4
|
|
@@ -440,19 +466,19 @@ function tryProcess(c: Conn): void {
|
|
|
440
466
|
c.pending = { parsed, bodyStart }
|
|
441
467
|
return
|
|
442
468
|
}
|
|
443
|
-
dispatchRequest(c, parsed, dec.data, dec.end)
|
|
469
|
+
dispatchRequest(c, parsed, dec.data, dec.end, buf)
|
|
444
470
|
} else if (parsed.contentLength > 0) {
|
|
445
471
|
if (bodyStart + parsed.contentLength > buf.length) {
|
|
446
472
|
c.pending = { parsed, bodyStart }
|
|
447
473
|
return
|
|
448
474
|
}
|
|
449
|
-
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength)
|
|
475
|
+
dispatchRequest(c, parsed, buf.subarray(bodyStart, bodyStart + parsed.contentLength), bodyStart + parsed.contentLength, buf)
|
|
450
476
|
} else {
|
|
451
|
-
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart)
|
|
477
|
+
dispatchRequest(c, parsed, new Uint8Array(0), bodyStart, buf)
|
|
452
478
|
}
|
|
453
479
|
}
|
|
454
480
|
|
|
455
|
-
function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number): void {
|
|
481
|
+
function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: number, wholeBuf?: Uint8Array): void {
|
|
456
482
|
const connHeader = (parsed.headers['connection'] || '').toLowerCase()
|
|
457
483
|
if (parsed.version === 'HTTP/1.1') c.keepAlive = connHeader.indexOf('close') < 0
|
|
458
484
|
else c.keepAlive = connHeader.indexOf('keep-alive') >= 0
|
|
@@ -477,7 +503,9 @@ function dispatchRequest(c: Conn, parsed: ParsedHeader, body: Uint8Array, end: n
|
|
|
477
503
|
|
|
478
504
|
c.ac = ac
|
|
479
505
|
c.current = { req }
|
|
480
|
-
|
|
506
|
+
// `wholeBuf` is the concatenation the caller already produced — reuse it
|
|
507
|
+
// instead of concatenating the very same parts a second time.
|
|
508
|
+
trimParts(c, wholeBuf ?? whole(c), end)
|
|
481
509
|
void handleRequest(c)
|
|
482
510
|
}
|
|
483
511
|
|
package/package.json
CHANGED
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
|