@mrrisega/dsh-remote 0.7.3-beta.1 → 0.7.3-beta.2
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/clients/dsh-remote/dsh-bridge.mjs +214 -9
- package/clients/dsh-remote/e2ee-shim-script.js +98 -18
- package/clients/dsh-remote/mirror-diag-script.js +19 -0
- package/clients/dsh-remote/mobile-adapter.mjs +552 -16
- package/clients/dsh-remote/test/bridge-guard.test.mjs +169 -0
- package/clients/dsh-remote/test/e2ee-shim-selfheal.test.mjs +264 -0
- package/clients/dsh-remote/test/mirror-diag.test.mjs +75 -0
- package/clients/dsh-remote/test/mobile-adapter-copy.test.mjs +56 -0
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +87 -8
- package/clients/dsh-remote/test/mobile-adapter-late-frame.test.mjs +9 -0
- package/clients/dsh-remote/test/mobile-adapter-load-watch.test.mjs +233 -28
- package/clients/dsh-remote/upstream-discovery.mjs +6 -1
- package/dsh-setup.mjs +225 -6
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/index.js +140 -3
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +214 -9
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/e2ee-shim-script.js +98 -18
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mirror-diag-script.js +19 -0
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +552 -16
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/upstream-discovery.mjs +6 -1
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +225 -6
- package/packages/dsh-remote-web/test/bridge-hardening.test.mjs +299 -0
- package/packages/dsh-remote-web/test/windows-compat.test.mjs +82 -12
|
@@ -38,6 +38,10 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
|
38
38
|
const INDEX_SRC = readFileSync(path.join(HERE, "..", "lib", "index.js"), "utf8").replace(/\r\n/g, "\n"); // 本文件就是 Windows 专项:CRLF 检出时源码锚点必须仍然匹配
|
|
39
39
|
const CLIENT_SRC = readFileSync(path.join(HERE, "..", "lib", "client.js"), "utf8").replace(/\r\n/g, "\n");
|
|
40
40
|
const SETUP_SRC = readFileSync(path.join(HERE, "..", "..", "..", "dsh-setup.mjs"), "utf8").replace(/\r\n/g, "\n");
|
|
41
|
+
// C1 黑窗不变量要覆盖"所有可能执行控制台程序的子进程",所以 bridge / 上游探测也要读进来
|
|
42
|
+
//(这两处正是 2026-10-20 诊断里漏掉 windowsHide 的两个真实回归点)。
|
|
43
|
+
const BRIDGE_SRC = readFileSync(path.join(HERE, "..", "..", "..", "clients", "dsh-remote", "dsh-bridge.mjs"), "utf8").replace(/\r\n/g, "\n");
|
|
44
|
+
const UPSTREAM_SRC = readFileSync(path.join(HERE, "..", "..", "..", "clients", "dsh-remote", "upstream-discovery.mjs"), "utf8").replace(/\r\n/g, "\n");
|
|
41
45
|
|
|
42
46
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
43
47
|
|
|
@@ -356,6 +360,33 @@ test("win32:启动 bridge 走 detached watcher(dsh-setup.mjs run),并落
|
|
|
356
360
|
}
|
|
357
361
|
});
|
|
358
362
|
|
|
363
|
+
test("win32:插件拉起 watcher 时必须显式传 DSH_RELAY_DIR(否则守护落在 ~/.dsh-remote)", async () => {
|
|
364
|
+
// C7 / 诊断报告 00 §6.2 + 1415 那台设备:relayDir 可能来自 config.relayDir(设置里改过目录),
|
|
365
|
+
// 而 spawnEnv 只"继承"宿主的 DSH_RELAY_DIR —— 宿主环境没有该变量时,watcher 会回落到
|
|
366
|
+
// ~/.dsh-remote:面板表现为「守护在跑、bridge 永远不出现」,pid 文件/日志全写去了另一个目录。
|
|
367
|
+
// 这里用"假 dsh-setup.mjs 把收到的 env 落盘"来证明**真的传进去了**,而不是只看源码像不像。
|
|
368
|
+
const env = await makeEnv({ forceWin: true, skipService: false });
|
|
369
|
+
const dump = path.join(env.root, "watcher-env.json");
|
|
370
|
+
await writeFile(path.join(env.relayDir, "dsh-setup.mjs"),
|
|
371
|
+
`import fs from "node:fs";\nfs.writeFileSync(${JSON.stringify(dump)}, JSON.stringify(process.env));\n`);
|
|
372
|
+
const routes = boot(env.relayDir);
|
|
373
|
+
const srv = await serve(routes);
|
|
374
|
+
try {
|
|
375
|
+
await writeConfig(env.relayDir);
|
|
376
|
+
const r = await post(srv.base, "/dsh-remote/start");
|
|
377
|
+
assert.ok([200, 500].includes(r.status), `不得 5xx 崩溃:${r.status}`);
|
|
378
|
+
for (let i = 0; i < 80 && !existsSync(dump); i += 1) await sleep(50);
|
|
379
|
+
assert.ok(existsSync(dump), "假 dsh-setup.mjs 没有被拉起(watcher spawn 没发生)");
|
|
380
|
+
const spawned = JSON.parse(readFileSync(dump, "utf8"));
|
|
381
|
+
assert.equal(spawned.DSH_RELAY_DIR, env.relayDir,
|
|
382
|
+
"★watcher 的 env 必须显式带 DSH_RELAY_DIR=当前 relayDir(缺了它就会落到 ~/.dsh-remote)");
|
|
383
|
+
} finally {
|
|
384
|
+
await srv.close();
|
|
385
|
+
routes.dispose();
|
|
386
|
+
await env.restore();
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
359
390
|
test("win32:停止 bridge 只结束 pid 文件里的进程,不碰 launchctl/systemctl", async () => {
|
|
360
391
|
const env = await makeEnv({ forceWin: true, skipService: true });
|
|
361
392
|
const routes = boot(env.relayDir);
|
|
@@ -604,14 +635,48 @@ test("win32:注册的是 wscript,且隐藏方式不可用时回退直接启
|
|
|
604
635
|
assert.ok(/schtasks\(\["\/Delete", "\/TN", WIN_TASK_NAME, "\/F"\]\)/.test(fn), "必须先删旧任务(迁移可见任务)");
|
|
605
636
|
});
|
|
606
637
|
|
|
607
|
-
test("win32
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
638
|
+
test("win32:所有可能执行控制台程序的 spawn/execFileSync/spawnSync 都必须带 windowsHide", () => {
|
|
639
|
+
// 根因(2026-10-20 诊断 W1,生产截图 04):
|
|
640
|
+
// · detached 子进程(DETACHED_PROCESS)**不继承父控制台**;
|
|
641
|
+
// · 非 detached 子进程,若**父进程本身没有控制台**(launchd/任务计划 wscript/插件的
|
|
642
|
+
// detached spawn 拉起的 watcher 就是这种),Windows 也会给它**新分配一个可见控制台窗口**。
|
|
643
|
+
// · execFileSync/spawnSync 跑 netstat/lsof/ps/powershell 这类控制台程序同理(闪现的黑窗)。
|
|
644
|
+
// 只有 windowsHide(CREATE_NO_WINDOW)能压掉它 → 必须**全量**扫描,不能只看 detached。
|
|
645
|
+
//
|
|
646
|
+
// 这正是之前测试漏掉的两个真实 bug:dsh-setup.mjs 的 bridge 子进程 spawn(非 detached)
|
|
647
|
+
// 与 upstream-discovery.mjs 的 tryExec(execFileSync)。本用例在旧代码上必须变红。
|
|
648
|
+
const files = {
|
|
649
|
+
"dsh-setup.mjs": SETUP_SRC,
|
|
650
|
+
"lib/index.js": INDEX_SRC,
|
|
651
|
+
"clients/dsh-remote/dsh-bridge.mjs": BRIDGE_SRC,
|
|
652
|
+
"clients/dsh-remote/upstream-discovery.mjs": UPSTREAM_SRC,
|
|
653
|
+
};
|
|
654
|
+
// POSIX 专属命令:Windows 上根本不会执行,不适用(sh/ps/pgrep/lsof/ioreg/ss/launchctl/systemctl)。
|
|
655
|
+
const POSIX_ONLY = /\/bin\/(sh|bash)|(^|[^\w/])(sh|ps|pgrep|lsof|ioreg|ss|launchctl|systemctl)\b/;
|
|
656
|
+
/** 把注释抹成空格(保留换行 → 行号不变):注释里提到的 spawn("npx.cmd") 不该被当成真实调用。 */
|
|
657
|
+
const stripComments = (src) => {
|
|
658
|
+
let out = "";
|
|
659
|
+
let i = 0;
|
|
660
|
+
while (i < src.length) {
|
|
661
|
+
const ch = src[i];
|
|
662
|
+
if (ch === "/" && src[i + 1] === "/") {
|
|
663
|
+
while (i < src.length && src[i] !== "\n") { out += " "; i += 1; }
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
if (ch === "/" && src[i + 1] === "*") {
|
|
667
|
+
out += " "; i += 2;
|
|
668
|
+
while (i < src.length && !(src[i] === "*" && src[i + 1] === "/")) { out += src[i] === "\n" ? "\n" : " "; i += 1; }
|
|
669
|
+
if (i < src.length) { out += " "; i += 2; }
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
out += ch; i += 1;
|
|
673
|
+
}
|
|
674
|
+
return out;
|
|
675
|
+
};
|
|
612
676
|
let checked = 0;
|
|
613
|
-
for (const [name,
|
|
614
|
-
const
|
|
677
|
+
for (const [name, rawSrc] of Object.entries(files)) {
|
|
678
|
+
const src = stripComments(rawSrc);
|
|
679
|
+
const re = /\b(spawn|spawnSync|execFileSync|execSync|execFile)\s*\(/g;
|
|
615
680
|
let m;
|
|
616
681
|
while ((m = re.exec(src))) {
|
|
617
682
|
let i = re.lastIndex - 1;
|
|
@@ -622,14 +687,19 @@ test("win32:所有 detached 子进程都必须带 windowsHide(否则必弹
|
|
|
622
687
|
else if (src[i] === ")") { depth -= 1; if (!depth) { end = i; break; } }
|
|
623
688
|
}
|
|
624
689
|
const call = src.slice(m.index, end + 1);
|
|
625
|
-
if (
|
|
626
|
-
// POSIX 专属(/bin/sh、launchctl…)在 Windows 上根本不会执行,不适用
|
|
627
|
-
if (/\/bin\/sh|\/bin\/bash|launchctl|systemctl/.test(call)) continue;
|
|
690
|
+
if (POSIX_ONLY.test(call)) continue; // 只在 POSIX 上存在的命令
|
|
628
691
|
checked += 1;
|
|
629
692
|
const lineNo = src.slice(0, m.index).split("\n").length;
|
|
630
693
|
assert.ok(/windowsHide:\s*true/.test(call),
|
|
631
|
-
`${name}:${lineNo} 的
|
|
694
|
+
`${name}:${lineNo} 的 ${m[1]} 缺 windowsHide → Windows 上会弹/闪黑色控制台窗口`);
|
|
632
695
|
}
|
|
633
696
|
}
|
|
634
|
-
|
|
697
|
+
// 回归点名(比上面的通用扫描给出更直白的失败信息):这两处就是诊断报告里的 W1 根因
|
|
698
|
+
const bridgeSpawnAt = SETUP_SRC.indexOf('clients/dsh-remote/dsh-bridge.mjs"');
|
|
699
|
+
assert.ok(bridgeSpawnAt > 0, "dsh-setup.mjs 里应能定位到 bridge 子进程的 spawn");
|
|
700
|
+
const bridgeCall = SETUP_SRC.slice(SETUP_SRC.lastIndexOf("spawn(", bridgeSpawnAt), SETUP_SRC.indexOf(");", bridgeSpawnAt) + 2);
|
|
701
|
+
assert.match(bridgeCall, /windowsHide:\s*true/, "dsh-setup.mjs 的 bridge 子进程 spawn 必须带 windowsHide(空白黑窗根因)");
|
|
702
|
+
assert.match(UPSTREAM_SRC.replace(/\r\n/g, "\n"), /execFileSync\(cmd, args, \{[^}]*windowsHide:\s*true/,
|
|
703
|
+
"upstream-discovery.mjs 的 tryExec 必须带 windowsHide(netstat 闪窗根因)");
|
|
704
|
+
assert.ok(checked >= 12, `至少应检查到 12 处子进程调用,实际 ${checked}(切片/匹配可能失效)`);
|
|
635
705
|
});
|