@lark-codem/codem-cli 0.1.185 → 0.1.187-windows-daemon-host.100100730469.0.gd9c67df
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/bin/codem.js +18 -2
- package/bin/platform-child-process.js +178 -0
- package/package.json +8 -8
package/bin/codem.js
CHANGED
|
@@ -18,6 +18,11 @@ import { createRequire } from "node:module";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
20
20
|
|
|
21
|
+
import {
|
|
22
|
+
applyHostedWindowsChildOptions,
|
|
23
|
+
watchWindowsDaemonHostMigration
|
|
24
|
+
} from "./platform-child-process.js";
|
|
25
|
+
|
|
21
26
|
const require = createRequire(import.meta.url);
|
|
22
27
|
const LAUNCHER_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
28
|
const PACKAGE_ROOT = path.resolve(LAUNCHER_DIR, "..");
|
|
@@ -51,7 +56,7 @@ function resolvePlatformBinary() {
|
|
|
51
56
|
// spawn 的失败有两条互斥出口:只有 EACCES / EAGAIN / EMFILE / ENFILE / ENOENT
|
|
52
57
|
// 会被 Node 降级成异步 `error` 事件,其余 errno(ENOEXEC / ENOTDIR / EPERM,以及
|
|
53
58
|
// Windows 上杀软 / EDR 拦截 CreateProcessW 时 libuv 报的 UNKNOWN=-4094)在
|
|
54
|
-
// `child_process.spawn
|
|
59
|
+
// `child_process.spawn` 里直接 `throw new ErrnoException(err, 'spawn')`。
|
|
55
60
|
// stderr 同步写。本文件里每一条诊断后面都紧跟 process.exit,而 process.exit()
|
|
56
61
|
// 按 Node 官方契约会**放弃尚未完成的 stdout/stderr I/O**("a note on process I/O")。
|
|
57
62
|
// stderr 是否同步取决于它接的是什么:
|
|
@@ -144,8 +149,18 @@ const binaryPath = resolvePlatformBinary();
|
|
|
144
149
|
|
|
145
150
|
if (binaryPath) {
|
|
146
151
|
let child;
|
|
152
|
+
let stopWindowsHostMigrationWatch = () => {};
|
|
147
153
|
try {
|
|
148
|
-
child = spawn(
|
|
154
|
+
child = spawn(
|
|
155
|
+
binaryPath,
|
|
156
|
+
process.argv.slice(2),
|
|
157
|
+
applyHostedWindowsChildOptions({ stdio: "inherit" })
|
|
158
|
+
);
|
|
159
|
+
stopWindowsHostMigrationWatch = watchWindowsDaemonHostMigration({
|
|
160
|
+
binaryPath,
|
|
161
|
+
daemonPid: child.pid,
|
|
162
|
+
launcherPid: process.pid
|
|
163
|
+
});
|
|
149
164
|
} catch (error) {
|
|
150
165
|
reportSpawnFailure(binaryPath, error);
|
|
151
166
|
}
|
|
@@ -175,6 +190,7 @@ if (binaryPath) {
|
|
|
175
190
|
});
|
|
176
191
|
|
|
177
192
|
child.on("exit", (code, signal) => {
|
|
193
|
+
stopWindowsHostMigrationWatch();
|
|
178
194
|
for (const [registeredSignal, handler] of signalHandlers) {
|
|
179
195
|
process.removeListener(registeredSignal, handler);
|
|
180
196
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
const WINDOWS_LEGACY_DAEMON_CAPABILITY = "shutdown_for_service_operation";
|
|
7
|
+
const WINDOWS_LEGACY_DAEMON_CAPABILITY_RELATIVE_PATH = path.join(
|
|
8
|
+
"runtime",
|
|
9
|
+
"legacy-daemon-control",
|
|
10
|
+
"capability.json"
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// The dedicated Windows daemon Host owns the top-level console policy. Every
|
|
14
|
+
// child it can reach must stay hidden, and a local spawn option must not be able
|
|
15
|
+
// to undo that invariant. Interactive and non-Windows launches keep their
|
|
16
|
+
// options object untouched.
|
|
17
|
+
export function applyHostedWindowsChildOptions(
|
|
18
|
+
options = {},
|
|
19
|
+
{
|
|
20
|
+
// Hosted mode belongs to the current daemon process, not to the child env
|
|
21
|
+
// object supplied by an individual caller. Otherwise a caller could omit
|
|
22
|
+
// or replace `options.env` and accidentally bypass the console invariant.
|
|
23
|
+
env = process.env,
|
|
24
|
+
platform = process.platform
|
|
25
|
+
} = {}
|
|
26
|
+
) {
|
|
27
|
+
if (platform !== "win32" || env?.CODEM_DAEMON_HOSTED_WINDOWS !== "1") {
|
|
28
|
+
return options;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...options,
|
|
33
|
+
windowsHide: true
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function shouldWatchWindowsDaemonHostMigration({
|
|
38
|
+
platform = process.platform,
|
|
39
|
+
argv = process.argv.slice(2),
|
|
40
|
+
env = process.env
|
|
41
|
+
} = {}) {
|
|
42
|
+
return platform === "win32" &&
|
|
43
|
+
argv[0] === "daemon" &&
|
|
44
|
+
env?.CODEM_DAEMON_HOSTED_WINDOWS !== "1" &&
|
|
45
|
+
typeof env?.CODEM_DAEMON_RUNTIME_CLIENT_INSTANCE_ID === "string" &&
|
|
46
|
+
env.CODEM_DAEMON_RUNTIME_CLIENT_INSTANCE_ID.trim() !== "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function isCurrentWindowsLegacyDaemonCapability(
|
|
50
|
+
value,
|
|
51
|
+
{ daemonPid, startedAtMs } = {}
|
|
52
|
+
) {
|
|
53
|
+
return value?.schemaVersion === 1 &&
|
|
54
|
+
value?.protocolVersion === 1 &&
|
|
55
|
+
value?.capability === WINDOWS_LEGACY_DAEMON_CAPABILITY &&
|
|
56
|
+
Number(value?.rootIdentity?.pid) === daemonPid &&
|
|
57
|
+
Number(value?.publishedAtUnixMs) >= startedAtMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function launchWindowsDaemonHostMigration({
|
|
61
|
+
binaryPath,
|
|
62
|
+
launcherPid = process.pid,
|
|
63
|
+
env = process.env,
|
|
64
|
+
spawnImpl = spawn
|
|
65
|
+
} = {}) {
|
|
66
|
+
if (String(binaryPath).includes("'")) {
|
|
67
|
+
throw new Error("CodeM runtime path cannot be represented in the Windows migration handoff");
|
|
68
|
+
}
|
|
69
|
+
const workingDirectory = path.win32.dirname(binaryPath);
|
|
70
|
+
if (workingDirectory.includes("'")) {
|
|
71
|
+
throw new Error("CodeM working directory cannot be represented in the Windows migration handoff");
|
|
72
|
+
}
|
|
73
|
+
const commandLine = `"${binaryPath}" service start --windows-migration-launcher-pid ${launcherPid}`;
|
|
74
|
+
const script = [
|
|
75
|
+
"$startup = ([wmiclass]'Win32_ProcessStartup').CreateInstance()",
|
|
76
|
+
"$startup.ShowWindow = 0",
|
|
77
|
+
`$result = ([wmiclass]'Win32_Process').Create('${commandLine}', '${workingDirectory}', $startup)`,
|
|
78
|
+
"if ($null -eq $result -or [int]$result.ReturnValue -ne 0) { exit 1 }"
|
|
79
|
+
].join("\r\n");
|
|
80
|
+
const powershellPath = path.win32.join(
|
|
81
|
+
env?.SystemRoot || String.raw`C:\Windows`,
|
|
82
|
+
"System32",
|
|
83
|
+
"WindowsPowerShell",
|
|
84
|
+
"v1.0",
|
|
85
|
+
"powershell.exe"
|
|
86
|
+
);
|
|
87
|
+
const bridge = spawnImpl(powershellPath, [
|
|
88
|
+
"-NoLogo",
|
|
89
|
+
"-NoProfile",
|
|
90
|
+
"-NonInteractive",
|
|
91
|
+
"-WindowStyle",
|
|
92
|
+
"Hidden",
|
|
93
|
+
"-EncodedCommand",
|
|
94
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
95
|
+
], {
|
|
96
|
+
env,
|
|
97
|
+
stdio: "ignore",
|
|
98
|
+
windowsHide: true
|
|
99
|
+
});
|
|
100
|
+
// Task Scheduler may place the launcher in a Job. CREATE_NEW_PROCESS_GROUP
|
|
101
|
+
// (`detached` in Node) does not escape that Job. Windows PowerShell delegates
|
|
102
|
+
// the exact worker creation to Win32_Process.Create, whose documented
|
|
103
|
+
// contract creates it outside the caller's Job; the short-lived bridge can
|
|
104
|
+
// then remain inside the Task without owning migration lifetime.
|
|
105
|
+
bridge.once?.("error", () => {});
|
|
106
|
+
bridge.unref?.();
|
|
107
|
+
return bridge;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function watchWindowsDaemonHostMigration({
|
|
111
|
+
binaryPath,
|
|
112
|
+
daemonPid,
|
|
113
|
+
launcherPid = process.pid,
|
|
114
|
+
argv = process.argv.slice(2),
|
|
115
|
+
env = process.env,
|
|
116
|
+
platform = process.platform,
|
|
117
|
+
codemHome = env?.CODEM_HOME || path.join(env?.USERPROFILE || os.homedir(), ".codem"),
|
|
118
|
+
fsImpl = fs,
|
|
119
|
+
spawnImpl = spawn,
|
|
120
|
+
pollIntervalMs = 250,
|
|
121
|
+
timeoutMs = 60_000,
|
|
122
|
+
nowImpl = Date.now,
|
|
123
|
+
setTimeoutImpl = setTimeout,
|
|
124
|
+
clearTimeoutImpl = clearTimeout
|
|
125
|
+
} = {}) {
|
|
126
|
+
if (!shouldWatchWindowsDaemonHostMigration({ platform, argv, env })) {
|
|
127
|
+
return () => {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const capabilityPath = path.join(codemHome, WINDOWS_LEGACY_DAEMON_CAPABILITY_RELATIVE_PATH);
|
|
131
|
+
const startedAtMs = nowImpl();
|
|
132
|
+
let stopped = false;
|
|
133
|
+
let timer = null;
|
|
134
|
+
|
|
135
|
+
const stop = () => {
|
|
136
|
+
stopped = true;
|
|
137
|
+
if (timer !== null) {
|
|
138
|
+
clearTimeoutImpl(timer);
|
|
139
|
+
timer = null;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const schedule = () => {
|
|
143
|
+
if (stopped) return;
|
|
144
|
+
timer = setTimeoutImpl(() => {
|
|
145
|
+
timer = null;
|
|
146
|
+
void probe();
|
|
147
|
+
}, pollIntervalMs);
|
|
148
|
+
timer?.unref?.();
|
|
149
|
+
};
|
|
150
|
+
const probe = async () => {
|
|
151
|
+
if (stopped) return;
|
|
152
|
+
try {
|
|
153
|
+
const value = JSON.parse(await fsImpl.readFile(capabilityPath, "utf8"));
|
|
154
|
+
if (isCurrentWindowsLegacyDaemonCapability(value, { daemonPid, startedAtMs })) {
|
|
155
|
+
stop();
|
|
156
|
+
launchWindowsDaemonHostMigration({
|
|
157
|
+
binaryPath,
|
|
158
|
+
launcherPid,
|
|
159
|
+
env,
|
|
160
|
+
spawnImpl
|
|
161
|
+
});
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
// Capability publication is atomic and expected to be absent while the
|
|
166
|
+
// daemon starts. A malformed/unreadable file is never accepted; keep the
|
|
167
|
+
// bounded watcher alive so a later valid replacement can win.
|
|
168
|
+
}
|
|
169
|
+
if (nowImpl() - startedAtMs >= timeoutMs) {
|
|
170
|
+
stop();
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
schedule();
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
void probe();
|
|
177
|
+
return stop;
|
|
178
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lark-codem/codem-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CodeM local CLI for Feishu-driven coding sessions",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -26,15 +26,15 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@lark-codem/codem-core": "0.8.
|
|
29
|
+
"@lark-codem/codem-core": "0.8.16-windows-daemon-host.100100730469.0.gb96411f"
|
|
30
30
|
},
|
|
31
31
|
"optionalDependencies": {
|
|
32
|
-
"@lark-codem/codem-cli-darwin-arm64": "0.1.
|
|
33
|
-
"@lark-codem/codem-cli-darwin-x64": "0.1.
|
|
34
|
-
"@lark-codem/codem-cli-linux-x64": "0.1.
|
|
35
|
-
"@lark-codem/codem-cli-linux-arm64": "0.1.
|
|
36
|
-
"@lark-codem/codem-cli-win32-arm64": "0.1.
|
|
37
|
-
"@lark-codem/codem-cli-win32-x64": "0.1.
|
|
32
|
+
"@lark-codem/codem-cli-darwin-arm64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
33
|
+
"@lark-codem/codem-cli-darwin-x64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
34
|
+
"@lark-codem/codem-cli-linux-x64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
35
|
+
"@lark-codem/codem-cli-linux-arm64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
36
|
+
"@lark-codem/codem-cli-win32-arm64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df",
|
|
37
|
+
"@lark-codem/codem-cli-win32-x64": "0.1.187-windows-daemon-host.100100730469.0.gd9c67df"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|