@1e0zj/dsh-plugin-mall 0.3.5 → 0.4.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.md +6 -6
- package/package.json +1 -1
- package/src/cli.js +160 -10
- package/src/guard.js +1576 -76
- package/src/index.js +75 -4
- package/src/installer.js +13 -5
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@ Two surfaces: a **Settings → Plugins → Marketplace** tab in the dsh web UI,
|
|
|
11
11
|
Curated lists only show what has been reviewed and merged. This marketplace is open by construction: **any repo tagged `topic:dsh-plugin` is discoverable the moment it is pushed** — no submission, no approval queue. To keep that openness usable:
|
|
12
12
|
|
|
13
13
|
- **Automatic verification** — every search result's `package.json` is fetched (jsDelivr/raw dual-source CDN, no API quota) and checked for the official `dsh.bundle` / `dsh.client` manifest. Verified plugins get a green badge; the default "verified only" view filters out ~73% of topic noise (empty repos and unrelated projects riding the tag).
|
|
14
|
-
- **Browse-time compatibility badges** — each card is also statically scanned against your profile before you click anything: declared conflicts, exclusive groups, loader-id collisions (from the repo's patch file), host-module shadowing, and peer/Node/OS ranges. Cards show 适配 / 有风险 / 冲突 / 适配未知 accordingly — advisory only; the install preflight remains the enforcing gate.
|
|
14
|
+
- **Browse-time compatibility badges** — each card is also statically scanned against your profile before you click anything: declared conflicts, exclusive groups, loader-id collisions (from the repo's patch file), rows the candidate's patch would switch off or reconfigure in the profile you already run, host-module shadowing, and peer/Node/OS ranges. Cards show 适配 / 有风险 / 冲突 / 适配未知 accordingly — advisory only; the install preflight remains the enforcing gate.
|
|
15
15
|
- **Anti-squatting** — an install prefers the npm tarball only when the registry entry's `repository` URL points back to the same GitHub repo; anything else falls back to the explicit `github:` spec.
|
|
16
16
|
- **npm-first installs** — registry tarballs are smaller than whole-repo GitHub downloads and come with integrity checks. Lookups follow the registry pnpm actually installs from (profile `.npmrc` → `pnpm config get registry` → npmjs), so a mirror user keeps npm-first instead of silently falling back to whole-repo clones.
|
|
17
17
|
- **Update management** — installed plugins are compared against the registry `latest`; one-click update per plugin.
|
|
18
|
-
- **Conflict guard** — every install runs an isolated preflight first: the candidate is installed with scripts disabled into a throwaway directory and scanned against the live profile for loader-id collisions, double mounts, host-module shadowing and version/OS/peer ranges. A hard conflict is blocked, warnings require explicit confirmation. The profile's load-bearing files are snapshotted before `pnpm` touches them and restored on failure. A pending install is resolved on the next start, however you start it: this plugin runs recovery as it loads, which only happens because dsh booted far enough to compose the profile. Starting through `guard launch` adds a grace window on top, so a plugin that boots and then crashes seconds later is rolled back and restarted once (see [Startup protection](#startup-protection-guard-cli)).
|
|
19
|
-
- **Resilience** — rate-limit circuit breaker, GitHub's 1000-result search window handled gracefully, `corepack enable pnpm` self-heal when pnpm is missing, one-click dsh restart (loopback-only, `allowRestart: false` to disable).
|
|
18
|
+
- **Conflict guard** — every install runs an isolated preflight first: the candidate is installed with scripts disabled into a throwaway directory and scanned against the live profile for loader-id collisions, double mounts, host-module shadowing and version/OS/peer ranges. A patch layer is not read entry by entry but composed the way the loader applies it — the profile is assembled with and without the candidate's layer, in the position dsh would apply it, and the two trees are diffed. That composition is checked against the loader's own `applyEntryPatches` on every test run, over fixed shapes and 300 generated patch combinations, so "what dsh would boot" is not a guess. What gets reported is what would actually change: disabling, re-enabling or replacing the config of someone else's rows warns and names them, past ten such rows the candidate is a rival composition rather than an addition and is blocked, and a bundle that is itself another front door (own `bin`, terminal peers, no browser half) is blocked once it switches rows on or off or rewrites a protection row — the fingerprint corroborates, it never blocks on its own. A hard conflict is blocked, warnings require explicit confirmation. The profile's load-bearing files are snapshotted before `pnpm` touches them and restored on failure. A pending install is resolved on the next start, however you start it: this plugin runs recovery as it loads, which only happens because dsh booted far enough to compose the profile. Starting through `guard launch` adds a grace window on top, so a plugin that boots and then crashes seconds later is rolled back and restarted once (see [Startup protection](#startup-protection-guard-cli)).
|
|
19
|
+
- **Resilience** — rate-limit circuit breaker, GitHub's 1000-result search window handled gracefully, `corepack enable pnpm` self-heal when pnpm is missing, one-click dsh restart (loopback-only, `allowRestart: false` to disable). The successor waits for the outgoing host to be gone before it binds — starting into an occupied port would read as "the pending plugin crashed dsh" and roll back an install that was fine. It leaves no stray console behind and does not re-open the browser, because the page that asked for the restart is still there and reconnects on its own; whatever the restart prints lands in `<home>/guard/restart-<profile>.log`.
|
|
20
20
|
|
|
21
21
|
## Install
|
|
22
22
|
|
|
@@ -149,12 +149,12 @@ pnpm --dir <profile> add "github:omdsh-dev/dsh-at-file" --ignore-scripts
|
|
|
149
149
|
与策展列表不同:**任何打上 `topic:dsh-plugin` 的仓库推送后立即可被发现**——无需投稿、无需审批。为保证开放性可用,做了这些事:
|
|
150
150
|
|
|
151
151
|
- **自动验证**:逐仓库拉取 `package.json`(jsDelivr/raw 双源 CDN,不占 API 配额),按官方 `dsh.bundle` / `dsh.client` 声明打徽章;默认"只看已验证"视图过滤约 73% 的话题噪音
|
|
152
|
-
- **浏览期适配徽章**:点安装之前,每张卡片就已对照你的 profile 做过一次静态扫描——声明冲突、独占组、loader-id
|
|
152
|
+
- **浏览期适配徽章**:点安装之前,每张卡片就已对照你的 profile 做过一次静态扫描——声明冲突、独占组、loader-id 冲突(取自仓库补丁文件)、候选包的补丁会停用或改写你当前 profile 里的哪些行、宿主模块遮蔽、peer/Node/OS 范围;卡片上直接显示 适配 / 有风险 / 冲突 / 适配未知。徽章只是提示,真正的拦截闸门仍是安装预检
|
|
153
153
|
- **防抢注**:仅当 npm registry 条目的 `repository` 指回同一 GitHub 仓库时才用 npm 安装,否则回退 `github:` 源
|
|
154
154
|
- **npm 优先安装**:registry tarball 比整仓库下载更小且带完整性校验;查询用的 registry 跟随 pnpm 实际安装源(profile `.npmrc` → `pnpm config get registry` → npmjs),换了镜像也不会退化成整仓库克隆
|
|
155
155
|
- **更新管理**:已装插件与 registry `latest` 比对,逐个一键更新
|
|
156
|
-
- **冲突防护**:每次安装先跑隔离预检——候选包在一次性目录里以禁用脚本的方式装好后,对照 live profile 扫描 loader-id 冲突、重复挂载、宿主模块遮蔽和版本/OS/peer
|
|
157
|
-
- **工程韧性**:限流熔断、GitHub 5xx/超时退避重试(504 瞬时故障不再直达用户)、GitHub 1000 条搜索上限优雅处理、pnpm 缺失时 `corepack` 自愈、一键重启 dsh(仅 loopback,可 `allowRestart: false`
|
|
156
|
+
- **冲突防护**:每次安装先跑隔离预检——候选包在一次性目录里以禁用脚本的方式装好后,对照 live profile 扫描 loader-id 冲突、重复挂载、宿主模块遮蔽和版本/OS/peer 范围。补丁不是逐条读,而是**按 loader 的方式组装**——把候选包的层放进 dsh 会放的位置,装前装后各组装一棵树再逐行比对。这套组装每次跑测试都会和 loader 自己的 `applyEntryPatches` 对拍(固定形状 + 300 组随机生成的补丁组合),所以「dsh 会装出什么树」不是猜的。报出来的是真正会变的东西:停用、重新启用、或替换别人整块 config,都点名是哪几条 id 并警告;超过十条就不是叠加而是另一套组合,直接拦截;候选包本身就是另一套门面(自带 bin、依赖终端栈、没有浏览器半边)时,只要它停用/启用了行、或改了沙箱审批这类保护行,就直接拦截——指纹只做佐证,单凭它不拦。硬冲突直接拦截,警告需显式确认。安装前给 profile 的承重文件拍快照、失败即回滚;pending 安装在下次启动时自动了结,**不挑启动方式**:本插件加载时就跑恢复,而能加载本身就证明 dsh 已经组装好 profile、活到了这一步。经 `guard launch` 启动则多一层观察期——插件启动几秒后才崩的情况也能回滚并原样重启一次(见下方「启动保护」)。
|
|
157
|
+
- **工程韧性**:限流熔断、GitHub 5xx/超时退避重试(504 瞬时故障不再直达用户)、GitHub 1000 条搜索上限优雅处理、pnpm 缺失时 `corepack` 自愈、一键重启 dsh(仅 loopback,可 `allowRestart: false` 关闭)。重启会**等旧进程真正退出**再绑端口——抢在端口没释放时启动,会被判成「新装的插件把 dsh 搞崩了」,把一次本来正常的安装回滚掉。重启后不留控制台窗口、也不再重开浏览器:发起重启的那个页面还在,会自己重连;重启过程的输出写进 `<home>/guard/restart-<profile>.log`(想停 dsh 用任务管理器结束 node 进程)
|
|
158
158
|
|
|
159
159
|
## 安装
|
|
160
160
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -637,6 +637,42 @@ function resolveWindowsCommand(command) {
|
|
|
637
637
|
return command;
|
|
638
638
|
}
|
|
639
639
|
|
|
640
|
+
/** How long `--await-exit` waits for the outgoing host before giving up. */
|
|
641
|
+
const AWAIT_EXIT_TIMEOUT_MS = 30000;
|
|
642
|
+
/** Settle time after the pid is gone, before the successor binds the port. */
|
|
643
|
+
const PORT_SETTLE_MS = 400;
|
|
644
|
+
|
|
645
|
+
const delay = (ms) => new Promise((resolvePromise) => { setTimeout(resolvePromise, ms); });
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Poll until `pid` is gone, or the timeout elapses.
|
|
649
|
+
*
|
|
650
|
+
* `kill(pid, 0)` is the portable liveness probe: it delivers no signal and
|
|
651
|
+
* throws ESRCH once the process is gone. EPERM means it exists under another
|
|
652
|
+
* owner — still alive, so keep waiting rather than racing it.
|
|
653
|
+
*
|
|
654
|
+
* A pid is only meaningful because the caller is the process that spawned us
|
|
655
|
+
* and named itself. It can still be recycled in principle; the bounded wait
|
|
656
|
+
* and the refusal on timeout keep that from turning into a hang.
|
|
657
|
+
*
|
|
658
|
+
* @returns true when the process is gone, false on timeout.
|
|
659
|
+
*/
|
|
660
|
+
async function waitForProcessExit(pid, timeoutMs, pollMs = 100) {
|
|
661
|
+
const target = Number(pid);
|
|
662
|
+
if (!Number.isInteger(target) || target <= 0) return true; // nothing to wait for
|
|
663
|
+
const deadline = Date.now() + timeoutMs;
|
|
664
|
+
for (;;) {
|
|
665
|
+
try {
|
|
666
|
+
process.kill(target, 0);
|
|
667
|
+
} catch (error) {
|
|
668
|
+
if (error?.code === "ESRCH") return true;
|
|
669
|
+
if (error?.code !== "EPERM") return true; // unprobeable: do not block the restart on it
|
|
670
|
+
}
|
|
671
|
+
if (Date.now() >= deadline) return false;
|
|
672
|
+
await delay(pollMs);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
640
676
|
/**
|
|
641
677
|
* Spawn the command after `--` with inherited stdio. POSIX uses shell:false so
|
|
642
678
|
* the argv reaches execvp untouched. On Windows a bare name is first resolved
|
|
@@ -644,8 +680,25 @@ function resolveWindowsCommand(command) {
|
|
|
644
680
|
* so they go through %ComSpec% with every token strictly quoted; .exe/.com and
|
|
645
681
|
* explicit paths spawn directly with shell:false.
|
|
646
682
|
*/
|
|
683
|
+
/**
|
|
684
|
+
* Whether to stop Windows conjuring a console for the child.
|
|
685
|
+
*
|
|
686
|
+
* Inherit the console we have; never create one we do not. Run from a terminal
|
|
687
|
+
* our stdio IS that terminal, the child attaches to it, and hiding would cost
|
|
688
|
+
* the interactive session its Ctrl+C. Run from the detached restart our stdio
|
|
689
|
+
* is the log file and we hold no console at all, so the child would be handed
|
|
690
|
+
* a brand-new window — blank, since its output goes to the log — which is the
|
|
691
|
+
* stray console a restart used to leave on screen. Every subprocess this
|
|
692
|
+
* package spawns already passes `windowsHide` (installer.js, six call sites),
|
|
693
|
+
* so nothing downstream depends on inheriting a visible console.
|
|
694
|
+
*/
|
|
695
|
+
function hideChildConsole() {
|
|
696
|
+
return process.platform === "win32" && process.stdout.isTTY !== true;
|
|
697
|
+
}
|
|
698
|
+
|
|
647
699
|
function spawnCommand(command, args) {
|
|
648
700
|
const resolved = process.platform === "win32" ? resolveWindowsCommand(command) : command;
|
|
701
|
+
const windowsHide = hideChildConsole();
|
|
649
702
|
if (process.platform === "win32" && /\.(?:cmd|bat)$/i.test(resolved)) {
|
|
650
703
|
const comspec = process.env.ComSpec ?? "cmd.exe";
|
|
651
704
|
const line = [resolved, ...args].map(quoteCmdArg).join(" ");
|
|
@@ -654,9 +707,9 @@ function spawnCommand(command, args) {
|
|
|
654
707
|
// windowsVerbatimArguments passes the line to CreateProcess exactly as
|
|
655
708
|
// built — otherwise libuv would re-quote it for CommandLineToArgvW and the
|
|
656
709
|
// escaped quotes would break cmd's /s stripping.
|
|
657
|
-
return spawn(comspec, ["/d", "/s", "/c", `"${line}"`], { shell: false, stdio: "inherit", env: process.env, windowsVerbatimArguments: true });
|
|
710
|
+
return spawn(comspec, ["/d", "/s", "/c", `"${line}"`], { shell: false, stdio: "inherit", env: process.env, windowsVerbatimArguments: true, windowsHide });
|
|
658
711
|
}
|
|
659
|
-
return spawn(resolved, args, { shell: false, stdio: "inherit", env: process.env });
|
|
712
|
+
return spawn(resolved, args, { shell: false, stdio: "inherit", env: process.env, windowsHide });
|
|
660
713
|
}
|
|
661
714
|
|
|
662
715
|
/** Forward SIGINT/SIGTERM to the child where the platform delivers them to us. */
|
|
@@ -831,10 +884,26 @@ function markerLooksValid(marker, profileDir, home) {
|
|
|
831
884
|
* A corrupt or legacy (pre-v2) marker, or a failed static-recovery step, fails
|
|
832
885
|
* closed: the command is NOT launched and no unvalidated path is deleted.
|
|
833
886
|
*/
|
|
834
|
-
async function cmdLaunch({ profile, home, graceMs, commandArgv }) {
|
|
887
|
+
async function cmdLaunch({ profile, home, graceMs, commandArgv, awaitExitPid, _waitForExit = waitForProcessExit }) {
|
|
835
888
|
const profileDir = profileDirOf(home, profile);
|
|
836
889
|
const grace = graceMs ?? DEFAULT_GRACE_MS;
|
|
837
890
|
const [command, ...args] = commandArgv;
|
|
891
|
+
|
|
892
|
+
// A restart hands us the pid of the host that is on its way out. Starting
|
|
893
|
+
// the successor while it still holds the listening port is not a race worth
|
|
894
|
+
// taking: the bind fails, the probation below reads that as "the pending
|
|
895
|
+
// install crashed dsh", and it rolls back an install that was fine. Wait for
|
|
896
|
+
// the process to be gone, then let the port settle.
|
|
897
|
+
//
|
|
898
|
+
// Refuse rather than start anyway if it outlives the timeout. Starting is
|
|
899
|
+
// the outcome that costs the user their install; not starting leaves the old
|
|
900
|
+
// host running and the marker pending, which the next launch resolves.
|
|
901
|
+
if (awaitExitPid !== undefined) {
|
|
902
|
+
if (!await _waitForExit(awaitExitPid, AWAIT_EXIT_TIMEOUT_MS)) {
|
|
903
|
+
throw new Error(`process ${awaitExitPid} was still running after ${AWAIT_EXIT_TIMEOUT_MS}ms — refusing to start a second host that would collide with it on the listening port (the old one is still up; nothing was changed)`);
|
|
904
|
+
}
|
|
905
|
+
await delay(PORT_SETTLE_MS);
|
|
906
|
+
}
|
|
838
907
|
// Mirrors guard.js pendingPath(): <home>/guard/pending-<profile>.json.
|
|
839
908
|
const markerPath = join(home, "guard", `pending-${profile}.json`);
|
|
840
909
|
|
|
@@ -915,7 +984,7 @@ Usage:
|
|
|
915
984
|
node src/cli.js guard list [--home <dir>]
|
|
916
985
|
node src/cli.js guard add <spec> --profile <name> [--home <dir>] [--accept-warnings]
|
|
917
986
|
node src/cli.js guard remove <package> --profile <name> [--home <dir>]
|
|
918
|
-
node src/cli.js guard launch --profile <name> [--home <dir>] [--grace-ms <ms>] -- <command> [args...]
|
|
987
|
+
node src/cli.js guard launch --profile <name> [--home <dir>] [--grace-ms <ms>] [--await-exit <pid>] -- <command> [args...]
|
|
919
988
|
node src/cli.js guard self-test
|
|
920
989
|
|
|
921
990
|
Commands:
|
|
@@ -956,7 +1025,7 @@ function parseArgs(argv) {
|
|
|
956
1025
|
if (args[0] === "guard") args.shift(); // `node cli.js guard recover` / `node cli.js recover`
|
|
957
1026
|
let command = args.shift() ?? "help";
|
|
958
1027
|
if (command === "--help" || command === "-h") command = "help"; // `cli.js --help`
|
|
959
|
-
const opts = { home: undefined, profile: undefined, graceMs: undefined, acceptWarnings: false, positionals: [], commandArgv: [] };
|
|
1028
|
+
const opts = { home: undefined, profile: undefined, graceMs: undefined, awaitExit: undefined, acceptWarnings: false, positionals: [], commandArgv: [] };
|
|
960
1029
|
for (let index = 0; index < args.length; index++) {
|
|
961
1030
|
const arg = args[index];
|
|
962
1031
|
if (arg === "--") { opts.commandArgv = args.slice(index + 1); break; } // launch: the wrapped command, verbatim
|
|
@@ -968,6 +1037,8 @@ function parseArgs(argv) {
|
|
|
968
1037
|
if (arg.startsWith("--profile=")) { opts.profile = arg.slice("--profile=".length); continue; }
|
|
969
1038
|
if (arg === "--grace-ms") { opts.graceMs = args[++index]; continue; }
|
|
970
1039
|
if (arg.startsWith("--grace-ms=")) { opts.graceMs = arg.slice("--grace-ms=".length); continue; }
|
|
1040
|
+
if (arg === "--await-exit") { opts.awaitExit = args[++index]; continue; }
|
|
1041
|
+
if (arg.startsWith("--await-exit=")) { opts.awaitExit = arg.slice("--await-exit=".length); continue; }
|
|
971
1042
|
if (arg === "--all") { opts.all = true; continue; }
|
|
972
1043
|
if (arg.startsWith("-")) throw new Error(`unknown option ${JSON.stringify(arg)}`);
|
|
973
1044
|
opts.positionals.push(arg);
|
|
@@ -1038,7 +1109,12 @@ async function main(argv) {
|
|
|
1038
1109
|
graceMs = Number(parsed.graceMs);
|
|
1039
1110
|
if (!Number.isFinite(graceMs) || graceMs < 0) throw usageError(`--grace-ms must be a non-negative number, got ${JSON.stringify(parsed.graceMs)}`);
|
|
1040
1111
|
}
|
|
1041
|
-
|
|
1112
|
+
let awaitExitPid;
|
|
1113
|
+
if (parsed.awaitExit !== undefined) {
|
|
1114
|
+
awaitExitPid = Number(parsed.awaitExit);
|
|
1115
|
+
if (!Number.isInteger(awaitExitPid) || awaitExitPid <= 0) throw usageError(`--await-exit must be a positive process id, got ${JSON.stringify(parsed.awaitExit)}`);
|
|
1116
|
+
}
|
|
1117
|
+
process.exitCode = await cmdLaunch({ profile: parsed.profile, home, graceMs, commandArgv: parsed.commandArgv, awaitExitPid });
|
|
1042
1118
|
return;
|
|
1043
1119
|
}
|
|
1044
1120
|
case "self-test": {
|
|
@@ -1101,6 +1177,78 @@ async function selfTest() {
|
|
|
1101
1177
|
if (p2.graceMs !== "0" || p2.commandArgv[0] !== "dsh.cmd" || p2.commandArgv.length !== 2) throw new Error("parseArgs launch =fixture failed");
|
|
1102
1178
|
const p3 = parseArgs(["launch", "--profile", "web", "--", "--weird-but-verbatim"]);
|
|
1103
1179
|
if (p3.commandArgv[0] !== "--weird-but-verbatim") throw new Error("parseArgs should pass post-`--` args through verbatim");
|
|
1180
|
+
const p4 = parseArgs(["launch", "--profile", "web", "--await-exit", "4321", "--", "dsh"]);
|
|
1181
|
+
if (p4.awaitExit !== "4321") throw new Error("parseArgs --await-exit fixture failed");
|
|
1182
|
+
const p5 = parseArgs(["launch", "--profile=web", "--await-exit=99", "--", "dsh"]);
|
|
1183
|
+
if (p5.awaitExit !== "99") throw new Error("parseArgs --await-exit= fixture failed");
|
|
1184
|
+
// A pid after `--` belongs to the wrapped command, not to us.
|
|
1185
|
+
const p6 = parseArgs(["launch", "--profile", "web", "--", "dsh", "--await-exit", "7"]);
|
|
1186
|
+
if (p6.awaitExit !== undefined || p6.commandArgv.join(" ") !== "dsh --await-exit 7") throw new Error("--await-exit after `--` must stay with the wrapped command");
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// `--await-exit`: the successor must not start while the outgoing host is
|
|
1190
|
+
// still holding the port. A pid that never goes away is refused outright —
|
|
1191
|
+
// starting anyway is what costs the user their install, because the failed
|
|
1192
|
+
// bind reads as "the pending plugin crashed dsh" and rolls it back.
|
|
1193
|
+
{
|
|
1194
|
+
const p = join(root, "profiles", "await-exit");
|
|
1195
|
+
mkdirSync(p, { recursive: true });
|
|
1196
|
+
writeFileSync(join(p, "package.json"), JSON.stringify({ dependencies: {} }));
|
|
1197
|
+
writeFileSync(join(p, "cordis.patch.yml"), "[]\n");
|
|
1198
|
+
const home = root;
|
|
1199
|
+
|
|
1200
|
+
let launched = false;
|
|
1201
|
+
const neverExits = async () => false;
|
|
1202
|
+
let refused = false;
|
|
1203
|
+
try {
|
|
1204
|
+
await cmdLaunch({
|
|
1205
|
+
profile: "await-exit",
|
|
1206
|
+
home,
|
|
1207
|
+
commandArgv: [process.execPath, "-e", "launched=1"],
|
|
1208
|
+
awaitExitPid: 999999,
|
|
1209
|
+
_waitForExit: neverExits,
|
|
1210
|
+
});
|
|
1211
|
+
} catch (error) {
|
|
1212
|
+
refused = /still running after/.test(error.message);
|
|
1213
|
+
}
|
|
1214
|
+
if (!refused) throw new Error("a pid that outlives the timeout must refuse the launch, not race it");
|
|
1215
|
+
|
|
1216
|
+
// …and a pid that does go away lets the command through.
|
|
1217
|
+
const exitsPromptly = async () => true;
|
|
1218
|
+
const code = await cmdLaunch({
|
|
1219
|
+
profile: "await-exit",
|
|
1220
|
+
home,
|
|
1221
|
+
commandArgv: [process.execPath, "-e", "process.exit(0)"],
|
|
1222
|
+
awaitExitPid: 999999,
|
|
1223
|
+
_waitForExit: exitsPromptly,
|
|
1224
|
+
});
|
|
1225
|
+
launched = code === 0;
|
|
1226
|
+
if (!launched) throw new Error("once the outgoing pid is gone the command must run");
|
|
1227
|
+
|
|
1228
|
+
// waitForProcessExit itself: this process is alive, a pid that cannot
|
|
1229
|
+
// exist is gone, and a nonsense pid is not something to wait on.
|
|
1230
|
+
if (await waitForProcessExit(process.pid, 120, 20)) throw new Error("waitForProcessExit must not report a live process as gone");
|
|
1231
|
+
if (!await waitForProcessExit(0x7ffffff0, 500, 20)) throw new Error("waitForProcessExit must report an absent pid as gone");
|
|
1232
|
+
if (!await waitForProcessExit(undefined, 500, 20)) throw new Error("waitForProcessExit must not block on a missing pid");
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// hideChildConsole: inherit the console we have, never create one we do
|
|
1236
|
+
// not. Getting this backwards is visible either way — a stray blank window
|
|
1237
|
+
// after every restart, or an interactive `guard launch` that has lost its
|
|
1238
|
+
// Ctrl+C. Windows-only by construction; asserted on both platforms so the
|
|
1239
|
+
// Linux CI still pins the POSIX half.
|
|
1240
|
+
{
|
|
1241
|
+
const realIsTty = process.stdout.isTTY;
|
|
1242
|
+
try {
|
|
1243
|
+
Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });
|
|
1244
|
+
if (hideChildConsole()) throw new Error("a child of an interactive terminal must inherit its console, not be hidden from it");
|
|
1245
|
+
Object.defineProperty(process.stdout, "isTTY", { value: undefined, configurable: true });
|
|
1246
|
+
if (hideChildConsole() !== (process.platform === "win32")) {
|
|
1247
|
+
throw new Error("with no console of our own the child must not be given a new window on Windows (and the flag is meaningless elsewhere)");
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
Object.defineProperty(process.stdout, "isTTY", { value: realIsTty, configurable: true });
|
|
1251
|
+
}
|
|
1104
1252
|
}
|
|
1105
1253
|
|
|
1106
1254
|
// quoteCmdArg: strict MSVCRT/CommandLineToArgvW quoting; cmd metacharacters
|
|
@@ -1333,9 +1481,11 @@ async function selfTest() {
|
|
|
1333
1481
|
if (!refused || !rolledBack) throw new Error("cmdRemove partial dsh reconcile must roll back despite generic validation passing");
|
|
1334
1482
|
}
|
|
1335
1483
|
|
|
1336
|
-
// Startup must apply the
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1484
|
+
// Startup must apply the remove-completion check before probation, so a
|
|
1485
|
+
// long-running app cannot commit a crash-partial remove once the grace
|
|
1486
|
+
// period elapses. The leftover bundle entry fails generic validation too
|
|
1487
|
+
// (a layer without a dsh.bundle manifest stops dsh at startup); the
|
|
1488
|
+
// remove-specific check is what names the actual cause.
|
|
1339
1489
|
{
|
|
1340
1490
|
const launchHome = join(root, "remove-launch-home");
|
|
1341
1491
|
const launchProfile = join(launchHome, "profiles", "web");
|
|
@@ -1353,7 +1503,7 @@ async function selfTest() {
|
|
|
1353
1503
|
dependencies: {},
|
|
1354
1504
|
dsh: { profile: { bundles: ["victim"] } },
|
|
1355
1505
|
}));
|
|
1356
|
-
if (
|
|
1506
|
+
if (validateInstalledProfile(launchProfile).ok) throw new Error("launch partial-remove fixture must fail validation before probation");
|
|
1357
1507
|
const exitCode = await cmdLaunch({
|
|
1358
1508
|
profile: "web",
|
|
1359
1509
|
home: launchHome,
|