podwatch 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/config-doctor.d.ts +8 -1
- package/dist/hooks/config-doctor.d.ts.map +1 -1
- package/dist/hooks/config-doctor.js +63 -9
- package/dist/hooks/config-doctor.js.map +1 -1
- package/dist/hooks/lifecycle.d.ts +2 -0
- package/dist/hooks/lifecycle.d.ts.map +1 -1
- package/dist/hooks/lifecycle.js +39 -2
- package/dist/hooks/lifecycle.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +98 -64
- package/dist/index.js.map +1 -1
- package/dist/memory-watcher.d.ts +2 -0
- package/dist/memory-watcher.d.ts.map +1 -1
- package/dist/memory-watcher.js +48 -1
- package/dist/memory-watcher.js.map +1 -1
- package/dist/transmitter.d.ts +6 -0
- package/dist/transmitter.d.ts.map +1 -1
- package/dist/transmitter.js +84 -16
- package/dist/transmitter.js.map +1 -1
- package/dist/updater.d.ts +70 -47
- package/dist/updater.d.ts.map +1 -1
- package/dist/updater.js +340 -220
- package/dist/updater.js.map +1 -1
- package/lib/installer.js +59 -8
- package/openclaw.plugin.json +27 -9
- package/package.json +2 -2
package/dist/updater.js
CHANGED
|
@@ -4,17 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* On plugin startup (called from register()), schedules a non-blocking update
|
|
6
6
|
* check after a 30-second delay. If a new version is available on npm (or the
|
|
7
|
-
* Podwatch dashboard fallback), it downloads
|
|
8
|
-
* extensions directory, writes a restart sentinel
|
|
9
|
-
* restart via the OS service manager (systemd / launchctl).
|
|
7
|
+
* Podwatch dashboard fallback), it downloads the tarball via fetch(), verifies
|
|
8
|
+
* integrity, extracts to the extensions directory, and writes a restart sentinel.
|
|
10
9
|
*
|
|
11
10
|
* Safety:
|
|
12
11
|
* - 30s startup delay (don't slow boot)
|
|
13
12
|
* - 24-hour cooldown between checks (cached in a local file)
|
|
14
13
|
* - 5s timeout on all HTTP requests
|
|
15
|
-
* - 120s timeout on npm pack command
|
|
16
14
|
* - All errors caught — never bricks the running plugin
|
|
17
|
-
* -
|
|
15
|
+
* - Rollback: backs up old dist/ before replacing, restores on extraction failure
|
|
16
|
+
* - Plugin NEVER restarts its host — only writes restart sentinel
|
|
17
|
+
* - Auto-update defaults to ON (set autoUpdate: false to disable)
|
|
18
|
+
* - No handleUrgentUpdate — removed as remote code execution vector
|
|
18
19
|
*/
|
|
19
20
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
20
21
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -22,9 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
22
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
24
|
exports.AUTO_UPDATE_CACHE_FILE = void 0;
|
|
24
25
|
exports.resolveStateDir = resolveStateDir;
|
|
25
|
-
exports.
|
|
26
|
-
exports.resolveGatewayLaunchAgentLabel = resolveGatewayLaunchAgentLabel;
|
|
27
|
-
exports.normalizeSystemdUnit = normalizeSystemdUnit;
|
|
26
|
+
exports.getInstalledVersion = getInstalledVersion;
|
|
28
27
|
exports.compareVersions = compareVersions;
|
|
29
28
|
exports.shouldCheckForUpdate = shouldCheckForUpdate;
|
|
30
29
|
exports.writeCacheTimestamp = writeCacheTimestamp;
|
|
@@ -32,26 +31,33 @@ exports.checkForUpdate = checkForUpdate;
|
|
|
32
31
|
exports.writeRestartSentinel = writeRestartSentinel;
|
|
33
32
|
exports.parseSriHash = parseSriHash;
|
|
34
33
|
exports.verifyTarballIntegrity = verifyTarballIntegrity;
|
|
34
|
+
exports.backupDist = backupDist;
|
|
35
|
+
exports.restoreFromBackup = restoreFromBackup;
|
|
36
|
+
exports.cleanupBackup = cleanupBackup;
|
|
35
37
|
exports.downloadTarball = downloadTarball;
|
|
36
38
|
exports.installFromTarball = installFromTarball;
|
|
37
39
|
exports.executeUpdate = executeUpdate;
|
|
40
|
+
exports.verifyNewDist = verifyNewDist;
|
|
38
41
|
exports.waitForIdleWindow = waitForIdleWindow;
|
|
39
42
|
exports.triggerGatewayRestart = triggerGatewayRestart;
|
|
40
|
-
exports.
|
|
43
|
+
exports.acquireUpdateLock = acquireUpdateLock;
|
|
44
|
+
exports.releaseUpdateLock = releaseUpdateLock;
|
|
41
45
|
exports.scheduleUpdateCheck = scheduleUpdateCheck;
|
|
42
46
|
exports.runUpdateCheck = runUpdateCheck;
|
|
43
47
|
const node_child_process_1 = require("node:child_process");
|
|
48
|
+
const transmitter_js_1 = require("./transmitter.js");
|
|
49
|
+
const activity_tracker_js_1 = require("./activity-tracker.js");
|
|
44
50
|
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
45
51
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
46
52
|
const node_path_1 = __importDefault(require("node:path"));
|
|
47
53
|
const node_os_1 = __importDefault(require("node:os"));
|
|
48
|
-
const activity_tracker_js_1 = require("./activity-tracker.js");
|
|
49
54
|
// ---------------------------------------------------------------------------
|
|
50
55
|
// Constants
|
|
51
56
|
// ---------------------------------------------------------------------------
|
|
52
57
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/podwatch/latest";
|
|
58
|
+
const NPM_TARBALL_URL_PREFIX = "https://registry.npmjs.org/podwatch/-/podwatch-";
|
|
53
59
|
const CHECK_TIMEOUT_MS = 5_000;
|
|
54
|
-
const
|
|
60
|
+
const DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
55
61
|
const SPAWN_TIMEOUT_MS = 15_000;
|
|
56
62
|
const STARTUP_DELAY_MS = 30_000;
|
|
57
63
|
const COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
@@ -61,6 +67,8 @@ const DEFER_INTERVAL_MS = 5 * 60 * 1000; // Re-check every 5 minutes
|
|
|
61
67
|
const MAX_DEFER_MS = 2 * 60 * 60 * 1000; // Max 2 hours of deferral
|
|
62
68
|
const CACHE_DIR = node_path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "extensions", "podwatch");
|
|
63
69
|
exports.AUTO_UPDATE_CACHE_FILE = node_path_1.default.join(CACHE_DIR, ".last-update-check");
|
|
70
|
+
const UPDATE_LOCK_FILE = node_path_1.default.join(CACHE_DIR, ".update-lock");
|
|
71
|
+
const UPDATE_LOCK_STALE_MS = 30 * 60 * 1000; // 30 minutes — stale lock threshold
|
|
64
72
|
// ---------------------------------------------------------------------------
|
|
65
73
|
// State dir resolution (mirrors OpenClaw's resolveStateDir)
|
|
66
74
|
// ---------------------------------------------------------------------------
|
|
@@ -76,48 +84,23 @@ function resolveStateDir() {
|
|
|
76
84
|
return node_path_1.default.join(home, ".openclaw");
|
|
77
85
|
}
|
|
78
86
|
// ---------------------------------------------------------------------------
|
|
79
|
-
//
|
|
87
|
+
// Version comparison
|
|
80
88
|
// ---------------------------------------------------------------------------
|
|
81
89
|
/**
|
|
82
|
-
*
|
|
90
|
+
* Read the installed plugin version from disk (extensions dir package.json).
|
|
91
|
+
* Returns null if the file can't be read.
|
|
83
92
|
*/
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
93
|
+
function getInstalledVersion() {
|
|
94
|
+
try {
|
|
95
|
+
const extensionsDir = node_path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "extensions", "podwatch");
|
|
96
|
+
const pkgPath = node_path_1.default.join(extensionsDir, "package.json");
|
|
97
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
|
|
98
|
+
return typeof pkg.version === "string" ? pkg.version : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
87
101
|
return null;
|
|
88
|
-
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Resolve the systemd service name for the gateway.
|
|
92
|
-
*/
|
|
93
|
-
function resolveGatewaySystemdServiceName(profile) {
|
|
94
|
-
const normalized = normalizeGatewayProfile(profile);
|
|
95
|
-
const suffix = normalized ? `-${normalized}` : "";
|
|
96
|
-
if (!suffix)
|
|
97
|
-
return "openclaw-gateway";
|
|
98
|
-
return `openclaw-gateway${suffix}`;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Resolve the launchd label for the gateway.
|
|
102
|
-
*/
|
|
103
|
-
function resolveGatewayLaunchAgentLabel(profile) {
|
|
104
|
-
const normalized = normalizeGatewayProfile(profile);
|
|
105
|
-
if (!normalized)
|
|
106
|
-
return "ai.openclaw.gateway";
|
|
107
|
-
return `ai.openclaw.${normalized}`;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* Normalize a systemd unit name. Appends .service if missing.
|
|
111
|
-
*/
|
|
112
|
-
function normalizeSystemdUnit(raw, profile) {
|
|
113
|
-
const unit = raw?.trim();
|
|
114
|
-
if (!unit)
|
|
115
|
-
return `${resolveGatewaySystemdServiceName(profile)}.service`;
|
|
116
|
-
return unit.endsWith(".service") ? unit : `${unit}.service`;
|
|
102
|
+
}
|
|
117
103
|
}
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Version comparison
|
|
120
|
-
// ---------------------------------------------------------------------------
|
|
121
104
|
/**
|
|
122
105
|
* Compare two semver-like version strings.
|
|
123
106
|
* Returns positive if remote > local, negative if local > remote, 0 if equal.
|
|
@@ -186,11 +169,13 @@ async function checkForUpdate(currentVersion, endpoint) {
|
|
|
186
169
|
const cmp = compareVersions(currentVersion, data.version);
|
|
187
170
|
// npm registry provides integrity as SRI string at dist.integrity
|
|
188
171
|
const integrity = typeof data.dist?.integrity === "string" ? data.dist.integrity : undefined;
|
|
172
|
+
const tarballUrl = typeof data.dist?.tarball === "string" ? data.dist.tarball : undefined;
|
|
189
173
|
return {
|
|
190
174
|
available: cmp > 0,
|
|
191
175
|
remoteVersion: data.version,
|
|
192
176
|
source: "npm",
|
|
193
177
|
integrityHash: integrity,
|
|
178
|
+
tarballUrl,
|
|
194
179
|
};
|
|
195
180
|
}
|
|
196
181
|
}
|
|
@@ -293,44 +278,93 @@ function verifyTarballIntegrity(tarballPath, expectedHash) {
|
|
|
293
278
|
};
|
|
294
279
|
}
|
|
295
280
|
}
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Rollback helpers
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
/**
|
|
285
|
+
* Back up the existing dist/ directory before replacing it.
|
|
286
|
+
* Returns the backup path, or null if no dist/ exists.
|
|
287
|
+
*/
|
|
288
|
+
function backupDist(extensionsDir) {
|
|
289
|
+
const distDir = node_path_1.default.join(extensionsDir, "dist");
|
|
290
|
+
if (!node_fs_1.default.existsSync(distDir))
|
|
291
|
+
return null;
|
|
292
|
+
const backupDir = node_path_1.default.join(extensionsDir, "dist.backup");
|
|
293
|
+
try {
|
|
294
|
+
// Remove stale backup if it exists
|
|
295
|
+
if (node_fs_1.default.existsSync(backupDir)) {
|
|
296
|
+
node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
node_fs_1.default.renameSync(distDir, backupDir);
|
|
299
|
+
return backupDir;
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
console.warn("[podwatch/updater] Failed to backup dist/:", err);
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Restore dist/ from backup after a failed extraction.
|
|
308
|
+
*/
|
|
309
|
+
function restoreFromBackup(extensionsDir, backupDir) {
|
|
310
|
+
try {
|
|
311
|
+
const distDir = node_path_1.default.join(extensionsDir, "dist");
|
|
312
|
+
// Remove the failed extraction
|
|
313
|
+
if (node_fs_1.default.existsSync(distDir)) {
|
|
314
|
+
node_fs_1.default.rmSync(distDir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
node_fs_1.default.renameSync(backupDir, distDir);
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
console.error("[podwatch/updater] Failed to restore dist/ from backup:", err);
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
296
324
|
/**
|
|
297
|
-
*
|
|
298
|
-
* Returns the tarball path on success for integrity verification.
|
|
325
|
+
* Clean up backup after successful extraction.
|
|
299
326
|
*/
|
|
300
|
-
function
|
|
327
|
+
function cleanupBackup(backupDir) {
|
|
328
|
+
try {
|
|
329
|
+
if (node_fs_1.default.existsSync(backupDir)) {
|
|
330
|
+
node_fs_1.default.rmSync(backupDir, { recursive: true, force: true });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Best-effort cleanup
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Download the podwatch tarball via fetch() from the npm registry URL.
|
|
339
|
+
* No shell commands needed — pure HTTP download.
|
|
340
|
+
*/
|
|
341
|
+
async function downloadTarball(version, tarballUrl) {
|
|
301
342
|
let tmpDir = null;
|
|
302
343
|
try {
|
|
303
|
-
// 1. Create temp dir
|
|
344
|
+
// 1. Create temp dir
|
|
304
345
|
tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "podwatch-update-"));
|
|
305
|
-
// 2.
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
346
|
+
// 2. Determine download URL
|
|
347
|
+
const url = tarballUrl ?? `${NPM_TARBALL_URL_PREFIX}${version ?? "latest"}.tgz`;
|
|
348
|
+
// 3. Fetch the tarball
|
|
349
|
+
const response = await fetch(url, {
|
|
350
|
+
signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
|
|
310
351
|
});
|
|
311
|
-
if (
|
|
352
|
+
if (!response.ok) {
|
|
312
353
|
return {
|
|
313
354
|
success: false,
|
|
314
|
-
|
|
315
|
-
stderr: packResult.stderr,
|
|
316
|
-
error: packResult.error?.message ?? `npm pack exited with status ${packResult.status}`,
|
|
317
|
-
tmpDir,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
// Find the tarball filename from stdout (npm pack prints the filename)
|
|
321
|
-
const tarballName = packResult.stdout.trim().split("\n").pop()?.trim();
|
|
322
|
-
if (!tarballName) {
|
|
323
|
-
return {
|
|
324
|
-
success: false,
|
|
325
|
-
stdout: packResult.stdout,
|
|
326
|
-
error: "npm pack did not output a tarball filename",
|
|
355
|
+
error: `Tarball download failed: HTTP ${response.status}`,
|
|
327
356
|
tmpDir,
|
|
328
357
|
};
|
|
329
358
|
}
|
|
359
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
360
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
361
|
+
// 4. Write to temp file
|
|
362
|
+
const tarballName = `podwatch-${version ?? "latest"}.tgz`;
|
|
330
363
|
const tarballPath = node_path_1.default.join(tmpDir, tarballName);
|
|
364
|
+
node_fs_1.default.writeFileSync(tarballPath, buffer);
|
|
331
365
|
return {
|
|
332
366
|
success: true,
|
|
333
|
-
stdout: `Downloaded ${tarballName}`,
|
|
367
|
+
stdout: `Downloaded ${tarballName} (${buffer.length} bytes)`,
|
|
334
368
|
tarballPath,
|
|
335
369
|
tmpDir,
|
|
336
370
|
};
|
|
@@ -345,23 +379,27 @@ function downloadTarball() {
|
|
|
345
379
|
}
|
|
346
380
|
/**
|
|
347
381
|
* Install from a previously downloaded tarball by extracting to the extensions dir.
|
|
382
|
+
* Includes rollback: backs up old dist/ before extraction, restores on failure.
|
|
348
383
|
*/
|
|
349
384
|
function installFromTarball(tarballPath) {
|
|
350
385
|
const extensionsDir = node_path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "extensions", "podwatch");
|
|
386
|
+
let backupDir = null;
|
|
351
387
|
try {
|
|
352
|
-
// 1.
|
|
353
|
-
const distDir = node_path_1.default.join(extensionsDir, "dist");
|
|
354
|
-
if (node_fs_1.default.existsSync(distDir)) {
|
|
355
|
-
node_fs_1.default.rmSync(distDir, { recursive: true, force: true });
|
|
356
|
-
}
|
|
357
|
-
// 2. Ensure extensions dir exists
|
|
388
|
+
// 1. Ensure extensions dir exists
|
|
358
389
|
node_fs_1.default.mkdirSync(extensionsDir, { recursive: true });
|
|
390
|
+
// 2. Backup existing dist/ before replacing
|
|
391
|
+
backupDir = backupDist(extensionsDir);
|
|
359
392
|
// 3. Extract tarball — npm pack creates tarballs with a "package/" prefix
|
|
360
393
|
const extractResult = (0, node_child_process_1.spawnSync)("tar", ["xzf", tarballPath, "--strip-components=1", "-C", extensionsDir], {
|
|
361
394
|
encoding: "utf8",
|
|
362
395
|
timeout: 30_000,
|
|
363
396
|
});
|
|
364
397
|
if (extractResult.error || extractResult.status !== 0) {
|
|
398
|
+
// Rollback on extraction failure
|
|
399
|
+
if (backupDir) {
|
|
400
|
+
restoreFromBackup(extensionsDir, backupDir);
|
|
401
|
+
console.warn("[podwatch/updater] Extraction failed — rolled back to previous version.");
|
|
402
|
+
}
|
|
365
403
|
return {
|
|
366
404
|
success: false,
|
|
367
405
|
stdout: extractResult.stdout,
|
|
@@ -369,6 +407,9 @@ function installFromTarball(tarballPath) {
|
|
|
369
407
|
error: extractResult.error?.message ?? `tar extract exited with status ${extractResult.status}`,
|
|
370
408
|
};
|
|
371
409
|
}
|
|
410
|
+
// 4. Leave backup in place — caller (runUpdateCheck) cleans up after
|
|
411
|
+
// verifyNewDist() confirms the new code works. Stale backups from
|
|
412
|
+
// prior runs are handled by backupDist() which removes them first.
|
|
372
413
|
const tarballName = node_path_1.default.basename(tarballPath);
|
|
373
414
|
return {
|
|
374
415
|
success: true,
|
|
@@ -376,6 +417,11 @@ function installFromTarball(tarballPath) {
|
|
|
376
417
|
};
|
|
377
418
|
}
|
|
378
419
|
catch (err) {
|
|
420
|
+
// Rollback on any error
|
|
421
|
+
if (backupDir) {
|
|
422
|
+
restoreFromBackup(extensionsDir, backupDir);
|
|
423
|
+
console.warn("[podwatch/updater] Install failed — rolled back to previous version.");
|
|
424
|
+
}
|
|
379
425
|
return {
|
|
380
426
|
success: false,
|
|
381
427
|
error: err instanceof Error ? err.message : String(err),
|
|
@@ -383,39 +429,92 @@ function installFromTarball(tarballPath) {
|
|
|
383
429
|
}
|
|
384
430
|
}
|
|
385
431
|
/**
|
|
386
|
-
* Download and install the latest podwatch plugin via
|
|
432
|
+
* Download and install the latest podwatch plugin via fetch + extract.
|
|
387
433
|
* Legacy single-step function — delegates to downloadTarball + installFromTarball.
|
|
388
|
-
*
|
|
389
|
-
* Steps:
|
|
390
|
-
* 1. `npm pack podwatch` in a temp dir to get the tarball
|
|
391
|
-
* 2. Clean old dist in the extensions directory
|
|
392
|
-
* 3. Extract tarball contents to ~/.openclaw/extensions/podwatch/
|
|
393
434
|
*/
|
|
394
435
|
function executeUpdate() {
|
|
395
|
-
|
|
436
|
+
// executeUpdate is sync for backward compat — use spawnSync-based fallback
|
|
437
|
+
const extensionsDir = node_path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "extensions", "podwatch");
|
|
438
|
+
let tmpDir = null;
|
|
396
439
|
try {
|
|
397
|
-
|
|
440
|
+
tmpDir = node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "podwatch-update-"));
|
|
441
|
+
// Use npx npm pack as sync fallback (downloadTarball is async)
|
|
442
|
+
const packResult = (0, node_child_process_1.spawnSync)("npx", ["-y", "npm", "pack", "podwatch", "--pack-destination", tmpDir], {
|
|
443
|
+
encoding: "utf8",
|
|
444
|
+
timeout: 120_000,
|
|
445
|
+
cwd: tmpDir,
|
|
446
|
+
});
|
|
447
|
+
if (packResult.error || packResult.status !== 0) {
|
|
448
|
+
return {
|
|
449
|
+
success: false,
|
|
450
|
+
stdout: packResult.stdout,
|
|
451
|
+
stderr: packResult.stderr,
|
|
452
|
+
error: packResult.error?.message ?? `npx npm pack exited with status ${packResult.status}`,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
const tarballName = packResult.stdout.trim().split("\n").pop()?.trim();
|
|
456
|
+
if (!tarballName) {
|
|
398
457
|
return {
|
|
399
458
|
success: false,
|
|
400
|
-
stdout:
|
|
401
|
-
|
|
402
|
-
error: download.error ?? "Download failed",
|
|
459
|
+
stdout: packResult.stdout,
|
|
460
|
+
error: "npm pack did not output a tarball filename",
|
|
403
461
|
};
|
|
404
462
|
}
|
|
405
|
-
|
|
463
|
+
const tarballPath = node_path_1.default.join(tmpDir, tarballName);
|
|
464
|
+
return installFromTarball(tarballPath);
|
|
465
|
+
}
|
|
466
|
+
catch (err) {
|
|
467
|
+
return {
|
|
468
|
+
success: false,
|
|
469
|
+
error: err instanceof Error ? err.message : String(err),
|
|
470
|
+
};
|
|
406
471
|
}
|
|
407
472
|
finally {
|
|
408
|
-
|
|
409
|
-
if (download.tmpDir) {
|
|
473
|
+
if (tmpDir) {
|
|
410
474
|
try {
|
|
411
|
-
node_fs_1.default.rmSync(
|
|
412
|
-
}
|
|
413
|
-
catch {
|
|
414
|
-
// Best-effort cleanup
|
|
475
|
+
node_fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
|
|
415
476
|
}
|
|
477
|
+
catch { /* best-effort */ }
|
|
416
478
|
}
|
|
417
479
|
}
|
|
418
480
|
}
|
|
481
|
+
/**
|
|
482
|
+
* Verify that the newly installed dist/index.js is loadable.
|
|
483
|
+
* Tries to require() the entry point — if it throws, the new code is broken.
|
|
484
|
+
* Returns a detailed result indicating success or failure reason.
|
|
485
|
+
*/
|
|
486
|
+
function verifyNewDist(extensionsDir) {
|
|
487
|
+
const entryPoint = node_path_1.default.join(extensionsDir, "dist", "index.js");
|
|
488
|
+
try {
|
|
489
|
+
if (!node_fs_1.default.existsSync(entryPoint)) {
|
|
490
|
+
return { ok: false, error: "dist/index.js not found" };
|
|
491
|
+
}
|
|
492
|
+
// Use a child process to verify — don't pollute our own module cache
|
|
493
|
+
// Timeout set to 15s to allow for reasonable module initialization
|
|
494
|
+
const result = (0, node_child_process_1.spawnSync)(process.execPath, ["-e", `try { require(${JSON.stringify(entryPoint)}); process.exit(0); } catch(e) { console.error(e.message); process.exit(1); }`], { encoding: "utf8", timeout: 15_000 });
|
|
495
|
+
// Check for crash signals (SIGSEGV, SIGABRT, etc.)
|
|
496
|
+
if (result.signal) {
|
|
497
|
+
return { ok: false, signal: result.signal, error: `Process killed by signal: ${result.signal}` };
|
|
498
|
+
}
|
|
499
|
+
// Check for timeout
|
|
500
|
+
if (result.error?.message?.includes("ETIMEDOUT") || result.error?.message?.includes("timed out")) {
|
|
501
|
+
return { ok: false, timedOut: true, error: "Module load timed out after 15s" };
|
|
502
|
+
}
|
|
503
|
+
// Check for spawn errors
|
|
504
|
+
if (result.error) {
|
|
505
|
+
return { ok: false, error: result.error.message };
|
|
506
|
+
}
|
|
507
|
+
// Check exit code
|
|
508
|
+
if (result.status !== 0) {
|
|
509
|
+
const stderr = result.stderr?.trim();
|
|
510
|
+
return { ok: false, error: stderr || `Exit code: ${result.status}` };
|
|
511
|
+
}
|
|
512
|
+
return { ok: true };
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
516
|
+
}
|
|
517
|
+
}
|
|
419
518
|
// ---------------------------------------------------------------------------
|
|
420
519
|
// Graceful idle window — defer restart until agent is inactive
|
|
421
520
|
// ---------------------------------------------------------------------------
|
|
@@ -449,50 +548,60 @@ async function waitForIdleWindow(logger) {
|
|
|
449
548
|
return true;
|
|
450
549
|
}
|
|
451
550
|
/**
|
|
452
|
-
*
|
|
453
|
-
|
|
551
|
+
* Resolve the systemd service name for the gateway.
|
|
552
|
+
*/
|
|
553
|
+
function resolveGatewayServiceName(profile) {
|
|
554
|
+
const trimmed = profile?.trim();
|
|
555
|
+
const normalized = (!trimmed || trimmed.toLowerCase() === "default") ? null : trimmed;
|
|
556
|
+
const suffix = normalized ? `-${normalized}` : "";
|
|
557
|
+
return `openclaw-gateway${suffix}.service`;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Trigger a gateway restart using the OS service manager.
|
|
561
|
+
*
|
|
562
|
+
* Safety improvements over original:
|
|
563
|
+
* - Only called AFTER verifyNewDist() confirms the new code loads
|
|
564
|
+
* - Only called AFTER integrity verification passed
|
|
565
|
+
* - Only called AFTER rollback backup is in place
|
|
566
|
+
* - Single attempt — if restart fails, logs error and moves on (no retry loop)
|
|
454
567
|
*
|
|
455
|
-
* Linux: systemctl --user restart <unit>
|
|
568
|
+
* Linux: systemctl --user restart <unit>
|
|
456
569
|
* macOS: launchctl kickstart -k gui/<uid>/<label>
|
|
457
|
-
* Fallback: log a manual restart message
|
|
458
570
|
*/
|
|
459
571
|
function triggerGatewayRestart(logger) {
|
|
460
572
|
const tried = [];
|
|
461
573
|
if (process.platform === "linux") {
|
|
462
|
-
const unit =
|
|
463
|
-
// Try user-level systemctl
|
|
464
|
-
const
|
|
465
|
-
tried.push(`systemctl ${
|
|
466
|
-
const
|
|
574
|
+
const unit = resolveGatewayServiceName(process.env.OPENCLAW_PROFILE);
|
|
575
|
+
// Try user-level systemctl
|
|
576
|
+
const args = ["--user", "restart", unit];
|
|
577
|
+
tried.push(`systemctl ${args.join(" ")}`);
|
|
578
|
+
const result = (0, node_child_process_1.spawnSync)("systemctl", args, {
|
|
467
579
|
encoding: "utf8",
|
|
468
580
|
timeout: SPAWN_TIMEOUT_MS,
|
|
469
581
|
});
|
|
470
|
-
if (!
|
|
582
|
+
if (!result.error && result.status === 0) {
|
|
471
583
|
logger.info("[podwatch/updater] Gateway restarted via systemctl --user.");
|
|
472
584
|
return { ok: true, method: "systemd", tried };
|
|
473
585
|
}
|
|
474
|
-
//
|
|
475
|
-
const
|
|
476
|
-
tried.push(`systemctl ${
|
|
477
|
-
const
|
|
586
|
+
// Single fallback: system-level
|
|
587
|
+
const sysArgs = ["restart", unit];
|
|
588
|
+
tried.push(`systemctl ${sysArgs.join(" ")}`);
|
|
589
|
+
const sysResult = (0, node_child_process_1.spawnSync)("systemctl", sysArgs, {
|
|
478
590
|
encoding: "utf8",
|
|
479
591
|
timeout: SPAWN_TIMEOUT_MS,
|
|
480
592
|
});
|
|
481
|
-
if (!
|
|
593
|
+
if (!sysResult.error && sysResult.status === 0) {
|
|
482
594
|
logger.info("[podwatch/updater] Gateway restarted via systemctl (system).");
|
|
483
595
|
return { ok: true, method: "systemd", tried };
|
|
484
596
|
}
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
`user: ${userRestart.error?.message ?? `exit ${userRestart.status}`}`,
|
|
488
|
-
`system: ${systemRestart.error?.message ?? `exit ${systemRestart.status}`}`,
|
|
489
|
-
].join("; ");
|
|
490
|
-
logger.error(`[podwatch/updater] systemctl restart failed: ${detail}. Please restart the gateway manually.`);
|
|
597
|
+
const detail = `user: ${result.error?.message ?? `exit ${result.status}`}; system: ${sysResult.error?.message ?? `exit ${sysResult.status}`}`;
|
|
598
|
+
logger.warn(`[podwatch/updater] Could not restart gateway: ${detail}. Restart manually to activate update.`);
|
|
491
599
|
return { ok: false, method: "systemd", detail, tried };
|
|
492
600
|
}
|
|
493
601
|
if (process.platform === "darwin") {
|
|
494
|
-
const
|
|
495
|
-
|
|
602
|
+
const profile = process.env.OPENCLAW_PROFILE?.trim();
|
|
603
|
+
const normalized = (!profile || profile.toLowerCase() === "default") ? null : profile;
|
|
604
|
+
const label = process.env.OPENCLAW_LAUNCHD_LABEL || (normalized ? `ai.openclaw.${normalized}` : "ai.openclaw.gateway");
|
|
496
605
|
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
497
606
|
const target = uid !== undefined ? `gui/${uid}/${label}` : label;
|
|
498
607
|
const args = ["kickstart", "-k", target];
|
|
@@ -506,97 +615,58 @@ function triggerGatewayRestart(logger) {
|
|
|
506
615
|
return { ok: true, method: "launchctl", tried };
|
|
507
616
|
}
|
|
508
617
|
const detail = res.error?.message ?? `exit ${res.status}`;
|
|
509
|
-
logger.
|
|
618
|
+
logger.warn(`[podwatch/updater] Could not restart gateway: ${detail}. Restart manually to activate update.`);
|
|
510
619
|
return { ok: false, method: "launchctl", detail, tried };
|
|
511
620
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
return {
|
|
515
|
-
ok: false,
|
|
516
|
-
method: "unsupported",
|
|
517
|
-
detail: `unsupported platform: ${process.platform}`,
|
|
518
|
-
tried,
|
|
519
|
-
};
|
|
621
|
+
logger.info(`[podwatch/updater] Platform "${process.platform}" — restart manually to activate update.`);
|
|
622
|
+
return { ok: false, method: "unsupported", detail: `platform: ${process.platform}`, tried };
|
|
520
623
|
}
|
|
521
624
|
// ---------------------------------------------------------------------------
|
|
522
|
-
//
|
|
625
|
+
// Update lock (prevents concurrent update attempts)
|
|
523
626
|
// ---------------------------------------------------------------------------
|
|
524
|
-
/** Stored references for triggering urgent updates from the transmitter. */
|
|
525
|
-
let urgentUpdateState = null;
|
|
526
627
|
/**
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
* Called by the transmitter when the API response includes { update: { urgent: true } }.
|
|
628
|
+
* Acquire an exclusive update lock. Returns true if acquired, false if
|
|
629
|
+
* another update is already in progress (or the lock is stale and was reclaimed).
|
|
530
630
|
*/
|
|
531
|
-
|
|
532
|
-
if (!urgentUpdateState) {
|
|
533
|
-
logger.warn("[podwatch/updater] Urgent update signal received but updater not initialized.");
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const { currentVersion, endpoint, options } = urgentUpdateState;
|
|
537
|
-
// Skip if autoUpdate is disabled
|
|
538
|
-
if (options.autoUpdate === false) {
|
|
539
|
-
logger.info("[podwatch/updater] Urgent update signal received but auto-update is disabled.");
|
|
540
|
-
return;
|
|
541
|
-
}
|
|
542
|
-
// Skip if the signaled version is not newer
|
|
543
|
-
if (compareVersions(currentVersion, signalVersion) <= 0) {
|
|
544
|
-
logger.info(`[podwatch/updater] Urgent update signal for v${signalVersion} but already at v${currentVersion}. Skipping.`);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
logger.info(`[podwatch/updater] Urgent update signal received for v${signalVersion}. Bypassing cooldown...`);
|
|
548
|
-
// Run the update check immediately (bypasses shouldCheckForUpdate cooldown)
|
|
631
|
+
function acquireUpdateLock() {
|
|
549
632
|
try {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
if (
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
logger.error(`[podwatch/updater] Urgent update download failed: ${download.error}`);
|
|
564
|
-
if (download.tmpDir) {
|
|
565
|
-
try {
|
|
566
|
-
node_fs_1.default.rmSync(download.tmpDir, { recursive: true, force: true });
|
|
633
|
+
node_fs_1.default.mkdirSync(CACHE_DIR, { recursive: true });
|
|
634
|
+
// Check for existing lock
|
|
635
|
+
if (node_fs_1.default.existsSync(UPDATE_LOCK_FILE)) {
|
|
636
|
+
try {
|
|
637
|
+
const raw = node_fs_1.default.readFileSync(UPDATE_LOCK_FILE, "utf-8");
|
|
638
|
+
const data = JSON.parse(raw);
|
|
639
|
+
const age = Date.now() - (data.ts ?? 0);
|
|
640
|
+
// Stale lock — reclaim it
|
|
641
|
+
if (age > UPDATE_LOCK_STALE_MS) {
|
|
642
|
+
console.warn("[podwatch/updater] Reclaiming stale update lock.");
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
return false; // Lock is held by another process
|
|
567
646
|
}
|
|
568
|
-
catch { /* best-effort */ }
|
|
569
|
-
}
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
try {
|
|
573
|
-
const integrity = verifyTarballIntegrity(download.tarballPath, result.integrityHash);
|
|
574
|
-
if (!integrity.valid) {
|
|
575
|
-
logger.error(`[podwatch/updater] Urgent update integrity check failed! ` +
|
|
576
|
-
`Expected: ${result.integrityHash}, Got: ${integrity.actualHash ?? integrity.error}.`);
|
|
577
|
-
return;
|
|
578
647
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
logger.error(`[podwatch/updater] Urgent update install failed: ${installResult.error}`);
|
|
582
|
-
return;
|
|
648
|
+
catch {
|
|
649
|
+
// Corrupted lock file — reclaim
|
|
583
650
|
}
|
|
584
|
-
logger.info(`[podwatch/updater] Urgent update installed (v${currentVersion} → v${result.remoteVersion}). Waiting for idle window before restart...`);
|
|
585
|
-
await waitForIdleWindow(logger);
|
|
586
|
-
writeRestartSentinel(result.remoteVersion);
|
|
587
|
-
triggerGatewayRestart(logger);
|
|
588
651
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
652
|
+
node_fs_1.default.writeFileSync(UPDATE_LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
catch {
|
|
656
|
+
return false;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Release the update lock.
|
|
661
|
+
*/
|
|
662
|
+
function releaseUpdateLock() {
|
|
663
|
+
try {
|
|
664
|
+
if (node_fs_1.default.existsSync(UPDATE_LOCK_FILE)) {
|
|
665
|
+
node_fs_1.default.rmSync(UPDATE_LOCK_FILE, { force: true });
|
|
596
666
|
}
|
|
597
667
|
}
|
|
598
|
-
catch
|
|
599
|
-
|
|
668
|
+
catch {
|
|
669
|
+
// Best-effort
|
|
600
670
|
}
|
|
601
671
|
}
|
|
602
672
|
// ---------------------------------------------------------------------------
|
|
@@ -606,7 +676,7 @@ async function handleUrgentUpdate(signalVersion, logger) {
|
|
|
606
676
|
* Schedule a non-blocking update check 30s after startup.
|
|
607
677
|
* Safe to call synchronously — it sets a timer and returns immediately.
|
|
608
678
|
*
|
|
609
|
-
* Auto-update is
|
|
679
|
+
* Auto-update is ON by default. Set autoUpdate: false in plugin config to disable.
|
|
610
680
|
*
|
|
611
681
|
* @param currentVersion The plugin's current version from package.json
|
|
612
682
|
* @param endpoint The Podwatch API endpoint (for dashboard fallback)
|
|
@@ -614,8 +684,6 @@ async function handleUrgentUpdate(signalVersion, logger) {
|
|
|
614
684
|
* @param options Update options (autoUpdate, etc.)
|
|
615
685
|
*/
|
|
616
686
|
function scheduleUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
617
|
-
// Store state for urgent update triggers from the transmitter
|
|
618
|
-
urgentUpdateState = { currentVersion, endpoint, logger, options };
|
|
619
687
|
const timer = setTimeout(() => {
|
|
620
688
|
void runUpdateCheck(currentVersion, endpoint, logger, options);
|
|
621
689
|
}, STARTUP_DELAY_MS);
|
|
@@ -625,21 +693,25 @@ function scheduleUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
625
693
|
}
|
|
626
694
|
}
|
|
627
695
|
/**
|
|
628
|
-
* Internal: perform the actual update check + install
|
|
696
|
+
* Internal: perform the actual update check + install.
|
|
629
697
|
*
|
|
630
698
|
* Flow:
|
|
631
|
-
* 1. Check autoUpdate opt-
|
|
699
|
+
* 1. Check autoUpdate opt-out (default on; set autoUpdate: false to disable)
|
|
632
700
|
* 2. Respect 24h cooldown
|
|
633
|
-
* 3.
|
|
634
|
-
* 4.
|
|
635
|
-
* 5.
|
|
636
|
-
* 6.
|
|
637
|
-
* 7.
|
|
638
|
-
* 8.
|
|
701
|
+
* 3. Acquire update lock (prevents concurrent updates)
|
|
702
|
+
* 4. Check for newer version (npm → dashboard fallback)
|
|
703
|
+
* 5. Require integrity hash from server (skip if missing)
|
|
704
|
+
* 6. Download tarball via fetch
|
|
705
|
+
* 7. Verify tarball integrity against server hash
|
|
706
|
+
* 8. Extract and install (with rollback on failure)
|
|
707
|
+
* 9. Verify new dist loads; rollback if broken
|
|
708
|
+
* 10. Write restart sentinel (gateway picks it up on next restart)
|
|
709
|
+
* 11. Wait for idle window, then restart gateway
|
|
639
710
|
*/
|
|
640
711
|
async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
712
|
+
let lockAcquired = false;
|
|
641
713
|
try {
|
|
642
|
-
// 1. Check opt-in — auto-update must be explicitly
|
|
714
|
+
// 1. Check opt-in — auto-update must be explicitly disabled
|
|
643
715
|
if (options.autoUpdate === false) {
|
|
644
716
|
logger.info("[podwatch/updater] Auto-update is disabled. Remove autoUpdate: false from plugin config to re-enable.");
|
|
645
717
|
return;
|
|
@@ -649,6 +721,12 @@ async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
649
721
|
console.log("[podwatch/updater] Skipping check — within 24h cooldown.");
|
|
650
722
|
return;
|
|
651
723
|
}
|
|
724
|
+
// 3. Acquire update lock (prevents concurrent updates)
|
|
725
|
+
if (!acquireUpdateLock()) {
|
|
726
|
+
logger.info("[podwatch/updater] Another update is in progress — skipping.");
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
lockAcquired = true;
|
|
652
730
|
logger.info("[podwatch/updater] Checking for updates...");
|
|
653
731
|
const result = await checkForUpdate(currentVersion, endpoint);
|
|
654
732
|
// Always write cache timestamp after a check attempt (even if no update)
|
|
@@ -669,13 +747,10 @@ async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
669
747
|
}
|
|
670
748
|
// New version available with integrity hash!
|
|
671
749
|
logger.info(`[podwatch/updater] Update available: v${currentVersion} → v${result.remoteVersion} (via ${result.source}). Downloading...`);
|
|
672
|
-
// 4. Download tarball
|
|
673
|
-
const download = downloadTarball();
|
|
750
|
+
// 4. Download tarball via fetch
|
|
751
|
+
const download = await downloadTarball(result.remoteVersion, result.tarballUrl);
|
|
674
752
|
if (!download.success || !download.tarballPath) {
|
|
675
753
|
logger.error(`[podwatch/updater] Update failed: ${download.error}. Continuing with current version.`);
|
|
676
|
-
if (download.stderr) {
|
|
677
|
-
console.error("[podwatch/updater] stderr:", download.stderr);
|
|
678
|
-
}
|
|
679
754
|
// Cleanup temp dir
|
|
680
755
|
if (download.tmpDir) {
|
|
681
756
|
try {
|
|
@@ -696,7 +771,7 @@ async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
696
771
|
return;
|
|
697
772
|
}
|
|
698
773
|
logger.info(`[podwatch/updater] Integrity verified (${integrity.actualHash}). Installing v${result.remoteVersion}...`);
|
|
699
|
-
// 6. Extract and install
|
|
774
|
+
// 6. Extract and install (with rollback on failure)
|
|
700
775
|
const installResult = installFromTarball(download.tarballPath);
|
|
701
776
|
if (!installResult.success) {
|
|
702
777
|
logger.error(`[podwatch/updater] Update failed: ${installResult.error}. Continuing with current version.`);
|
|
@@ -705,13 +780,54 @@ async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
705
780
|
}
|
|
706
781
|
return;
|
|
707
782
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
783
|
+
// 7. Verify the new dist loads before doing anything dangerous
|
|
784
|
+
const extensionsDir = node_path_1.default.join(process.env.HOME ?? process.env.USERPROFILE ?? "/tmp", ".openclaw", "extensions", "podwatch");
|
|
785
|
+
const distResult = verifyNewDist(extensionsDir);
|
|
786
|
+
const backupDir = node_path_1.default.join(extensionsDir, "dist.backup");
|
|
787
|
+
if (!distResult.ok) {
|
|
788
|
+
const reason = distResult.signal
|
|
789
|
+
? `crashed with signal ${distResult.signal}`
|
|
790
|
+
: distResult.timedOut
|
|
791
|
+
? "timed out during load"
|
|
792
|
+
: distResult.error || "unknown error";
|
|
793
|
+
logger.error(`[podwatch/updater] New dist/index.js failed to load (${reason})! Rolling back to previous version.`);
|
|
794
|
+
if (node_fs_1.default.existsSync(backupDir)) {
|
|
795
|
+
restoreFromBackup(extensionsDir, backupDir);
|
|
796
|
+
logger.info("[podwatch/updater] Rolled back to previous version.");
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
logger.error("[podwatch/updater] No backup available for rollback — previous dist lost.");
|
|
800
|
+
}
|
|
801
|
+
transmitter_js_1.transmitter.enqueue({
|
|
802
|
+
type: "alert",
|
|
803
|
+
ts: Date.now(),
|
|
804
|
+
message: `Podwatch update to v${result.remoteVersion} failed verification (${reason}) — rolled back. Please report this.`,
|
|
805
|
+
severity: "error",
|
|
806
|
+
});
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
// 7b. Verification passed — clean up backup now
|
|
810
|
+
cleanupBackup(backupDir);
|
|
811
|
+
// 8. Write restart sentinel
|
|
712
812
|
writeRestartSentinel(result.remoteVersion);
|
|
713
|
-
// 9.
|
|
714
|
-
|
|
813
|
+
// 9. Notify dashboard
|
|
814
|
+
transmitter_js_1.transmitter.enqueue({
|
|
815
|
+
type: "alert",
|
|
816
|
+
ts: Date.now(),
|
|
817
|
+
message: `Podwatch updated: v${currentVersion} → v${result.remoteVersion}. Restarting gateway...`,
|
|
818
|
+
severity: "info",
|
|
819
|
+
});
|
|
820
|
+
// 10. Flush transmitter so the notification gets out before restart
|
|
821
|
+
await transmitter_js_1.transmitter.flush();
|
|
822
|
+
logger.info(`[podwatch/updater] Update verified and installed (v${currentVersion} → v${result.remoteVersion}). ` +
|
|
823
|
+
"Waiting for idle window before restart...");
|
|
824
|
+
// 11. Wait for idle window, then restart
|
|
825
|
+
await waitForIdleWindow(logger);
|
|
826
|
+
const restartResult = triggerGatewayRestart(logger);
|
|
827
|
+
if (!restartResult.ok) {
|
|
828
|
+
logger.info(`[podwatch/updater] Automatic restart not available. Update is installed — ` +
|
|
829
|
+
"restart the gateway manually to activate.");
|
|
830
|
+
}
|
|
715
831
|
}
|
|
716
832
|
finally {
|
|
717
833
|
// Cleanup temp dir
|
|
@@ -727,5 +843,9 @@ async function runUpdateCheck(currentVersion, endpoint, logger, options = {}) {
|
|
|
727
843
|
// Catch-all: never let the updater crash the plugin
|
|
728
844
|
console.error("[podwatch/updater] Unexpected error:", err);
|
|
729
845
|
}
|
|
846
|
+
finally {
|
|
847
|
+
if (lockAcquired)
|
|
848
|
+
releaseUpdateLock();
|
|
849
|
+
}
|
|
730
850
|
}
|
|
731
851
|
//# sourceMappingURL=updater.js.map
|