clawconnect-agent 0.2.13 → 0.2.15
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 +2 -2
- package/README.zh-CN.md +1 -1
- package/dist/commands/install.js +5 -3
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/send-file.d.ts +2 -0
- package/dist/commands/send-file.js +8 -0
- package/dist/commands/send-file.js.map +1 -1
- package/dist/config/profile-log-rotation.d.ts +1 -1
- package/dist/config/profile-log-rotation.js +1 -1
- package/dist/config/profile-log-rotation.js.map +1 -1
- package/dist/core/relay/source-commit.d.ts +7 -0
- package/dist/core/relay/source-commit.js +25 -1
- package/dist/core/relay/source-commit.js.map +1 -1
- package/dist/hermes/hermes-relay-manager.d.ts +3 -0
- package/dist/hermes/hermes-relay-manager.js +7 -0
- package/dist/hermes/hermes-relay-manager.js.map +1 -1
- package/dist/hermes/relay/hermes-cron-change-relay.d.ts +28 -0
- package/dist/hermes/relay/hermes-cron-change-relay.js +55 -0
- package/dist/hermes/relay/hermes-cron-change-relay.js.map +1 -0
- package/dist/hermes/runtime/hermes-cron-change-watcher.d.ts +34 -0
- package/dist/hermes/runtime/hermes-cron-change-watcher.js +135 -0
- package/dist/hermes/runtime/hermes-cron-change-watcher.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-cli.d.ts +16 -0
- package/dist/hermes/runtime/hermes-runtime-chat-cli.js +359 -0
- package/dist/hermes/runtime/hermes-runtime-chat-cli.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.d.ts +57 -0
- package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.js +321 -0
- package/dist/hermes/runtime/hermes-runtime-chat-file-transfer.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-message.d.ts +10 -0
- package/dist/hermes/runtime/hermes-runtime-chat-message.js +133 -0
- package/dist/hermes/runtime/hermes-runtime-chat-message.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-output.d.ts +17 -0
- package/dist/hermes/runtime/hermes-runtime-chat-output.js +90 -0
- package/dist/hermes/runtime/hermes-runtime-chat-output.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-queue.d.ts +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat-queue.js +18 -0
- package/dist/hermes/runtime/hermes-runtime-chat-queue.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-chat.d.ts +3 -33
- package/dist/hermes/runtime/hermes-runtime-chat.js +41 -910
- package/dist/hermes/runtime/hermes-runtime-chat.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-history-turn-output.d.ts +30 -0
- package/dist/hermes/runtime/hermes-runtime-history-turn-output.js +61 -0
- package/dist/hermes/runtime/hermes-runtime-history-turn-output.js.map +1 -0
- package/dist/hermes/runtime/hermes-runtime-history.js +24 -3
- package/dist/hermes/runtime/hermes-runtime-history.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-slash-command.d.ts +14 -1
- package/dist/hermes/runtime/hermes-runtime-slash-command.js +6 -1
- package/dist/hermes/runtime/hermes-runtime-slash-command.js.map +1 -1
- package/dist/hermes/runtime/hermes-runtime-state-db.js +28 -0
- package/dist/hermes/runtime/hermes-runtime-state-db.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/openclaw/handlers/local-handlers.js +1 -1
- package/dist/openclaw/handlers/local-handlers.js.map +1 -1
- package/dist/openclaw/relay/chat-history.d.ts +5 -1
- package/dist/openclaw/relay/chat-history.js +4 -7
- package/dist/openclaw/relay/chat-history.js.map +1 -1
- package/dist/openclaw/relay/openclaw-source-commit-watcher.d.ts +7 -4
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js +2 -0
- package/dist/openclaw/relay/openclaw-source-commit-watcher.js.map +1 -1
- package/dist/openclaw/relay/openclaw-source-cursor-connections.d.ts +2 -0
- package/dist/openclaw/relay/openclaw-source-cursor-connections.js +12 -0
- package/dist/openclaw/relay/openclaw-source-cursor-connections.js.map +1 -1
- package/dist/openclaw/relay/source-commit-projection.d.ts +28 -6
- package/dist/openclaw/relay/source-commit-projection.js +106 -30
- package/dist/openclaw/relay/source-commit-projection.js.map +1 -1
- package/dist/openclaw/relay/source-row-fingerprints.d.ts +22 -0
- package/dist/openclaw/relay/source-row-fingerprints.js +48 -0
- package/dist/openclaw/relay/source-row-fingerprints.js.map +1 -0
- package/dist/openclaw/relay-manager.js +44 -22
- package/dist/openclaw/relay-manager.js.map +1 -1
- package/dist/platform/service-manager-windows.d.ts +39 -29
- package/dist/platform/service-manager-windows.js +256 -448
- package/dist/platform/service-manager-windows.js.map +1 -1
- package/dist/platform/windows-process-control.d.ts +16 -0
- package/dist/platform/windows-process-control.js +91 -0
- package/dist/platform/windows-process-control.js.map +1 -0
- package/dist/platform/windows-service-host-source.d.ts +23 -0
- package/dist/platform/windows-service-host-source.js +357 -0
- package/dist/platform/windows-service-host-source.js.map +1 -0
- package/dist/platform/windows-service-legacy-startup.d.ts +27 -0
- package/dist/platform/windows-service-legacy-startup.js +101 -0
- package/dist/platform/windows-service-legacy-startup.js.map +1 -0
- package/dist/platform/windows-service-paths.d.ts +15 -0
- package/dist/platform/windows-service-paths.js +30 -0
- package/dist/platform/windows-service-paths.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
|
-
import { existsSync, readFileSync,
|
|
2
|
+
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
|
-
import { ensureLogDir, getProfileErrorLogPath, getProfileLogDir, getProfileLogPath, getProgramArgs,
|
|
5
|
-
import { getActiveProfile,
|
|
4
|
+
import { ensureLogDir, getProfileErrorLogPath, getProfileLogDir, getProfileLogPath, getProgramArgs, } from "./service-manager-common.js";
|
|
5
|
+
import { getActiveProfile, normalizeProfileName } from "../config/profile.js";
|
|
6
6
|
import { decodeTextBuffer } from "./text-file-decoder.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// Native process stdout is UTF-8; teach PowerShell to decode it correctly
|
|
21
|
-
// and keep redirection writes in UTF-8 so log readers see stable text.
|
|
22
|
-
`$utf8NoBom = [System.Text.UTF8Encoding]::new($false)`,
|
|
23
|
-
`[Console]::InputEncoding = $utf8NoBom`,
|
|
24
|
-
`[Console]::OutputEncoding = $utf8NoBom`,
|
|
25
|
-
`$OutputEncoding = $utf8NoBom`,
|
|
26
|
-
`$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'`,
|
|
27
|
-
command,
|
|
28
|
-
].join("; ");
|
|
7
|
+
import { buildWindowsServiceHostConfig, buildWindowsServiceHostSource, WINDOWS_HOST_GRANT_LOGON_RIGHT_FLAG, } from "./windows-service-host-source.js";
|
|
8
|
+
import { removeLegacyWindowsStartup } from "./windows-service-legacy-startup.js";
|
|
9
|
+
import { isProcessTimeoutError, killWindowsProcess, reportWindowsQueryTimeout, WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS, } from "./windows-process-control.js";
|
|
10
|
+
import { windowsServiceHostConfigPath, windowsServiceHostExePath, windowsServiceHostSourcePath, windowsServiceName, windowsServiceRunnerPath, } from "./windows-service-paths.js";
|
|
11
|
+
export { windowsServiceName } from "./windows-service-paths.js";
|
|
12
|
+
export { buildWindowsProcessQueryScript, WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS, } from "./windows-process-control.js";
|
|
13
|
+
export const WINDOWS_SERVICE_STATE_POLL_INTERVAL_MS = 500;
|
|
14
|
+
export const WINDOWS_SERVICE_STATE_TIMEOUT_MS = 30_000;
|
|
15
|
+
/** 失败后第 1、2 次 5 秒重启,之后 60 秒;24 小时无失败则清零计数。 */
|
|
16
|
+
export const WINDOWS_SERVICE_FAILURE_ACTIONS = "restart/5000/restart/5000/restart/60000";
|
|
17
|
+
export const WINDOWS_SERVICE_FAILURE_RESET_SECONDS = 86_400;
|
|
18
|
+
function defaultSleepSync(ms) {
|
|
19
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
29
20
|
}
|
|
30
21
|
export function normalizeWindowsServiceLogEncoding(path) {
|
|
31
22
|
if (!existsSync(path)) {
|
|
@@ -36,510 +27,327 @@ export function normalizeWindowsServiceLogEncoding(path) {
|
|
|
36
27
|
writeFileSync(path, decoded.text, "utf8");
|
|
37
28
|
}
|
|
38
29
|
}
|
|
39
|
-
export function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
30
|
+
export function buildWindowsServiceStateScript() {
|
|
31
|
+
return `[string](Get-Service -Name $env:CLAW_SERVICE_NAME -ErrorAction Stop).Status`;
|
|
32
|
+
}
|
|
33
|
+
/** 服务账户:域/计算机名\用户名。服务必须以当前用户运行,才能读写其目录下的配置、日志与本机网关。 */
|
|
34
|
+
export function windowsServiceAccount(env = process.env) {
|
|
35
|
+
const user = env.USERNAME?.trim();
|
|
36
|
+
const domain = (env.USERDOMAIN ?? env.COMPUTERNAME)?.trim();
|
|
37
|
+
if (!user || !domain) {
|
|
38
|
+
throw new Error("Cannot determine the current Windows account (USERNAME / USERDOMAIN are not set)");
|
|
39
|
+
}
|
|
40
|
+
return `${domain}\\${user}`;
|
|
45
41
|
}
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
/**
|
|
43
|
+
* 创建服务并设置当前用户凭据。密码只经 Read-Host -AsSecureString 在 PowerShell 进程内流转,
|
|
44
|
+
* 不进入命令行参数,也不经过 Node。创建前先校验密码,避免注册一个永远以 1069 启动失败的服务。
|
|
45
|
+
*/
|
|
46
|
+
export function buildWindowsServiceCreateScript() {
|
|
47
|
+
return [
|
|
48
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
49
|
+
`$secure = Read-Host -Prompt ('Windows password for ' + $env:CLAW_SERVICE_ACCOUNT) -AsSecureString`,
|
|
50
|
+
`if ($secure.Length -eq 0) { throw 'A Windows service cannot log on with an empty password.' }`,
|
|
51
|
+
`$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)`,
|
|
52
|
+
`try { $plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }`,
|
|
53
|
+
`Add-Type -AssemblyName System.DirectoryServices.AccountManagement`,
|
|
54
|
+
`$contextType = if ($env:USERDOMAIN -eq $env:COMPUTERNAME) { 'Machine' } else { 'Domain' }`,
|
|
55
|
+
`$context = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($contextType)`,
|
|
56
|
+
`if (-not $context.ValidateCredentials($env:USERNAME, $plain)) { throw 'The Windows password is incorrect.' }`,
|
|
57
|
+
`$plain = $null`,
|
|
58
|
+
`$credential = New-Object System.Management.Automation.PSCredential($env:CLAW_SERVICE_ACCOUNT, $secure)`,
|
|
59
|
+
`New-Service -Name $env:CLAW_SERVICE_NAME -BinaryPathName $env:CLAW_SERVICE_BINPATH -DisplayName $env:CLAW_SERVICE_NAME -Description 'ClawConnect host agent' -StartupType Automatic -Credential $credential | Out-Null`,
|
|
60
|
+
].join("\n");
|
|
48
61
|
}
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
/** SCM 启动宿主时传入的参数:服务名 + 宿主配置路径。 */
|
|
63
|
+
export function buildWindowsServiceBinaryPath(hostExePath, serviceName, configPath) {
|
|
64
|
+
return `"${hostExePath}" "${serviceName}" "${configPath}"`;
|
|
65
|
+
}
|
|
66
|
+
export function resolveWindowsCscPath(fileExists = existsSync, windowsDir = process.env.windir ?? process.env.SystemRoot ?? "C:\\Windows") {
|
|
67
|
+
const candidates = [
|
|
68
|
+
join(windowsDir, "Microsoft.NET", "Framework64", "v4.0.30319", "csc.exe"),
|
|
69
|
+
join(windowsDir, "Microsoft.NET", "Framework", "v4.0.30319", "csc.exe"),
|
|
70
|
+
];
|
|
71
|
+
return candidates.find((candidate) => fileExists(candidate));
|
|
51
72
|
}
|
|
52
|
-
export function
|
|
73
|
+
export function buildWindowsServiceHostCompileArgs(sourcePath, outputPath) {
|
|
53
74
|
return [
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"/
|
|
57
|
-
|
|
58
|
-
"/
|
|
59
|
-
|
|
60
|
-
"/d",
|
|
61
|
-
taskCommand,
|
|
62
|
-
"/f",
|
|
75
|
+
"/nologo",
|
|
76
|
+
"/target:exe",
|
|
77
|
+
"/optimize+",
|
|
78
|
+
`/out:${outputPath}`,
|
|
79
|
+
"/reference:System.ServiceProcess.dll",
|
|
80
|
+
sourcePath,
|
|
63
81
|
];
|
|
64
82
|
}
|
|
65
|
-
function
|
|
83
|
+
export function isWindowsElevated(execFileSyncImpl = execFileSync) {
|
|
66
84
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
windowsHide: true,
|
|
70
|
-
});
|
|
85
|
+
// fltmc 只有提权后的进程才能成功执行,比解析 whoami 输出更不依赖系统语言。
|
|
86
|
+
execFileSyncImpl("fltmc", [], { stdio: "pipe", windowsHide: true, timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS });
|
|
71
87
|
return true;
|
|
72
88
|
}
|
|
73
89
|
catch {
|
|
74
90
|
return false;
|
|
75
91
|
}
|
|
76
92
|
}
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
execFileSyncImpl("reg.exe", ["query", WINDOWS_RUN_KEY, "/v", taskName], {
|
|
80
|
-
stdio: "pipe",
|
|
81
|
-
windowsHide: true,
|
|
82
|
-
timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS,
|
|
83
|
-
});
|
|
93
|
+
function requireElevation(action, profile, deps) {
|
|
94
|
+
if (isWindowsElevated(deps.execFileSyncImpl)) {
|
|
84
95
|
return true;
|
|
85
96
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
97
|
+
const profileArg = profile ? ` --profile ${profile}` : "";
|
|
98
|
+
console.error(`[clawconnect] ${action} a Windows service requires an elevated terminal. `
|
|
99
|
+
+ `Open PowerShell with "Run as administrator" and run: clawconnect install${profileArg}`);
|
|
100
|
+
return false;
|
|
92
101
|
}
|
|
93
|
-
function
|
|
102
|
+
export function queryWindowsServiceState(serviceName, deps = {}) {
|
|
103
|
+
const execFileSyncImpl = deps.execFileSyncImpl ?? execFileSync;
|
|
94
104
|
try {
|
|
95
|
-
execFileSyncImpl("
|
|
105
|
+
const output = execFileSyncImpl("powershell", ["-NoProfile", "-NonInteractive", "-Command", buildWindowsServiceStateScript()], {
|
|
96
106
|
stdio: "pipe",
|
|
97
|
-
|
|
107
|
+
env: { ...process.env, CLAW_SERVICE_NAME: serviceName },
|
|
98
108
|
timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS,
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
windowsHide: true,
|
|
110
|
+
}).toString().trim().toLowerCase();
|
|
111
|
+
if (output === "running")
|
|
112
|
+
return "running";
|
|
113
|
+
if (output === "stopped")
|
|
114
|
+
return "stopped";
|
|
115
|
+
if (output.endsWith("pending") || output === "paused")
|
|
116
|
+
return "pending";
|
|
117
|
+
return "unknown";
|
|
101
118
|
}
|
|
102
119
|
catch (error) {
|
|
103
120
|
if (isProcessTimeoutError(error)) {
|
|
104
|
-
onQueryTimeout("
|
|
121
|
+
(deps.onQueryTimeout ?? reportWindowsQueryTimeout)("service state");
|
|
122
|
+
return "unknown";
|
|
105
123
|
}
|
|
106
|
-
|
|
124
|
+
// Get-Service 找不到服务会以非零退出码结束。
|
|
125
|
+
return "missing";
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const defaultProfileConfigured = options.defaultProfileConfigured
|
|
116
|
-
?? listProfileNames().includes("default");
|
|
117
|
-
if (defaultProfileConfigured) {
|
|
118
|
-
// A configured default profile legitimately owns the unsuffixed task and Run value.
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
const execFileSyncImpl = options.execFileSyncImpl ?? execFileSync;
|
|
122
|
-
const onQueryTimeout = options.onQueryTimeout ?? reportWindowsQueryTimeout;
|
|
123
|
-
let queryTimedOut = false;
|
|
124
|
-
const reportQueryTimeout = (probe) => {
|
|
125
|
-
queryTimedOut = true;
|
|
126
|
-
onQueryTimeout(probe);
|
|
127
|
-
};
|
|
128
|
-
const legacyTaskExists = hasWindowsScheduledTask(TASK_NAME, execFileSyncImpl, reportQueryTimeout);
|
|
129
|
-
const legacyStartupExists = hasWindowsStartupEntry(TASK_NAME, execFileSyncImpl, reportQueryTimeout);
|
|
130
|
-
if (queryTimedOut) {
|
|
131
|
-
return false;
|
|
132
|
-
}
|
|
133
|
-
if (!legacyTaskExists && !legacyStartupExists) {
|
|
134
|
-
return true;
|
|
135
|
-
}
|
|
136
|
-
let migrated = true;
|
|
137
|
-
if (legacyTaskExists) {
|
|
138
|
-
try {
|
|
139
|
-
execFileSyncImpl("schtasks", ["/end", "/tn", TASK_NAME], {
|
|
140
|
-
stdio: "pipe",
|
|
141
|
-
windowsHide: true,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
// The legacy task can be installed without currently running.
|
|
146
|
-
}
|
|
147
|
-
try {
|
|
148
|
-
execFileSyncImpl("schtasks", ["/delete", "/tn", TASK_NAME, "/f"], {
|
|
149
|
-
stdio: "pipe",
|
|
150
|
-
windowsHide: true,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
migrated = false;
|
|
128
|
+
function waitForServiceState(serviceName, wanted, deps) {
|
|
129
|
+
const sleep = deps.sleepSync ?? defaultSleepSync;
|
|
130
|
+
const attempts = Math.ceil(WINDOWS_SERVICE_STATE_TIMEOUT_MS / WINDOWS_SERVICE_STATE_POLL_INTERVAL_MS);
|
|
131
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
132
|
+
if (queryWindowsServiceState(serviceName, deps) === wanted) {
|
|
133
|
+
return true;
|
|
155
134
|
}
|
|
135
|
+
sleep(WINDOWS_SERVICE_STATE_POLL_INTERVAL_MS);
|
|
156
136
|
}
|
|
157
|
-
|
|
158
|
-
migrated = false;
|
|
159
|
-
}
|
|
160
|
-
(options.stopLegacyProcess ?? (() => { killWindowsProcess(undefined); }))();
|
|
161
|
-
return migrated;
|
|
137
|
+
return false;
|
|
162
138
|
}
|
|
163
|
-
|
|
164
|
-
|
|
139
|
+
function runServiceControl(args, deps) {
|
|
140
|
+
(deps.execFileSyncImpl ?? execFileSync)("sc.exe", args, { stdio: "pipe", windowsHide: true });
|
|
165
141
|
}
|
|
166
|
-
function
|
|
142
|
+
function stopServiceAndWait(serviceName, deps) {
|
|
143
|
+
const state = queryWindowsServiceState(serviceName, deps);
|
|
144
|
+
if (state === "missing" || state === "stopped") {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
167
147
|
try {
|
|
168
|
-
|
|
169
|
-
stdio: "pipe",
|
|
170
|
-
env: { ...process.env, CLAW_TASK_NAME: taskName },
|
|
171
|
-
timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS,
|
|
172
|
-
windowsHide: true,
|
|
173
|
-
})
|
|
174
|
-
.toString()
|
|
175
|
-
.trim();
|
|
176
|
-
return /^running$/i.test(output);
|
|
148
|
+
runServiceControl(["stop", serviceName], deps);
|
|
177
149
|
}
|
|
178
|
-
catch
|
|
179
|
-
|
|
180
|
-
onQueryTimeout("scheduled task state");
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
150
|
+
catch {
|
|
151
|
+
// 已处于停止中等状态时 sc stop 会报错,以下面的等待结果为准。
|
|
183
152
|
}
|
|
153
|
+
return waitForServiceState(serviceName, "stopped", deps);
|
|
184
154
|
}
|
|
185
|
-
function
|
|
186
|
-
if (
|
|
187
|
-
|
|
155
|
+
function writeServiceHostConfig(profile, deps = {}) {
|
|
156
|
+
if (deps.writeConfig) {
|
|
157
|
+
deps.writeConfig(profile);
|
|
158
|
+
return;
|
|
188
159
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
160
|
+
const logPath = getProfileLogPath(profile);
|
|
161
|
+
const errorLogPath = getProfileErrorLogPath(profile);
|
|
162
|
+
ensureLogDir(profile);
|
|
163
|
+
// 旧版 PowerShell 重定向写出的是 UTF-16 日志;宿主按原始 UTF-8 字节追加,先归一化避免混合编码。
|
|
164
|
+
normalizeWindowsServiceLogEncoding(logPath);
|
|
165
|
+
normalizeWindowsServiceLogEncoding(errorLogPath);
|
|
166
|
+
const [executablePath, ...executableArgs] = getProgramArgs(profile);
|
|
167
|
+
const configPath = windowsServiceHostConfigPath(profile);
|
|
168
|
+
writeFileSync(configPath, buildWindowsServiceHostConfig({
|
|
169
|
+
executablePath,
|
|
170
|
+
executableArgs,
|
|
171
|
+
workingDirectory: getProfileLogDir(profile),
|
|
172
|
+
stdoutLogPath: logPath,
|
|
173
|
+
stderrLogPath: errorLogPath,
|
|
174
|
+
}), "utf8");
|
|
201
175
|
}
|
|
202
|
-
function
|
|
176
|
+
function compileServiceHost(profile, deps) {
|
|
177
|
+
if (deps.compileHost) {
|
|
178
|
+
deps.compileHost(profile);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const cscPath = deps.cscPath ?? resolveWindowsCscPath();
|
|
182
|
+
if (!cscPath) {
|
|
183
|
+
throw new Error("The .NET Framework 4 C# compiler (csc.exe) was not found; it is required to build the service host");
|
|
184
|
+
}
|
|
185
|
+
const sourcePath = windowsServiceHostSourcePath(profile);
|
|
186
|
+
writeFileSync(sourcePath, buildWindowsServiceHostSource(), "utf8");
|
|
203
187
|
try {
|
|
204
|
-
|
|
205
|
-
// 由 Windows PowerShell 的 Start-Process 创建独立隐藏进程,并要求返回真实 PID 后才算启动成功。
|
|
206
|
-
const output = execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", buildWindowsDetachedRunnerStartScript()], {
|
|
207
|
-
stdio: "pipe",
|
|
208
|
-
env: { ...process.env, CLAW_RUNNER_PATH: runnerPath },
|
|
209
|
-
timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS,
|
|
210
|
-
windowsHide: true,
|
|
211
|
-
}).toString().trim();
|
|
212
|
-
return /^\d+$/.test(output);
|
|
188
|
+
(deps.execFileSyncImpl ?? execFileSync)(cscPath, buildWindowsServiceHostCompileArgs(sourcePath, windowsServiceHostExePath(profile)), { stdio: "pipe", windowsHide: true });
|
|
213
189
|
}
|
|
214
190
|
catch (error) {
|
|
215
|
-
|
|
216
|
-
|
|
191
|
+
const output = error.stdout?.toString().trim();
|
|
192
|
+
throw new Error(`Failed to compile the Windows service host${output ? `:\n${output}` : ""}`);
|
|
217
193
|
}
|
|
218
194
|
}
|
|
219
|
-
function
|
|
195
|
+
function createService(profile, serviceName, deps) {
|
|
196
|
+
const execFileSyncImpl = deps.execFileSyncImpl ?? execFileSync;
|
|
197
|
+
const account = deps.account ?? windowsServiceAccount();
|
|
198
|
+
const hostExePath = windowsServiceHostExePath(profile);
|
|
199
|
+
execFileSyncImpl("powershell", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", buildWindowsServiceCreateScript()], {
|
|
200
|
+
// 继承 stdio:密码提示必须直接出现在用户的终端里。
|
|
201
|
+
stdio: "inherit",
|
|
202
|
+
env: {
|
|
203
|
+
...process.env,
|
|
204
|
+
CLAW_SERVICE_NAME: serviceName,
|
|
205
|
+
CLAW_SERVICE_BINPATH: buildWindowsServiceBinaryPath(hostExePath, serviceName, windowsServiceHostConfigPath(profile)),
|
|
206
|
+
CLAW_SERVICE_ACCOUNT: account,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
execFileSyncImpl(hostExePath, [WINDOWS_HOST_GRANT_LOGON_RIGHT_FLAG, account], { stdio: "pipe", windowsHide: true });
|
|
210
|
+
runServiceControl([
|
|
211
|
+
"failure", serviceName,
|
|
212
|
+
"reset=", String(WINDOWS_SERVICE_FAILURE_RESET_SECONDS),
|
|
213
|
+
"actions=", WINDOWS_SERVICE_FAILURE_ACTIONS,
|
|
214
|
+
], deps);
|
|
215
|
+
// 宿主进程以非零码退出也算失败,同样触发上面的自动重启。
|
|
216
|
+
runServiceControl(["failureflag", serviceName, "1"], deps);
|
|
217
|
+
}
|
|
218
|
+
function startServiceAndWait(serviceName, deps) {
|
|
220
219
|
try {
|
|
221
|
-
|
|
220
|
+
runServiceControl(["start", serviceName], deps);
|
|
222
221
|
}
|
|
223
|
-
catch {
|
|
224
|
-
|
|
222
|
+
catch (error) {
|
|
223
|
+
const detail = error.stdout?.toString().trim();
|
|
224
|
+
console.error(`[clawconnect] Failed to start Windows service ${serviceName}${detail ? `: ${detail}` : ""}`);
|
|
225
|
+
return false;
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
stdio: "pipe",
|
|
230
|
-
windowsHide: true,
|
|
231
|
-
});
|
|
232
|
-
if (!startWindowsRunner(runnerPath)) {
|
|
233
|
-
throw new Error("Windows startup entry was created, but its service runner did not start");
|
|
227
|
+
if (!waitForServiceState(serviceName, "running", deps)) {
|
|
228
|
+
console.error(`[clawconnect] Windows service ${serviceName} did not reach the running state.`);
|
|
229
|
+
return false;
|
|
234
230
|
}
|
|
231
|
+
return true;
|
|
235
232
|
}
|
|
236
|
-
export function
|
|
237
|
-
const runnerPath = windowsServiceRunnerPath(profile);
|
|
238
|
-
const script = `${buildWindowsServiceProcessCommand(args, logPath, errorLogPath)}\r\n`;
|
|
239
|
-
// Windows PowerShell 5.1 treats UTF-8 files without a BOM as the active ANSI
|
|
240
|
-
// code page. Keep the BOM so user names and install paths remain intact.
|
|
241
|
-
const utf8WithBom = Buffer.concat([
|
|
242
|
-
Buffer.from([0xef, 0xbb, 0xbf]),
|
|
243
|
-
Buffer.from(script, "utf8"),
|
|
244
|
-
]);
|
|
245
|
-
writeFileSync(runnerPath, utf8WithBom);
|
|
246
|
-
setRestrictiveFilePermissions(runnerPath);
|
|
247
|
-
return runnerPath;
|
|
248
|
-
}
|
|
249
|
-
function refreshWindowsServiceRunner(profile) {
|
|
233
|
+
export function installWindowsService(profile, deps = {}) {
|
|
250
234
|
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
ensureLogDir(resolvedProfile);
|
|
254
|
-
// 升级 npm 包后 runner 必须改用当前 Node/CLI 绝对路径;同时先归一化旧版 PowerShell 日志编码。
|
|
255
|
-
normalizeWindowsServiceLogEncoding(logPath);
|
|
256
|
-
normalizeWindowsServiceLogEncoding(errorLogPath);
|
|
257
|
-
return writeWindowsServiceRunner(resolvedProfile, getProgramArgs(resolvedProfile), logPath, errorLogPath);
|
|
258
|
-
}
|
|
259
|
-
/**
|
|
260
|
-
* Kill any running node processes that are executing this specific script.
|
|
261
|
-
* Uses execFileSync to bypass cmd.exe, avoiding CMD quote-mangling.
|
|
262
|
-
* Returns true if any processes were killed.
|
|
263
|
-
*/
|
|
264
|
-
export function buildWindowsProcessQueryScript(profile) {
|
|
265
|
-
const normalizedProfile = normalizeProfileName(profile);
|
|
266
|
-
const profileFilter = normalizedProfile
|
|
267
|
-
? `($_.CommandLine -match ('(?:^|\\s)--profile(?:\\s+|=)' + [regex]::Escape($env:CLAW_PROFILE_NAME) + '(?:\\s|$)') -or $_.CommandLine -match [regex]::Escape($env:CLAW_RUNNER_PATH))`
|
|
268
|
-
: `($_.CommandLine -notmatch '(?:^|\\s)--profile(?:\\s|=)')`;
|
|
269
|
-
return `Get-CimInstance Win32_Process -Filter "Name = 'node.exe'" -OperationTimeoutSec 3 | Where-Object { ($_.CommandLine -match 'clawconnect' -or $_.CommandLine -match [regex]::Escape($env:CLAW_SCRIPT_PATH) -or $_.CommandLine -match [regex]::Escape($env:CLAW_RUNNER_PATH)) -and ${profileFilter} } | Select-Object -ExpandProperty ProcessId`;
|
|
270
|
-
}
|
|
271
|
-
function windowsProcessQueryEnv(profile, scriptPath = process.argv[1]) {
|
|
272
|
-
const normalizedProfile = normalizeProfileName(profile);
|
|
273
|
-
return {
|
|
274
|
-
...process.env,
|
|
275
|
-
CLAW_SCRIPT_PATH: scriptPath ? resolveServiceEntryPath(scriptPath) : "",
|
|
276
|
-
CLAW_RUNNER_PATH: windowsServiceRunnerPath(normalizedProfile),
|
|
277
|
-
CLAW_PROFILE_NAME: normalizedProfile ?? "",
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
function killWindowsProcess(profile) {
|
|
281
|
-
const scriptPath = process.argv[1];
|
|
282
|
-
if (!scriptPath)
|
|
235
|
+
const serviceName = windowsServiceName(resolvedProfile);
|
|
236
|
+
if (!requireElevation("Installing", resolvedProfile, deps)) {
|
|
283
237
|
return false;
|
|
284
|
-
|
|
238
|
+
}
|
|
285
239
|
try {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
.toString()
|
|
294
|
-
.trim();
|
|
295
|
-
const pids = output
|
|
296
|
-
.split(/\r?\n/)
|
|
297
|
-
.map((p) => p.trim())
|
|
298
|
-
.filter((p) => /\d+/.test(p) && Number(p) !== currentPid);
|
|
299
|
-
if (pids.length === 0)
|
|
300
|
-
return false;
|
|
301
|
-
// Step 1: graceful shutdown — taskkill without /F sends WM_CLOSE.
|
|
302
|
-
// For console/background Node.js processes this is best-effort only.
|
|
303
|
-
for (const pid of pids) {
|
|
304
|
-
try {
|
|
305
|
-
execFileSync("taskkill", ["/PID", String(pid)], { stdio: "pipe" });
|
|
306
|
-
}
|
|
307
|
-
catch {
|
|
308
|
-
// ignore — will fall back to /F below
|
|
309
|
-
}
|
|
240
|
+
// 先清理旧计划任务 / Run 项并结束遗留 Agent:否则下次登录会多出一个实例与服务抢占 Relay 会话。
|
|
241
|
+
if (!removeLegacyWindowsStartup(resolvedProfile, {
|
|
242
|
+
execFileSyncImpl: deps.execFileSyncImpl,
|
|
243
|
+
onQueryTimeout: deps.onQueryTimeout,
|
|
244
|
+
stopLegacyProcess: deps.killProcess ? (target) => { deps.killProcess?.(target); } : undefined,
|
|
245
|
+
})) {
|
|
246
|
+
throw new Error(`Unable to remove the legacy Windows startup entry ${serviceName}`);
|
|
310
247
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
execFileSync("powershell", ["-NoProfile", "-Command", "Start-Sleep -Seconds 3"], { stdio: "pipe" });
|
|
248
|
+
if (!stopServiceAndWait(serviceName, deps)) {
|
|
249
|
+
throw new Error(`Windows service ${serviceName} did not stop; cannot replace its host executable`);
|
|
314
250
|
}
|
|
315
|
-
|
|
316
|
-
|
|
251
|
+
(deps.killProcess ?? killWindowsProcess)(resolvedProfile);
|
|
252
|
+
compileServiceHost(resolvedProfile, deps);
|
|
253
|
+
writeServiceHostConfig(resolvedProfile, deps);
|
|
254
|
+
// 已存在的服务保留原账户与凭据,更新/重装不需要再次输入密码。
|
|
255
|
+
if (queryWindowsServiceState(serviceName, deps) === "missing") {
|
|
256
|
+
createService(resolvedProfile, serviceName, deps);
|
|
317
257
|
}
|
|
318
|
-
|
|
319
|
-
for (const pid of pids) {
|
|
320
|
-
try {
|
|
321
|
-
execFileSync("taskkill", ["/F", "/PID", String(pid)], { stdio: "pipe" });
|
|
322
|
-
}
|
|
323
|
-
catch {
|
|
324
|
-
// ignore — probably already exited gracefully
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return true;
|
|
258
|
+
return startServiceAndWait(serviceName, deps);
|
|
328
259
|
}
|
|
329
260
|
catch (err) {
|
|
330
|
-
console.error("[clawconnect] Failed to
|
|
261
|
+
console.error("[clawconnect] Failed to install Windows service:", err instanceof Error ? err.message : err);
|
|
331
262
|
return false;
|
|
332
263
|
}
|
|
333
264
|
}
|
|
334
|
-
export function
|
|
265
|
+
export function restartWindowsService(profile, deps = {}) {
|
|
335
266
|
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
336
|
-
const
|
|
267
|
+
const serviceName = windowsServiceName(resolvedProfile);
|
|
268
|
+
if (queryWindowsServiceState(serviceName, deps) === "missing") {
|
|
269
|
+
return installWindowsService(resolvedProfile, deps);
|
|
270
|
+
}
|
|
271
|
+
if (!requireElevation("Restarting", resolvedProfile, deps)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
337
274
|
try {
|
|
338
|
-
if (!
|
|
339
|
-
throw new Error(`
|
|
340
|
-
}
|
|
341
|
-
try {
|
|
342
|
-
execFileSync("schtasks", ["/end", "/tn", taskName], { stdio: "pipe" });
|
|
343
|
-
}
|
|
344
|
-
catch {
|
|
345
|
-
// First install or already stopped.
|
|
346
|
-
}
|
|
347
|
-
killWindowsProcess(resolvedProfile);
|
|
348
|
-
// Keep the scheduled task command short. schtasks limits /tr to 261
|
|
349
|
-
// characters, while NVM/npm paths plus UTF-8 bootstrap and log redirects
|
|
350
|
-
// can easily exceed it when embedded inline.
|
|
351
|
-
const runnerPath = refreshWindowsServiceRunner(resolvedProfile);
|
|
352
|
-
const taskCommand = buildWindowsServiceTaskCommand(runnerPath);
|
|
353
|
-
const taskArguments = taskCommand.slice("powershell.exe ".length);
|
|
354
|
-
// ── Attempt 1: PowerShell ScheduledTasks module ──────────────────────
|
|
355
|
-
// Supports RestartOnFailure (crash recovery) and full settings control.
|
|
356
|
-
// Available on Windows 10 / Server 2016+.
|
|
357
|
-
try {
|
|
358
|
-
// We use 'powershell -WindowStyle Hidden' to launch node without a visible console window.
|
|
359
|
-
// This is the most reliable native way to run a background console app on login.
|
|
360
|
-
// Stdout/stderr are redirected to log files, matching the behavior of the
|
|
361
|
-
// Linux (systemd/nohup) and macOS (launchd) service managers.
|
|
362
|
-
const psScript = [
|
|
363
|
-
`$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument ${qps(taskArguments)}`,
|
|
364
|
-
`$trigger = New-ScheduledTaskTrigger -AtLogon`,
|
|
365
|
-
`$principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType Interactive -RunLevel Limited`,
|
|
366
|
-
`$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries`,
|
|
367
|
-
`Register-ScheduledTask -TaskName ${qps(taskName)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null`,
|
|
368
|
-
].join("\n");
|
|
369
|
-
execFileSync("powershell", ["-NoProfile", "-Command", psScript], { stdio: "pipe" });
|
|
370
|
-
removeWindowsStartupEntry(taskName);
|
|
371
|
-
startScheduledTaskOrRunner(taskName, runnerPath);
|
|
372
|
-
return true;
|
|
275
|
+
if (!stopServiceAndWait(serviceName, deps)) {
|
|
276
|
+
throw new Error(`Windows service ${serviceName} did not stop`);
|
|
373
277
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// ── Attempt 2: Basic schtasks (no crash recovery) ────────────────────
|
|
378
|
-
try {
|
|
379
|
-
execFileSync("schtasks", [
|
|
380
|
-
"/create",
|
|
381
|
-
"/tn",
|
|
382
|
-
taskName,
|
|
383
|
-
"/tr",
|
|
384
|
-
taskCommand,
|
|
385
|
-
"/sc",
|
|
386
|
-
"onlogon",
|
|
387
|
-
"/rl",
|
|
388
|
-
"LIMITED",
|
|
389
|
-
"/f",
|
|
390
|
-
], { stdio: "pipe" });
|
|
391
|
-
removeWindowsStartupEntry(taskName);
|
|
392
|
-
startScheduledTaskOrRunner(taskName, runnerPath);
|
|
393
|
-
return true;
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
// Some Windows policies reserve Task Scheduler registration for
|
|
397
|
-
// administrators. HKCU Run is a per-user startup mechanism and does not
|
|
398
|
-
// require elevation.
|
|
399
|
-
}
|
|
400
|
-
// An older task can be readable but not replaceable by the current
|
|
401
|
-
// unelevated shell. Keep that login trigger instead of installing a second
|
|
402
|
-
// autostart path, and launch the newly written runner immediately.
|
|
403
|
-
if (hasWindowsScheduledTask(taskName)) {
|
|
404
|
-
removeWindowsStartupEntry(taskName);
|
|
405
|
-
startScheduledTaskOrRunner(taskName, runnerPath);
|
|
406
|
-
return true;
|
|
407
|
-
}
|
|
408
|
-
installWindowsStartupEntry(taskName, taskCommand, runnerPath);
|
|
409
|
-
return true;
|
|
278
|
+
// npm install -g 可能改变 Node 或全局包路径;重启必须刷新宿主配置,不能继续执行旧入口。
|
|
279
|
+
writeServiceHostConfig(resolvedProfile, deps);
|
|
280
|
+
return startServiceAndWait(serviceName, deps);
|
|
410
281
|
}
|
|
411
282
|
catch (err) {
|
|
412
|
-
console.error("[clawconnect] Failed to
|
|
283
|
+
console.error("[clawconnect] Failed to restart Windows service:", err instanceof Error ? err.message : err);
|
|
413
284
|
return false;
|
|
414
285
|
}
|
|
415
286
|
}
|
|
416
|
-
export function
|
|
287
|
+
export function stopWindowsService(profile, deps = {}) {
|
|
417
288
|
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
418
|
-
const
|
|
419
|
-
const
|
|
289
|
+
const serviceName = windowsServiceName(resolvedProfile);
|
|
290
|
+
const state = queryWindowsServiceState(serviceName, deps);
|
|
291
|
+
let changed = false;
|
|
292
|
+
if (state === "running" || state === "pending") {
|
|
293
|
+
if (!requireElevation("Stopping", resolvedProfile, deps)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
changed = stopServiceAndWait(serviceName, deps);
|
|
297
|
+
}
|
|
298
|
+
// 前台手动启动的 Agent 不归服务管,stop 也要一并结束。
|
|
299
|
+
return (deps.killProcess ?? killWindowsProcess)(resolvedProfile) || changed;
|
|
300
|
+
}
|
|
301
|
+
export function uninstallWindowsService(profile, deps = {}) {
|
|
302
|
+
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
303
|
+
const serviceName = windowsServiceName(resolvedProfile);
|
|
304
|
+
const state = queryWindowsServiceState(serviceName, deps);
|
|
305
|
+
if (state !== "missing" && !requireElevation("Removing", resolvedProfile, deps)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
420
308
|
try {
|
|
421
309
|
let changed = false;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// task might not exist
|
|
428
|
-
}
|
|
429
|
-
if (killWindowsProcess(resolvedProfile)) {
|
|
430
|
-
changed = true;
|
|
431
|
-
}
|
|
432
|
-
if (removeWindowsStartupEntry(taskName)) {
|
|
310
|
+
if (state !== "missing") {
|
|
311
|
+
if (!stopServiceAndWait(serviceName, deps)) {
|
|
312
|
+
throw new Error(`Windows service ${serviceName} did not stop`);
|
|
313
|
+
}
|
|
314
|
+
runServiceControl(["delete", serviceName], deps);
|
|
433
315
|
changed = true;
|
|
434
316
|
}
|
|
435
|
-
|
|
436
|
-
unlinkSync(runnerPath);
|
|
317
|
+
if ((deps.killProcess ?? killWindowsProcess)(resolvedProfile)) {
|
|
437
318
|
changed = true;
|
|
438
319
|
}
|
|
439
|
-
|
|
440
|
-
|
|
320
|
+
for (const path of [
|
|
321
|
+
windowsServiceHostExePath(resolvedProfile),
|
|
322
|
+
windowsServiceHostSourcePath(resolvedProfile),
|
|
323
|
+
windowsServiceHostConfigPath(resolvedProfile),
|
|
324
|
+
windowsServiceRunnerPath(resolvedProfile),
|
|
325
|
+
]) {
|
|
326
|
+
if (existsSync(path)) {
|
|
327
|
+
rmSync(path, { force: true });
|
|
328
|
+
changed = true;
|
|
329
|
+
}
|
|
441
330
|
}
|
|
442
331
|
return changed;
|
|
443
332
|
}
|
|
444
333
|
catch (err) {
|
|
445
|
-
console.error("[clawconnect] Failed to uninstall Windows
|
|
334
|
+
console.error("[clawconnect] Failed to uninstall Windows service:", err instanceof Error ? err.message : err);
|
|
446
335
|
return false;
|
|
447
336
|
}
|
|
448
337
|
}
|
|
449
|
-
export function
|
|
338
|
+
export function getWindowsServiceStatus(profile, deps = {}) {
|
|
450
339
|
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
console.error(`[clawconnect] Failed to remove legacy Windows startup ${TASK_NAME}`);
|
|
454
|
-
return false;
|
|
455
|
-
}
|
|
456
|
-
// Try to stop the task first
|
|
457
|
-
try {
|
|
458
|
-
execFileSync("schtasks", ["/end", "/tn", taskName], { stdio: "pipe" });
|
|
459
|
-
}
|
|
460
|
-
catch {
|
|
461
|
-
// ignore if not running
|
|
462
|
-
}
|
|
463
|
-
killWindowsProcess(resolvedProfile);
|
|
464
|
-
let runnerPath;
|
|
465
|
-
try {
|
|
466
|
-
// `npm install -g` 可能改变 Node 或全局包路径;普通 restart 也必须刷新 runner,不能继续执行旧版本入口。
|
|
467
|
-
runnerPath = refreshWindowsServiceRunner(resolvedProfile);
|
|
468
|
-
}
|
|
469
|
-
catch (error) {
|
|
470
|
-
console.error("[clawconnect] Failed to refresh Windows service runner:", error);
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
if (hasWindowsStartupEntry(taskName)) {
|
|
474
|
-
return startWindowsRunner(runnerPath);
|
|
475
|
-
}
|
|
476
|
-
try {
|
|
477
|
-
execFileSync("schtasks", ["/run", "/tn", taskName], { stdio: "pipe" });
|
|
478
|
-
return true;
|
|
479
|
-
}
|
|
480
|
-
catch {
|
|
481
|
-
// If the task doesn't exist, install it
|
|
482
|
-
return installWindowsService(resolvedProfile);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
export function stopWindowsService(profile) {
|
|
486
|
-
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
487
|
-
const taskName = windowsTaskName(resolvedProfile);
|
|
488
|
-
try {
|
|
489
|
-
execFileSync("schtasks", ["/end", "/tn", taskName], { stdio: "pipe" });
|
|
490
|
-
}
|
|
491
|
-
catch {
|
|
492
|
-
// ignore if not running
|
|
493
|
-
}
|
|
494
|
-
return killWindowsProcess(resolvedProfile);
|
|
495
|
-
}
|
|
496
|
-
export function getWindowsServiceStatus(profile, options = {}) {
|
|
497
|
-
const resolvedProfile = normalizeProfileName(profile ?? getActiveProfile());
|
|
498
|
-
const taskName = windowsTaskName(resolvedProfile);
|
|
499
|
-
const logPath = getProfileLogPath(resolvedProfile);
|
|
500
|
-
const execFileSyncImpl = options.execFileSyncImpl ?? execFileSync;
|
|
501
|
-
const onQueryTimeout = options.onQueryTimeout ?? reportWindowsQueryTimeout;
|
|
502
|
-
const installedViaTask = hasWindowsScheduledTask(taskName, execFileSyncImpl, onQueryTimeout);
|
|
503
|
-
const installedViaStartup = hasWindowsStartupEntry(taskName, execFileSyncImpl, onQueryTimeout);
|
|
504
|
-
const installed = installedViaTask || installedViaStartup;
|
|
505
|
-
// Task Scheduler already tracks the blocking PowerShell runner. Prefer that
|
|
506
|
-
// state over Win32_Process.CommandLine, which can be blank under Windows
|
|
507
|
-
// privacy/UAC policies even for processes owned by the current user.
|
|
508
|
-
let running = installedViaTask
|
|
509
|
-
&& isWindowsScheduledTaskRunning(taskName, execFileSyncImpl, onQueryTimeout);
|
|
510
|
-
if (installed && !running) {
|
|
511
|
-
try {
|
|
512
|
-
const scriptPath = options.scriptPath ?? process.argv[1];
|
|
513
|
-
if (scriptPath) {
|
|
514
|
-
const psScript = buildWindowsProcessQueryScript(resolvedProfile);
|
|
515
|
-
const output = execFileSyncImpl("powershell", ["-NoProfile", "-Command", psScript], {
|
|
516
|
-
stdio: "pipe",
|
|
517
|
-
env: windowsProcessQueryEnv(resolvedProfile, scriptPath),
|
|
518
|
-
timeout: WINDOWS_READ_ONLY_QUERY_TIMEOUT_MS,
|
|
519
|
-
windowsHide: true,
|
|
520
|
-
})
|
|
521
|
-
.toString()
|
|
522
|
-
.trim();
|
|
523
|
-
running = /\d+/.test(output);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
catch (error) {
|
|
527
|
-
if (isProcessTimeoutError(error)) {
|
|
528
|
-
onQueryTimeout("running process");
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
}
|
|
340
|
+
const serviceName = windowsServiceName(resolvedProfile);
|
|
341
|
+
const state = queryWindowsServiceState(serviceName, deps);
|
|
532
342
|
return {
|
|
533
343
|
platform: "windows",
|
|
534
|
-
installed,
|
|
535
|
-
running,
|
|
536
|
-
serviceName
|
|
537
|
-
manager:
|
|
538
|
-
servicePath:
|
|
539
|
-
logPath,
|
|
540
|
-
startHint:
|
|
541
|
-
? `schtasks /run /tn "${taskName}"`
|
|
542
|
-
: buildWindowsServiceTaskCommand(windowsServiceRunnerPath(resolvedProfile)),
|
|
344
|
+
installed: state !== "missing",
|
|
345
|
+
running: state === "running",
|
|
346
|
+
serviceName,
|
|
347
|
+
manager: "Windows Service",
|
|
348
|
+
servicePath: windowsServiceHostExePath(resolvedProfile),
|
|
349
|
+
logPath: getProfileLogPath(resolvedProfile),
|
|
350
|
+
startHint: `sc.exe start "${serviceName}"`,
|
|
543
351
|
};
|
|
544
352
|
}
|
|
545
353
|
//# sourceMappingURL=service-manager-windows.js.map
|