dsh-wechat-remote 1.7.5 → 1.7.8
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 +16 -12
- package/assets/plugin.tgz +0 -0
- package/assets/release.json +8 -10
- package/bin/dsh-discovery.mjs +175 -0
- package/bin/native-control.mjs +4 -2
- package/bin/native-recovery.mjs +44 -0
- package/bin/release-selection.mjs +36 -33
- package/bin/setup.mjs +129 -61
- package/lib/install-control.js +1 -1
- package/lib/install-profile.js +113 -159
- package/lib/install-runtime.js +17 -0
- package/lib/native-recovery.js +7478 -0
- package/lib/update-policy.js +26 -27
- package/lib/update-worker.js +295 -176
- package/package.json +1 -1
package/lib/update-worker.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* Generated from the shared plugin installation sources. */
|
|
2
2
|
|
|
3
3
|
// src/update-worker.ts
|
|
4
|
-
import
|
|
5
|
-
import
|
|
4
|
+
import fs4 from "node:fs";
|
|
5
|
+
import path4 from "node:path";
|
|
6
6
|
import http from "node:http";
|
|
7
|
-
import { spawn as
|
|
7
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
8
8
|
import { createHash as createHash2, createPublicKey } from "node:crypto";
|
|
9
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
10
|
|
|
11
11
|
// src/secure-file.ts
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
@@ -76,62 +76,70 @@ function writePrivateJsonAtomic(file, value) {
|
|
|
76
76
|
// src/install-profile.ts
|
|
77
77
|
import fs from "node:fs";
|
|
78
78
|
import path from "node:path";
|
|
79
|
-
import {
|
|
80
|
-
import {
|
|
81
|
-
|
|
82
|
-
// src/install-runtime.ts
|
|
83
|
-
var INSTALL_PNPM_VERSION = "11.22.0";
|
|
84
|
-
|
|
85
|
-
// src/install-profile.ts
|
|
79
|
+
import { createHash, randomBytes as randomBytes2 } from "node:crypto";
|
|
80
|
+
import { spawn, execFile } from "node:child_process";
|
|
86
81
|
var PLUGIN_PACKAGE = "@harness-remote/dsh-wechat-remote";
|
|
87
82
|
function safeProfileName(value) {
|
|
88
83
|
return /^[A-Za-z0-9_-]{1,80}$/.test(value);
|
|
89
84
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
85
|
+
function backupProfile(profile, backup) {
|
|
86
|
+
if (fs.existsSync(backup)) throw new Error("\u672C\u6B21\u5B89\u88C5\u5907\u4EFD\u5DF2\u5B58\u5728\uFF0C\u672A\u8986\u76D6\u3002");
|
|
87
|
+
fs.cpSync(profile, backup, {
|
|
88
|
+
recursive: true,
|
|
89
|
+
dereference: false,
|
|
90
|
+
verbatimSymlinks: true,
|
|
91
|
+
mode: fs.constants.COPYFILE_FICLONE,
|
|
92
|
+
filter(source, target) {
|
|
93
|
+
if (!fs.lstatSync(source).isSymbolicLink()) return true;
|
|
94
|
+
const type = process.platform === "win32" ? fs.statSync(source, { throwIfNoEntry: false })?.isDirectory() ? "junction" : "file" : void 0;
|
|
95
|
+
fs.symlinkSync(fs.readlinkSync(source), target, type);
|
|
96
|
+
return false;
|
|
101
97
|
}
|
|
102
|
-
};
|
|
103
|
-
walk(root);
|
|
98
|
+
});
|
|
104
99
|
}
|
|
105
100
|
function installToolPath(directory, runtime) {
|
|
106
101
|
const bin = path.join(directory, "tool-bin");
|
|
107
102
|
fs.mkdirSync(bin, { mode: 448 });
|
|
108
|
-
const runner = path.join(bin, "pnpm-run.cjs");
|
|
109
|
-
fs.writeFileSync(runner, `require('node:child_process').spawnSync(${JSON.stringify(runtime.executable)},[${JSON.stringify(runtime.cli)},...process.argv.slice(2)],{stdio:'inherit',shell:false,windowsHide:true}).status===0?process.exit(0):process.exit(1)
|
|
110
|
-
`, { mode: 384 });
|
|
111
103
|
if (process.platform === "win32") {
|
|
112
|
-
if (/["\r\n]/.test(runtime.executable)) throw new Error("
|
|
113
|
-
fs.writeFileSync(path.join(bin, "pnpm.cmd"),
|
|
114
|
-
"${runtime.executable.replace(/%/g, "%%")}" "%~dp0pnpm-run.cjs" %*\r
|
|
115
|
-
`, { mode: 448 });
|
|
104
|
+
if (/["\r\n]/.test(runtime.executable + runtime.cli)) throw new Error("\u5B89\u88C5\u5DE5\u5177\u8DEF\u5F84\u5305\u542B\u65E0\u6548\u5B57\u7B26\u3002");
|
|
105
|
+
fs.writeFileSync(path.join(bin, "pnpm.cmd"), '@echo off\r\nsetlocal DisableDelayedExpansion\r\n"%HARNESS_INSTALL_NODE%" "%HARNESS_INSTALL_PNPM%" %*\r\n', { mode: 448 });
|
|
116
106
|
} else {
|
|
117
|
-
|
|
118
|
-
fs.writeFileSync(path.join(bin, "pnpm"), `#!/bin/sh
|
|
119
|
-
exec ${quote(runtime.executable)} ${quote(runner)} "$@"
|
|
120
|
-
`, { mode: 448 });
|
|
107
|
+
fs.writeFileSync(path.join(bin, "pnpm"), '#!/bin/sh\nexec "$HARNESS_INSTALL_NODE" "$HARNESS_INSTALL_PNPM" "$@"\n', { mode: 448 });
|
|
121
108
|
}
|
|
122
109
|
return bin;
|
|
123
110
|
}
|
|
124
|
-
|
|
125
|
-
|
|
111
|
+
var NativeInstallError = class extends Error {
|
|
112
|
+
constructor(message, mayStillBeRunning = false) {
|
|
113
|
+
super(message);
|
|
114
|
+
this.mayStillBeRunning = mayStillBeRunning;
|
|
115
|
+
this.name = "NativeInstallError";
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
function runNativePlugin(cli, profile, home, toolPath, runtime, logFile, archiveName, timeoutMs = 6e5, operation = "add") {
|
|
119
|
+
if (!safeProfileName(profile) || !/^harness-remote-[\w.+-]+\.tgz$/.test(archiveName)) throw new Error("\u65E0\u6548\u7684\u5B89\u88C5\u76EE\u6807\u3002");
|
|
126
120
|
return new Promise((resolve, reject) => {
|
|
127
121
|
const log = fs.openSync(logFile, "a", 384);
|
|
122
|
+
const env = { ...process.env };
|
|
123
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path");
|
|
124
|
+
const inheritedPath = pathKey ? env[pathKey] : "";
|
|
125
|
+
if (process.platform === "win32") {
|
|
126
|
+
for (const key of Object.keys(env)) if (key.toLowerCase() === "path") delete env[key];
|
|
127
|
+
}
|
|
128
|
+
Object.assign(env, {
|
|
129
|
+
DSH_HOME: home,
|
|
130
|
+
PATH: toolPath + path.delimiter + (inheritedPath || ""),
|
|
131
|
+
HARNESS_INSTALL_NODE: runtime.executable,
|
|
132
|
+
HARNESS_INSTALL_PNPM: runtime.cli,
|
|
133
|
+
CI: "true",
|
|
134
|
+
COREPACK_ENABLE_AUTO_PIN: "0",
|
|
135
|
+
npm_config_manage_package_manager_versions: "false"
|
|
136
|
+
});
|
|
128
137
|
const child = spawn(runtime.executable, [
|
|
129
138
|
cli,
|
|
130
139
|
"plugin",
|
|
131
140
|
"--profile",
|
|
132
141
|
profile,
|
|
133
|
-
"add",
|
|
134
|
-
"file:harness-remote-update.tgz",
|
|
142
|
+
...operation === "install" ? ["install"] : ["add", `file:${archiveName}`],
|
|
135
143
|
"--ignore-scripts",
|
|
136
144
|
"--config.frozen-lockfile=false",
|
|
137
145
|
"--prefer-offline",
|
|
@@ -141,78 +149,142 @@ function runNativePlugin(cli, profile, home, toolPath, runtime, logFile) {
|
|
|
141
149
|
cwd: home,
|
|
142
150
|
shell: false,
|
|
143
151
|
windowsHide: true,
|
|
152
|
+
detached: process.platform !== "win32",
|
|
144
153
|
stdio: ["ignore", log, log],
|
|
145
|
-
env
|
|
146
|
-
...process.env,
|
|
147
|
-
DSH_HOME: home,
|
|
148
|
-
PATH: toolPath + path.delimiter + (process.env.PATH || ""),
|
|
149
|
-
CI: "true",
|
|
150
|
-
COREPACK_ENABLE_AUTO_PIN: "0",
|
|
151
|
-
npm_config_manage_package_manager_versions: "false"
|
|
152
|
-
}
|
|
154
|
+
env
|
|
153
155
|
});
|
|
154
156
|
fs.closeSync(log);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
157
|
+
let finished = false, terminating = false, closed = false, terminationTimer;
|
|
158
|
+
const fail = (message, uncertain = false) => finish(new NativeInstallError(`${message} \u5B89\u88C5\u65E5\u5FD7\uFF1A${logFile}`, uncertain));
|
|
159
|
+
const finish = (error) => {
|
|
160
|
+
if (finished) return;
|
|
161
|
+
finished = true;
|
|
160
162
|
clearTimeout(timer);
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
clearTimeout(terminationTimer);
|
|
164
|
+
error ? reject(error) : resolve();
|
|
165
|
+
};
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
if (closed) return;
|
|
168
|
+
terminating = true;
|
|
169
|
+
terminationTimer = setTimeout(() => fail("\u5B89\u88C5\u8D85\u65F6\uFF0C\u5C1A\u4E0D\u80FD\u786E\u8BA4\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\uFF0C\u8BF7\u52FF\u91CD\u590D\u5B89\u88C5\u3002", true), 15e3);
|
|
170
|
+
if (process.platform === "win32") {
|
|
171
|
+
if (!child.pid) return fail("\u5B89\u88C5\u7A0B\u5E8F\u672A\u542F\u52A8\u3002");
|
|
172
|
+
execFile(
|
|
173
|
+
path.join(process.env.SystemRoot || "C:\\Windows", "System32", "taskkill.exe"),
|
|
174
|
+
["/PID", String(child.pid), "/T", "/F"],
|
|
175
|
+
{ windowsHide: true, timeout: 1e4 },
|
|
176
|
+
(error) => {
|
|
177
|
+
if (error) fail("\u5B89\u88C5\u8D85\u65F6\uFF0C\u65E0\u6CD5\u786E\u8BA4\u5B89\u88C5\u8FDB\u7A0B\u6811\u5DF2\u9000\u51FA\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\uFF0C\u8BF7\u52FF\u91CD\u590D\u5B89\u88C5\u3002", true);
|
|
178
|
+
else if (closed) fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002");
|
|
179
|
+
else child.once("close", () => fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002"));
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
try {
|
|
184
|
+
if (child.pid) process.kill(-child.pid, "SIGKILL");
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error.code !== "ESRCH") return fail("\u65E0\u6CD5\u505C\u6B62\u8D85\u65F6\u7684\u5B89\u88C5\u8FDB\u7A0B\uFF1B\u672A\u81EA\u52A8\u56DE\u9000\u3002", true);
|
|
187
|
+
}
|
|
188
|
+
if (closed) fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002");
|
|
189
|
+
else child.once("close", () => fail("\u4E0B\u8F7D\u6216\u5B89\u88C5\u8D85\u65F6\uFF0C\u5B89\u88C5\u8FDB\u7A0B\u5DF2\u505C\u6B62\u3002"));
|
|
190
|
+
}
|
|
191
|
+
}, timeoutMs);
|
|
192
|
+
child.once("error", (error) => fail(`\u65E0\u6CD5\u542F\u52A8 DSH \u539F\u751F\u5B89\u88C5\u7A0B\u5E8F\uFF08${error.code || error.name}\uFF09\u3002`));
|
|
193
|
+
child.once("close", (code, signal) => {
|
|
194
|
+
closed = true;
|
|
195
|
+
if (terminating) return;
|
|
196
|
+
if (code === 0) finish();
|
|
197
|
+
else fail(`DSH \u539F\u751F\u5B89\u88C5\u672A\u5B8C\u6210\uFF08${signal ? `\u4FE1\u53F7 ${signal}` : `\u9000\u51FA\u7801 ${code}`}\uFF09\u3002`);
|
|
166
198
|
});
|
|
167
199
|
});
|
|
168
200
|
}
|
|
169
|
-
async function
|
|
170
|
-
const scope = path.basename(job.profile);
|
|
171
|
-
if (!safeProfileName(scope) ||
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
filter: (p) => !["node_modules", ".harness-remote-update.lock"].includes(path.basename(p))
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
let before = null;
|
|
183
|
-
const filename = path.join(staged, "package.json");
|
|
184
|
-
if (fs.existsSync(filename)) {
|
|
185
|
-
before = JSON.parse(fs.readFileSync(filename, "utf8"));
|
|
186
|
-
for (const [name, spec] of Object.entries(before.dependencies || {})) {
|
|
187
|
-
if (name !== PLUGIN_PACKAGE && !/^[~^]?\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(String(spec))) throw new Error("\u5176\u4ED6\u63D2\u4EF6\u4F7F\u7528\u4E86\u672C\u5730\u94FE\u63A5\u6216\u7279\u6B8A\u6765\u6E90\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
|
|
188
|
-
}
|
|
189
|
-
for (const e of fs.readdirSync(staged, { withFileTypes: true })) if (e.isSymbolicLink()) throw new Error("profile \u914D\u7F6E\u5305\u542B\u94FE\u63A5\uFF0C\u672A\u4FEE\u6539\u5F53\u524D\u5B89\u88C5\u3002");
|
|
190
|
-
if (before.packageManager && !/^pnpm@\d+\.\d+\.\d+(?:\+.*)?$/.test(before.packageManager)) throw new Error("\u5F53\u524D profile \u4F7F\u7528\u4E86\u5176\u4ED6\u5305\u7BA1\u7406\u5668\uFF0C\u672A\u4FEE\u6539\u5B89\u88C5\u3002");
|
|
191
|
-
writePrivateJsonAtomic(filename, { ...before, dependencies: {
|
|
192
|
-
...before.dependencies,
|
|
193
|
-
[PLUGIN_PACKAGE]: "file:harness-remote-update.tgz"
|
|
194
|
-
}, packageManager: `pnpm@${INSTALL_PNPM_VERSION}` });
|
|
195
|
-
}
|
|
196
|
-
fs.copyFileSync(path.join(job.directory, "release.tgz"), path.join(staged, "harness-remote-update.tgz"));
|
|
201
|
+
async function installProfile(job) {
|
|
202
|
+
const scope = path.basename(job.profile), home = path.dirname(path.dirname(job.profile));
|
|
203
|
+
if (!safeProfileName(scope) || path.dirname(job.profile) !== path.join(home, "profiles") || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u5B89\u88C5\u76EE\u6807\u4E0D\u660E\u786E\u3002");
|
|
204
|
+
fs.mkdirSync(job.profile, { recursive: true, mode: 448 });
|
|
205
|
+
const archive = path.join(job.directory, "release.tgz");
|
|
206
|
+
const digest = createHash("sha256").update(fs.readFileSync(archive)).digest("hex");
|
|
207
|
+
const archiveBase = `harness-remote-${job.targetVersion}-${digest}`;
|
|
208
|
+
const suffix = fs.existsSync(path.join(job.profile, `${archiveBase}.tgz`)) ? `-${randomBytes2(8).toString("hex")}` : "";
|
|
209
|
+
const archiveName = `${archiveBase}${suffix}.tgz`;
|
|
210
|
+
fs.copyFileSync(archive, path.join(job.profile, archiveName), fs.constants.COPYFILE_EXCL);
|
|
197
211
|
const tools = installToolPath(job.directory, job.runtime);
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
212
|
+
const logFile = path.join(job.directory, "install.log");
|
|
213
|
+
const deadline = Date.now() + 6e5;
|
|
214
|
+
const run2 = (operation = "add") => runNativePlugin(job.cli, scope, home, tools, job.runtime, logFile, archiveName, Math.max(1, deadline - Date.now()), operation);
|
|
215
|
+
try {
|
|
216
|
+
await run2();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const layoutError = /^\s*(?:\[ERR_PNPM_|ERR_PNPM_)(?:UNEXPECTED_VIRTUAL_STORE|UNEXPECTED_STORE|MODULES_BREAKING_CHANGE|VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF|PUBLIC_HOIST_PATTERN_DIFF|HOIST_PATTERN_DIFF)(?:\]|\s)/m;
|
|
219
|
+
if (!(error instanceof NativeInstallError) || error.mayStillBeRunning || !layoutError.test(fs.readFileSync(logFile, "utf8"))) throw error;
|
|
220
|
+
fs.appendFileSync(logFile, "\nRestoring native pnpm layout with dsh plugin install.\n");
|
|
221
|
+
await run2("install");
|
|
222
|
+
await run2();
|
|
223
|
+
}
|
|
224
|
+
const installed = path.join(job.profile, "node_modules", PLUGIN_PACKAGE);
|
|
202
225
|
if (JSON.parse(fs.readFileSync(path.join(installed, "package.json"), "utf8")).version !== job.targetVersion) throw new Error("\u5B89\u88C5\u540E\u63D2\u4EF6\u7248\u672C\u4E0D\u5339\u914D\u3002");
|
|
203
|
-
const after = JSON.parse(fs.readFileSync(
|
|
226
|
+
const after = JSON.parse(fs.readFileSync(path.join(job.profile, "package.json"), "utf8"));
|
|
204
227
|
if (!after.dsh?.profile?.bundles?.includes(PLUGIN_PACKAGE)) throw new Error("DSH \u5C1A\u672A\u5C06\u63D2\u4EF6\u6CE8\u518C\u4E3A\u539F\u751F profile \u5C42\u3002");
|
|
205
|
-
for (const name of Object.keys(before?.dependencies || {})) if (name !== PLUGIN_PACKAGE) {
|
|
206
|
-
if (hash(path.join(job.profile, "node_modules", name, "package.json")) !== hash(path.join(staged, "node_modules", name, "package.json"))) throw new Error("\u5B89\u88C5\u8BD5\u56FE\u6539\u53D8\u5176\u4ED6\u63D2\u4EF6\uFF0C\u539F\u5B89\u88C5\u4FDD\u6301\u4E0D\u53D8\u3002");
|
|
207
|
-
}
|
|
208
|
-
assertRelocatableProfile(staged);
|
|
209
|
-
return staged;
|
|
210
228
|
}
|
|
211
229
|
|
|
212
|
-
// src/install-
|
|
230
|
+
// src/install-runtime.ts
|
|
213
231
|
import fs2 from "node:fs";
|
|
214
232
|
import path2 from "node:path";
|
|
215
|
-
import {
|
|
233
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
234
|
+
var INSTALL_PNPM_VERSION = "11.22.0";
|
|
235
|
+
async function pinInstallRuntime(runtime, directory) {
|
|
236
|
+
let root = path2.dirname(runtime.cli);
|
|
237
|
+
while (!fs2.existsSync(path2.join(root, "package.json"))) {
|
|
238
|
+
const parent = path2.dirname(root);
|
|
239
|
+
if (parent === root) throw new Error("\u65E0\u6CD5\u5B9A\u4F4D\u5B89\u88C5\u5DE5\u5177\u5305\u3002");
|
|
240
|
+
root = parent;
|
|
241
|
+
}
|
|
242
|
+
const manifest = JSON.parse(fs2.readFileSync(path2.join(root, "package.json"), "utf8"));
|
|
243
|
+
if (manifest.name !== "pnpm" || manifest.version !== runtime.version) throw new Error("\u5B89\u88C5\u5DE5\u5177\u7248\u672C\u4E0D\u4E00\u81F4\u3002");
|
|
244
|
+
const target = path2.join(directory, "install-runtime");
|
|
245
|
+
if (fs2.existsSync(target)) throw new Error("\u672C\u6B21\u5B89\u88C5\u5DE5\u5177\u76EE\u5F55\u5DF2\u5B58\u5728\u3002");
|
|
246
|
+
fs2.cpSync(root, target, { recursive: true, dereference: false, verbatimSymlinks: true, mode: fs2.constants.COPYFILE_FICLONE });
|
|
247
|
+
const pinned = { ...runtime, cli: path2.join(target, path2.relative(root, runtime.cli)) };
|
|
248
|
+
await verifyInstallRuntime(pinned);
|
|
249
|
+
return pinned;
|
|
250
|
+
}
|
|
251
|
+
function verifyInstallRuntime(runtime) {
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const child = spawn2(runtime.executable, [runtime.cli, "--version"], {
|
|
254
|
+
windowsHide: true,
|
|
255
|
+
shell: false,
|
|
256
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
257
|
+
env: { ...process.env, COREPACK_ENABLE_AUTO_PIN: "0", npm_config_manage_package_manager_versions: "false" }
|
|
258
|
+
});
|
|
259
|
+
let stdout = "", size = 0, settled = false;
|
|
260
|
+
const finish = (error) => {
|
|
261
|
+
if (settled) return;
|
|
262
|
+
settled = true;
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
error ? reject(error) : resolve();
|
|
265
|
+
};
|
|
266
|
+
const timer = setTimeout(() => {
|
|
267
|
+
child.kill();
|
|
268
|
+
finish(new Error("\u5B89\u88C5\u5DE5\u5177\u542F\u52A8\u8D85\u65F6\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002"));
|
|
269
|
+
}, 15e3);
|
|
270
|
+
child.stdout.on("data", (b) => {
|
|
271
|
+
size += b.length;
|
|
272
|
+
if (size > 16384) {
|
|
273
|
+
child.kill();
|
|
274
|
+
finish(new Error("\u5B89\u88C5\u5DE5\u5177\u54CD\u5E94\u5F02\u5E38\u3002"));
|
|
275
|
+
} else stdout += b.toString();
|
|
276
|
+
});
|
|
277
|
+
child.stderr.on("data", () => {
|
|
278
|
+
});
|
|
279
|
+
child.once("error", () => finish(new Error("\u65E0\u6CD5\u542F\u52A8\u5B89\u88C5\u5DE5\u5177\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002")));
|
|
280
|
+
child.once("close", (code) => finish(code === 0 && stdout.trim() === runtime.version ? void 0 : new Error("\u5B89\u88C5\u5DE5\u5177\u8FD0\u884C\u9A8C\u8BC1\u5931\u8D25\uFF0C\u672A\u4FEE\u6539\u63D2\u4EF6\u3002")));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/install-lifecycle.ts
|
|
285
|
+
import fs3 from "node:fs";
|
|
286
|
+
import path3 from "node:path";
|
|
287
|
+
import { execFileSync as execFileSync2, spawn as spawn3 } from "node:child_process";
|
|
216
288
|
var serviceName = (s) => /^[A-Za-z0-9_.@-]{1,180}$/.test(s);
|
|
217
289
|
function run(command, args) {
|
|
218
290
|
return execFileSync2(command, args, { encoding: "utf8", windowsHide: true, timeout: 15e3, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
@@ -220,7 +292,7 @@ function run(command, args) {
|
|
|
220
292
|
function validateManager(value) {
|
|
221
293
|
if (value.kind === "process") return;
|
|
222
294
|
if (value.kind === "systemd" && process.platform === "linux" && serviceName(value.unit) && value.unit.endsWith(".service")) return;
|
|
223
|
-
if (value.kind === "launchd" && process.platform === "darwin" && serviceName(value.label) && /^gui\/\d+$/.test(value.domain) && value.domain === `gui/${process.getuid?.()}` &&
|
|
295
|
+
if (value.kind === "launchd" && process.platform === "darwin" && serviceName(value.label) && /^gui\/\d+$/.test(value.domain) && value.domain === `gui/${process.getuid?.()}` && path3.isAbsolute(value.plist) && fs3.statSync(value.plist).isFile()) return;
|
|
224
296
|
throw new Error("\u65E0\u6CD5\u786E\u8BA4\u539F\u540E\u53F0\u670D\u52A1\uFF0C\u672A\u505C\u6B62 DSH\u3002");
|
|
225
297
|
}
|
|
226
298
|
function stopManagedHost(manager) {
|
|
@@ -236,7 +308,7 @@ function startManagedHost(manager) {
|
|
|
236
308
|
else throw new Error("\u666E\u901A DSH \u8FDB\u7A0B\u5FC5\u987B\u7531\u539F\u73AF\u5883\u542F\u52A8\u3002");
|
|
237
309
|
}
|
|
238
310
|
function finishUpdateWorker(manager, directory) {
|
|
239
|
-
const id =
|
|
311
|
+
const id = path3.basename(directory);
|
|
240
312
|
if (manager?.kind !== "launchd" || !/^[a-f0-9]{32}$/.test(id)) return;
|
|
241
313
|
try {
|
|
242
314
|
run("/bin/launchctl", ["remove", `dsh.wechat.update.${id}`]);
|
|
@@ -247,21 +319,21 @@ function finishUpdateWorker(manager, directory) {
|
|
|
247
319
|
// src/update-worker.ts
|
|
248
320
|
function releaseOwnedUpdateLock(lock, id) {
|
|
249
321
|
try {
|
|
250
|
-
if (
|
|
322
|
+
if (fs4.readFileSync(lock, "utf8") === id) fs4.unlinkSync(lock);
|
|
251
323
|
} catch {
|
|
252
324
|
}
|
|
253
325
|
}
|
|
254
326
|
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
255
|
-
var hashFile = (f) => createHash2("sha256").update(
|
|
327
|
+
var hashFile = (f) => createHash2("sha256").update(fs4.readFileSync(f)).digest("hex");
|
|
256
328
|
function within(parent, child) {
|
|
257
|
-
const relative =
|
|
258
|
-
return Boolean(relative) && !relative.startsWith("..") && !
|
|
329
|
+
const relative = path4.relative(parent, child);
|
|
330
|
+
return Boolean(relative) && !relative.startsWith("..") && !path4.isAbsolute(relative);
|
|
259
331
|
}
|
|
260
332
|
function safePlainDirectory(p) {
|
|
261
|
-
if (!
|
|
333
|
+
if (!fs4.statSync(p).isDirectory() || fs4.lstatSync(p).isSymbolicLink() || fs4.realpathSync(p) !== path4.resolve(p)) throw new Error("\u5B89\u88C5\u76EE\u5F55\u4E0D\u662F\u53EF\u5B89\u5168\u66FF\u6362\u7684\u72EC\u7ACB\u76EE\u5F55");
|
|
262
334
|
}
|
|
263
335
|
function validateJob(job) {
|
|
264
|
-
if (!/^[a-f0-9]{32}$/.test(job.id) ||
|
|
336
|
+
if (!/^[a-f0-9]{32}$/.test(job.id) || path4.basename(job.directory) !== job.id || !within(path4.join(job.home, "harness-remote-updates"), job.directory) || !within(path4.join(job.home, "profiles"), job.profile) || path4.dirname(job.profile) !== path4.join(job.home, "profiles") || !within(job.home, job.stateFile) || !Number.isInteger(job.parentPid) || job.parentPid < 1 || ![job.webPort, job.gatePort, job.localPort].every((p) => Number.isInteger(p) && p > 0 && p <= 65535) || job.argv[0] !== job.cli || !/^[\w.+-]{1,80}$/.test(job.targetVersion)) throw new Error("\u66F4\u65B0\u4EFB\u52A1\u8303\u56F4\u6821\u9A8C\u5931\u8D25");
|
|
265
337
|
safePlainDirectory(job.profile);
|
|
266
338
|
safePlainDirectory(job.directory);
|
|
267
339
|
if (job.identityFile && !within(job.home, job.identityFile)) throw new Error("\u8282\u70B9\u8EAB\u4EFD\u6587\u4EF6\u4E0D\u5C5E\u4E8E\u5F53\u524D DSH");
|
|
@@ -270,7 +342,7 @@ function validateJob(job) {
|
|
|
270
342
|
if (u.protocol !== "http:" || u.hostname !== "127.0.0.1" || !u.port || u.username || u.password || u.pathname !== "/" || u.search || u.hash) throw new Error("\u5B89\u88C5\u63A7\u5236\u5730\u5740\u65E0\u6548");
|
|
271
343
|
validateManager(job.manager);
|
|
272
344
|
}
|
|
273
|
-
for (const f of [job.executable, job.cli, job.pnpm, ...job.previousVersion === "0.0.0" ? [] : [job.stateFile]]) if (!
|
|
345
|
+
for (const f of [job.executable, job.cli, job.pnpm, ...job.previousVersion === "0.0.0" ? [] : [job.stateFile]]) if (!fs4.statSync(f).isFile()) throw new Error("\u5B89\u88C5\u8FD0\u884C\u65F6\u5DF2\u53D8\u5316");
|
|
274
346
|
}
|
|
275
347
|
async function control(job, operation, input = {}) {
|
|
276
348
|
let res;
|
|
@@ -296,7 +368,7 @@ async function beforeRpc(job, method, payload = {}) {
|
|
|
296
368
|
return job.controlOrigin ? control(job, "read", { method, payload }) : rpc(job, method, payload);
|
|
297
369
|
}
|
|
298
370
|
async function rpc(job, method, payload = {}, deadline) {
|
|
299
|
-
const state = JSON.parse(
|
|
371
|
+
const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
|
|
300
372
|
const res = await fetch(`http://127.0.0.1:${job.gatePort}/api/${method}`, {
|
|
301
373
|
method: "POST",
|
|
302
374
|
headers: { authorization: `Bearer ${state.token}`, "content-type": "application/json", "x-harness-update-probe": job.statusToken },
|
|
@@ -311,38 +383,38 @@ async function rpc(job, method, payload = {}, deadline) {
|
|
|
311
383
|
function durableSnapshot(job) {
|
|
312
384
|
const result = {};
|
|
313
385
|
const walk = (dir) => {
|
|
314
|
-
if (!
|
|
315
|
-
for (const e of
|
|
316
|
-
const file =
|
|
317
|
-
if (e.isSymbolicLink())
|
|
318
|
-
if (e.isDirectory()) walk(file);
|
|
319
|
-
else if (e.isFile()) result[
|
|
386
|
+
if (!fs4.existsSync(dir)) return;
|
|
387
|
+
for (const e of fs4.readdirSync(dir, { withFileTypes: true })) {
|
|
388
|
+
const file = path4.join(dir, e.name);
|
|
389
|
+
if (e.isSymbolicLink()) result[path4.relative(job.home, file)] = createHash2("sha256").update(fs4.readlinkSync(file)).digest("hex");
|
|
390
|
+
else if (e.isDirectory()) walk(file);
|
|
391
|
+
else if (e.isFile()) result[path4.relative(job.home, file)] = hashFile(file);
|
|
320
392
|
}
|
|
321
393
|
};
|
|
322
|
-
walk(
|
|
323
|
-
walk(
|
|
324
|
-
walk(
|
|
325
|
-
for (const e of
|
|
394
|
+
walk(path4.join(job.home, "sessions"));
|
|
395
|
+
walk(path4.join(job.home, "attachments"));
|
|
396
|
+
walk(path4.join(job.home, "harness-remote"));
|
|
397
|
+
for (const e of fs4.readdirSync(job.home, { withFileTypes: true })) {
|
|
326
398
|
if (e.isFile() && /(?:identity|settings|credentials|public|gate-wechat)/i.test(e.name) && !e.name.endsWith(".log")) {
|
|
327
|
-
const f =
|
|
399
|
+
const f = path4.join(job.home, e.name);
|
|
328
400
|
if (f === job.stateFile) continue;
|
|
329
401
|
result[e.name] = hashFile(f);
|
|
330
402
|
}
|
|
331
403
|
}
|
|
332
|
-
if (
|
|
333
|
-
const state = JSON.parse(
|
|
404
|
+
if (fs4.existsSync(job.stateFile)) {
|
|
405
|
+
const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
|
|
334
406
|
result["$binding"] = createHash2("sha256").update(JSON.stringify([state.token, state.wechatBindings])).digest("hex");
|
|
335
407
|
}
|
|
336
408
|
return result;
|
|
337
409
|
}
|
|
338
410
|
function assertPreserved(before, after) {
|
|
339
|
-
for (const [key,
|
|
411
|
+
for (const [key, hash] of Object.entries(before)) if (after[key] !== hash) throw new Error("\u5347\u7EA7\u540E\u6570\u636E\u6821\u9A8C\u4E0D\u4E00\u81F4\uFF1B\u505C\u6B62\u81EA\u52A8\u64CD\u4F5C\u5E76\u4FDD\u7559\u5907\u4EFD");
|
|
340
412
|
}
|
|
341
413
|
function migrateLegacyGrantOwner(job) {
|
|
342
|
-
if (job.previousVersion !== "1.5.5" || !job.identityFile || !
|
|
343
|
-
const state = JSON.parse(
|
|
414
|
+
if (job.previousVersion !== "1.5.5" || !job.identityFile || !fs4.existsSync(job.identityFile)) return;
|
|
415
|
+
const state = JSON.parse(fs4.readFileSync(job.stateFile, "utf8"));
|
|
344
416
|
if (state.publicIdentityNodeId) return;
|
|
345
|
-
const identity = JSON.parse(
|
|
417
|
+
const identity = JSON.parse(fs4.readFileSync(job.identityFile, "utf8"));
|
|
346
418
|
const publicKey = createPublicKey(identity.privateKeyPem).export({ format: "der", type: "spki" });
|
|
347
419
|
const savedKey = createPublicKey(identity.publicKeyPem).export({ format: "der", type: "spki" });
|
|
348
420
|
const nodeId = createHash2("sha256").update(publicKey).digest().subarray(0, 18).toString("base64url");
|
|
@@ -380,20 +452,20 @@ function start(job) {
|
|
|
380
452
|
startManagedHost(job.manager);
|
|
381
453
|
return;
|
|
382
454
|
}
|
|
383
|
-
const log =
|
|
384
|
-
const child =
|
|
455
|
+
const log = fs4.openSync(path4.join(job.directory, "restart.log"), "a", 384);
|
|
456
|
+
const child = spawn4(job.executable, [...job.execArgv, ...job.argv], {
|
|
385
457
|
cwd: job.cwd,
|
|
386
458
|
env: { ...process.env, HARNESS_REMOTE_UPDATE_JOB: job.directory },
|
|
387
459
|
stdio: ["ignore", log, log],
|
|
388
460
|
detached: true,
|
|
389
461
|
windowsHide: true
|
|
390
462
|
});
|
|
391
|
-
|
|
463
|
+
fs4.closeSync(log);
|
|
392
464
|
child.on("error", () => {
|
|
393
465
|
});
|
|
394
466
|
child.unref();
|
|
395
467
|
if (!child.pid) throw new Error("\u65E0\u6CD5\u542F\u52A8\u539F DSH \u547D\u4EE4");
|
|
396
|
-
writePrivateJsonAtomic(
|
|
468
|
+
writePrivateJsonAtomic(path4.join(job.directory, "restarted-process.json"), { pid: child.pid, cli: job.cli, home: job.home, webPort: job.webPort });
|
|
397
469
|
return child;
|
|
398
470
|
}
|
|
399
471
|
async function stopOriginal(job) {
|
|
@@ -416,7 +488,7 @@ async function stopOriginal(job) {
|
|
|
416
488
|
}
|
|
417
489
|
throw new Error("\u539F DSH \u5C1A\u672A\u7ED3\u675F\uFF0C\u672A\u66FF\u6362\u63D2\u4EF6");
|
|
418
490
|
}
|
|
419
|
-
async function stopRestarted(child, timeoutMs = 3e4) {
|
|
491
|
+
async function stopRestarted(child, timeoutMs = 3e4, forceTimeoutMs = 5e3) {
|
|
420
492
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
421
493
|
child.kill("SIGTERM");
|
|
422
494
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -425,8 +497,54 @@ async function stopRestarted(child, timeoutMs = 3e4) {
|
|
|
425
497
|
await wait(Math.min(100, Math.max(1, deadline - Date.now())));
|
|
426
498
|
}
|
|
427
499
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
500
|
+
child.kill("SIGKILL");
|
|
501
|
+
const forcedDeadline = Date.now() + forceTimeoutMs;
|
|
502
|
+
while (Date.now() < forcedDeadline) {
|
|
503
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
504
|
+
await wait(Math.min(100, Math.max(1, forcedDeadline - Date.now())));
|
|
505
|
+
}
|
|
506
|
+
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
428
507
|
throw new Error("\u66F4\u65B0\u540E\u7684 DSH \u672A\u6309\u65F6\u505C\u6B62");
|
|
429
508
|
}
|
|
509
|
+
function captureCandidateLock(home, child) {
|
|
510
|
+
if (!child.pid || child.exitCode !== null || child.signalCode !== null) return;
|
|
511
|
+
const filename = path4.join(home, ".credentials.yaml.lock");
|
|
512
|
+
try {
|
|
513
|
+
const info = fs4.lstatSync(filename);
|
|
514
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 32) return;
|
|
515
|
+
const value = fs4.readFileSync(filename, "utf8");
|
|
516
|
+
let owned = value === `${child.pid}
|
|
517
|
+
`;
|
|
518
|
+
if (!owned && value === "" && process.platform === "linux") {
|
|
519
|
+
const directory = `/proc/${child.pid}/fd`;
|
|
520
|
+
owned = fs4.readdirSync(directory).some((fd) => {
|
|
521
|
+
try {
|
|
522
|
+
const file = path4.join(directory, fd);
|
|
523
|
+
if (fs4.readlinkSync(file) !== filename) return false;
|
|
524
|
+
const opened = fs4.statSync(file);
|
|
525
|
+
return opened.dev === info.dev && opened.ino === info.ino;
|
|
526
|
+
} catch {
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (owned) return { filename, dev: info.dev, ino: info.ino, pid: child.pid };
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function retireCandidateLock(lock, child, directory) {
|
|
536
|
+
if (!lock || child.pid !== lock.pid || child.exitCode === null && child.signalCode === null) return;
|
|
537
|
+
try {
|
|
538
|
+
const info = fs4.lstatSync(lock.filename);
|
|
539
|
+
if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.dev !== lock.dev || info.ino !== lock.ino || info.size > 32) return;
|
|
540
|
+
const value = fs4.readFileSync(lock.filename, "utf8");
|
|
541
|
+
if (value !== "" && value !== `${lock.pid}
|
|
542
|
+
`) return;
|
|
543
|
+
const saved = path4.join(directory, "candidate-credentials-lock.before-rollback");
|
|
544
|
+
if (!fs4.existsSync(saved)) fs4.renameSync(lock.filename, saved);
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
430
548
|
async function stopChild(pid) {
|
|
431
549
|
process.kill(pid, "SIGTERM");
|
|
432
550
|
for (let i = 0; i < 100; i++) {
|
|
@@ -441,20 +559,15 @@ async function stopChild(pid) {
|
|
|
441
559
|
}
|
|
442
560
|
async function executeUpdate(job, progress, quiesce) {
|
|
443
561
|
validateJob(job);
|
|
444
|
-
|
|
445
|
-
const previous = path3.join(job.directory, "profile-before");
|
|
562
|
+
const previous = path4.join(job.directory, "profile-before");
|
|
446
563
|
const emit = (phase, n, message) => progress({ phase, progress: n, message, terminal: false });
|
|
447
|
-
let stopped = false, disposed = false,
|
|
564
|
+
let stopped = false, disposed = false, modified = false, newChild;
|
|
448
565
|
let before = {}, sessionIds = [], readableIds = [];
|
|
449
566
|
try {
|
|
450
|
-
emit("
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
cli: job.cli,
|
|
455
|
-
targetVersion: job.targetVersion,
|
|
456
|
-
runtime: { executable: job.executable, cli: job.pnpm, version: INSTALL_PNPM_VERSION }
|
|
457
|
-
});
|
|
567
|
+
emit("preparing", 25, "\u51C6\u5907\u5B89\u88C5\u5DE5\u5177\uFF0C\u5F53\u524D\u8282\u70B9\u4ECD\u53EF\u4F7F\u7528");
|
|
568
|
+
const runtime = await pinInstallRuntime({ executable: job.executable, cli: job.pnpm, version: INSTALL_PNPM_VERSION }, job.directory);
|
|
569
|
+
job.pnpm = runtime.cli;
|
|
570
|
+
writePrivateJsonAtomic(path4.join(job.directory, "job.json"), job);
|
|
458
571
|
emit("checking", 50, "\u786E\u8BA4\u4F1A\u8BDD\u7A7A\u95F2\u5E76\u4FDD\u5B58\u72B6\u6001");
|
|
459
572
|
const old = job.controlOrigin ? await control(job, "describe") : await describe(job);
|
|
460
573
|
if (old.pluginVersion !== job.previousVersion) throw new Error("\u5F53\u524D\u63D2\u4EF6\u5728\u68C0\u67E5\u540E\u53D1\u751F\u53D8\u5316");
|
|
@@ -471,29 +584,17 @@ async function executeUpdate(job, progress, quiesce) {
|
|
|
471
584
|
await quiesce();
|
|
472
585
|
disposed = Boolean(job.controlOrigin);
|
|
473
586
|
before = durableSnapshot(job);
|
|
474
|
-
writePrivateJsonAtomic(
|
|
475
|
-
emit("
|
|
476
|
-
const backup = path3.join(job.directory, "home-before");
|
|
477
|
-
fs3.mkdirSync(backup, { mode: 448 });
|
|
478
|
-
for (const entry of fs3.readdirSync(job.home, { withFileTypes: true })) {
|
|
479
|
-
if (["harness-remote-updates", "profiles"].includes(entry.name)) continue;
|
|
480
|
-
if (entry.isSymbolicLink()) throw new Error("\u6570\u636E\u76EE\u5F55\u5305\u542B\u5916\u90E8\u94FE\u63A5\uFF0C\u8BF7\u624B\u5DE5\u5907\u4EFD\u540E\u66F4\u65B0");
|
|
481
|
-
fs3.cpSync(path3.join(job.home, entry.name), path3.join(backup, entry.name), { recursive: true });
|
|
482
|
-
}
|
|
483
|
-
emit("restarting", 70, "\u6B63\u5728\u91CD\u542F\u5F53\u524D DSH\uFF0C\u8FDE\u63A5\u4F1A\u6682\u65F6\u65AD\u5F00");
|
|
587
|
+
writePrivateJsonAtomic(path4.join(job.directory, "before-hashes.json"), before);
|
|
588
|
+
emit("installing", 60, "\u6B63\u5728\u5B89\u88C5\u63D2\u4EF6\uFF0C\u5F53\u524D DSH \u8FDE\u63A5\u4F1A\u6682\u65F6\u65AD\u5F00");
|
|
484
589
|
await stopOriginal(job);
|
|
485
590
|
stopped = true;
|
|
486
591
|
assertPreserved(before, durableSnapshot(job));
|
|
592
|
+
backupProfile(job.profile, previous);
|
|
487
593
|
migrateLegacyGrantOwner(job);
|
|
488
594
|
safePlainDirectory(job.profile);
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
swapped = true;
|
|
493
|
-
} catch (error) {
|
|
494
|
-
fs3.renameSync(previous, job.profile);
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
595
|
+
modified = true;
|
|
596
|
+
await installProfile({ profile: job.profile, directory: job.directory, cli: job.cli, targetVersion: job.targetVersion, runtime });
|
|
597
|
+
emit("restarting", 75, "\u5B89\u88C5\u5B8C\u6210\uFF0C\u6B63\u5728\u6062\u590D\u5F53\u524D DSH");
|
|
497
598
|
newChild = start(job);
|
|
498
599
|
emit("verifying", 85, "\u68C0\u67E5\u63D2\u4EF6\u7248\u672C\u3001\u8282\u70B9\u8EAB\u4EFD\u548C\u4F1A\u8BDD");
|
|
499
600
|
await healthy(job, job.targetVersion);
|
|
@@ -502,15 +603,23 @@ async function executeUpdate(job, progress, quiesce) {
|
|
|
502
603
|
const after = (await rpc(job, "session.list")).items.map((s) => s.sessionId).sort();
|
|
503
604
|
if (JSON.stringify(after) !== JSON.stringify(sessionIds)) throw new Error("\u91CD\u542F\u540E\u4F1A\u8BDD\u5217\u8868\u4E0D\u4E00\u81F4");
|
|
504
605
|
for (const id of readableIds) await rpc(job, "session.history", { sessionId: id, maxMessages: 1 });
|
|
505
|
-
writePrivateJsonAtomic(
|
|
606
|
+
writePrivateJsonAtomic(path4.join(job.directory, "verification-complete.json"), { id: job.id });
|
|
506
607
|
return { phase: "complete", progress: 100, message: "\u63D2\u4EF6\u66F4\u65B0\u5B8C\u6210\uFF0CDSH \u5DF2\u6062\u590D\uFF1B\u539F\u8282\u70B9\u65E0\u9700\u91CD\u65B0\u914D\u5BF9\u3002", terminal: true, ok: true };
|
|
507
608
|
} catch (error) {
|
|
508
609
|
let rollback = false;
|
|
509
|
-
writePrivateJsonAtomic(
|
|
610
|
+
writePrivateJsonAtomic(path4.join(job.directory, "failure.json"), {
|
|
510
611
|
message: error instanceof Error ? error.message : "unknown",
|
|
511
612
|
stack: error instanceof Error ? error.stack : void 0,
|
|
512
613
|
cause: error instanceof Error && error.cause instanceof Error ? error.cause.message : void 0
|
|
513
614
|
});
|
|
615
|
+
if (error instanceof NativeInstallError && error.mayStillBeRunning) return {
|
|
616
|
+
phase: "attention",
|
|
617
|
+
progress: 100,
|
|
618
|
+
message: error.message,
|
|
619
|
+
terminal: true,
|
|
620
|
+
ok: false,
|
|
621
|
+
rollback: false
|
|
622
|
+
};
|
|
514
623
|
if (job.controlOrigin && !disposed) {
|
|
515
624
|
try {
|
|
516
625
|
disposed = (await control(job, "describe")).quiesced === true;
|
|
@@ -524,20 +633,28 @@ async function executeUpdate(job, progress, quiesce) {
|
|
|
524
633
|
await stopOriginal(job);
|
|
525
634
|
stopped = true;
|
|
526
635
|
}
|
|
527
|
-
if (newChild)
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
636
|
+
if (newChild) {
|
|
637
|
+
const ownedLock = captureCandidateLock(job.home, newChild);
|
|
638
|
+
await stopRestarted(newChild);
|
|
639
|
+
retireCandidateLock(ownedLock, newChild, job.directory);
|
|
640
|
+
} else if (modified && job.manager && job.manager.kind !== "process") stopManagedHost(job.manager);
|
|
641
|
+
if (modified) {
|
|
642
|
+
fs4.renameSync(job.profile, path4.join(job.directory, "profile-failed"));
|
|
643
|
+
fs4.renameSync(previous, job.profile);
|
|
644
|
+
}
|
|
645
|
+
if (job.previousVersion === "0.0.0") {
|
|
646
|
+
const recovery = await import(pathToFileURL(path4.join(job.directory, "native-recovery.js")).href);
|
|
647
|
+
await recovery.verifyNativeRestore(job, () => start(job), sessionIds, readableIds);
|
|
648
|
+
} else {
|
|
649
|
+
start(job);
|
|
650
|
+
await healthy(job, job.previousVersion);
|
|
532
651
|
}
|
|
533
|
-
start(job);
|
|
534
|
-
await healthy(job, job.previousVersion);
|
|
535
652
|
assertPreserved(before, durableSnapshot(job));
|
|
536
653
|
rollback = true;
|
|
537
|
-
writePrivateJsonAtomic(
|
|
654
|
+
writePrivateJsonAtomic(path4.join(job.directory, "verification-complete.json"), { id: job.id });
|
|
538
655
|
} catch (rollbackError) {
|
|
539
656
|
try {
|
|
540
|
-
writePrivateJsonAtomic(
|
|
657
|
+
writePrivateJsonAtomic(path4.join(job.directory, "rollback-failure.json"), {
|
|
541
658
|
message: rollbackError instanceof Error ? rollbackError.message : "unknown",
|
|
542
659
|
stack: rollbackError instanceof Error ? rollbackError.stack : void 0
|
|
543
660
|
});
|
|
@@ -550,14 +667,14 @@ async function executeUpdate(job, progress, quiesce) {
|
|
|
550
667
|
}
|
|
551
668
|
}
|
|
552
669
|
async function workerMain(filename) {
|
|
553
|
-
const job = JSON.parse(
|
|
670
|
+
const job = JSON.parse(fs4.readFileSync(filename, "utf8"));
|
|
554
671
|
validateJob(job);
|
|
555
672
|
if (!job.controlOrigin && (process.ppid !== job.parentPid || !process.connected)) throw new Error("Updater requires its initiating parent");
|
|
556
673
|
let status = { phase: "starting", progress: 20, message: "\u6B63\u5728\u51C6\u5907\u66F4\u65B0", terminal: false };
|
|
557
674
|
const record = (value) => {
|
|
558
675
|
status = value;
|
|
559
676
|
try {
|
|
560
|
-
writePrivateJsonAtomic(
|
|
677
|
+
writePrivateJsonAtomic(path4.join(job.directory, "result.json"), value);
|
|
561
678
|
} catch (error) {
|
|
562
679
|
console.error("Update progress journal unavailable:", error.code || "write-failed");
|
|
563
680
|
}
|
|
@@ -590,11 +707,11 @@ async function workerMain(filename) {
|
|
|
590
707
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
591
708
|
try {
|
|
592
709
|
if (job.controlOrigin) {
|
|
593
|
-
writePrivateJsonAtomic(
|
|
710
|
+
writePrivateJsonAtomic(path4.join(job.directory, "worker-ready.json"), { id: job.id, origin: `http://127.0.0.1:${server.address().port}` });
|
|
594
711
|
let authorized = false;
|
|
595
712
|
for (let i = 0; i < 150; i++) {
|
|
596
713
|
try {
|
|
597
|
-
authorized = JSON.parse(
|
|
714
|
+
authorized = JSON.parse(fs4.readFileSync(path4.join(job.directory, "authorized.json"), "utf8")).id === job.id;
|
|
598
715
|
} catch {
|
|
599
716
|
}
|
|
600
717
|
if (authorized) break;
|
|
@@ -656,8 +773,8 @@ async function workerMain(filename) {
|
|
|
656
773
|
message: "\u66F4\u65B0\u88AB\u5F02\u5E38\u4E2D\u65AD\uFF0C\u65E0\u6CD5\u786E\u8BA4\u6062\u590D\u7ED3\u679C\u3002\u8BF7\u4FDD\u7559\u4E3B\u673A\u66F4\u65B0\u76EE\u5F55\u5E76\u68C0\u67E5\u539F\u63D2\u4EF6\u5907\u4EFD\uFF0C\u4E0D\u8981\u5220\u9664\u8282\u70B9\u6216\u91CD\u590D\u5B89\u88C5\u3002"
|
|
657
774
|
});
|
|
658
775
|
}
|
|
659
|
-
const lock =
|
|
660
|
-
releaseOwnedUpdateLock(lock, job.id);
|
|
776
|
+
const lock = path4.join(job.profile, ".harness-remote-update.lock");
|
|
777
|
+
if (status.phase !== "attention") releaseOwnedUpdateLock(lock, job.id);
|
|
661
778
|
if (job.controlOrigin) {
|
|
662
779
|
try {
|
|
663
780
|
await control(job, "close");
|
|
@@ -672,18 +789,20 @@ async function workerMain(filename) {
|
|
|
672
789
|
}, 12e4);
|
|
673
790
|
timer.unref();
|
|
674
791
|
}
|
|
675
|
-
if (process.argv[1] &&
|
|
792
|
+
if (process.argv[1] && path4.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
676
793
|
void workerMain(process.argv[2]).catch(() => {
|
|
677
794
|
process.exitCode = 1;
|
|
678
795
|
if (process.connected) process.disconnect();
|
|
679
796
|
});
|
|
680
797
|
}
|
|
681
798
|
export {
|
|
799
|
+
captureCandidateLock,
|
|
682
800
|
control,
|
|
683
801
|
executeUpdate,
|
|
684
802
|
healthy,
|
|
685
803
|
migrateLegacyGrantOwner,
|
|
686
804
|
releaseOwnedUpdateLock,
|
|
805
|
+
retireCandidateLock,
|
|
687
806
|
stopRestarted,
|
|
688
807
|
validateJob
|
|
689
808
|
};
|