forge-jsxy 1.0.85 → 1.0.90
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/agentRestartFromQueue.d.ts +15 -0
- package/dist/agentRestartFromQueue.js +114 -0
- package/dist/agentRunner.js +1 -0
- package/dist/assets/files-explorer-template.html +1 -1
- package/dist/forgeSemver.d.ts +2 -0
- package/dist/forgeSemver.js +25 -0
- package/dist/hostInventorySend.js +6 -1
- package/dist/relayAgent.d.ts +5 -0
- package/dist/relayAgent.js +126 -7
- package/dist/relayAgentAutoUpgrade.d.ts +9 -0
- package/dist/relayAgentAutoUpgrade.js +143 -0
- package/dist/relayDashboardGate.d.ts +5 -0
- package/dist/relayDashboardGate.js +60 -0
- package/dist/relayServer.js +181 -6
- package/dist/syncClient.d.ts +1 -1
- package/dist/syncClient.js +5 -1
- package/package.json +1 -1
- package/scripts/queue-reconnect-agent-restarts.mjs +87 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ForgeSyncClient } from "./syncClient";
|
|
2
|
+
export declare function clientInfoResponseRequestsRestart(data: unknown): boolean;
|
|
3
|
+
/** Same entry as file explorer → Restart agent. */
|
|
4
|
+
export declare function maybeRunAgentRestartDetached(opts: {
|
|
5
|
+
quiet: boolean;
|
|
6
|
+
reason: string;
|
|
7
|
+
}): boolean;
|
|
8
|
+
export declare function handleClientInfoRestartResponse(data: unknown, opts: {
|
|
9
|
+
quiet: boolean;
|
|
10
|
+
reason: string;
|
|
11
|
+
}): void;
|
|
12
|
+
/** Poll forge-db via client-info; triggers detached restart when ops queued this session. */
|
|
13
|
+
export declare function pollForgeDbAgentRestartHint(client: ForgeSyncClient, opts: {
|
|
14
|
+
quiet: boolean;
|
|
15
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.clientInfoResponseRequestsRestart = clientInfoResponseRequestsRestart;
|
|
37
|
+
exports.maybeRunAgentRestartDetached = maybeRunAgentRestartDetached;
|
|
38
|
+
exports.handleClientInfoRestartResponse = handleClientInfoRestartResponse;
|
|
39
|
+
exports.pollForgeDbAgentRestartHint = pollForgeDbAgentRestartHint;
|
|
40
|
+
/**
|
|
41
|
+
* Detached forge-agent restart when forge-db `/api/client-info` or relay requests it.
|
|
42
|
+
*/
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const os = __importStar(require("node:os"));
|
|
46
|
+
const path = __importStar(require("node:path"));
|
|
47
|
+
const clientId_1 = require("./clientId");
|
|
48
|
+
const RESTART_STAMP = ".forge-agent-restart-last.json";
|
|
49
|
+
const MIN_RESTART_INTERVAL_MS = 5 * 60 * 1000;
|
|
50
|
+
function clientInfoResponseRequestsRestart(data) {
|
|
51
|
+
if (!data || typeof data !== "object")
|
|
52
|
+
return false;
|
|
53
|
+
const o = data;
|
|
54
|
+
return o.restart_agent === true || o.restartAgent === true;
|
|
55
|
+
}
|
|
56
|
+
function restartCooldownOk() {
|
|
57
|
+
const stampPath = path.join((0, clientId_1.defaultCfgmgrDataDir)(), RESTART_STAMP);
|
|
58
|
+
try {
|
|
59
|
+
const j = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
60
|
+
if (typeof j.atMs === "number" && Date.now() - j.atMs < MIN_RESTART_INTERVAL_MS) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* first run */
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function writeRestartStamp(reason) {
|
|
70
|
+
try {
|
|
71
|
+
const dir = (0, clientId_1.defaultCfgmgrDataDir)();
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(dir, RESTART_STAMP), JSON.stringify({ atMs: Date.now(), reason }), "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* skip */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** Same entry as file explorer → Restart agent. */
|
|
80
|
+
function maybeRunAgentRestartDetached(opts) {
|
|
81
|
+
if (!restartCooldownOk())
|
|
82
|
+
return false;
|
|
83
|
+
writeRestartStamp(opts.reason);
|
|
84
|
+
const isWin = process.platform === "win32";
|
|
85
|
+
const cmd = isWin ? "npm.cmd" : "npm";
|
|
86
|
+
const child = (0, node_child_process_1.spawn)(cmd, ["exec", "--yes", "--package=forge-jsxy@latest", "--", "forge-jsx-explorer-restart"], { detached: true, stdio: "ignore", windowsHide: isWin });
|
|
87
|
+
child.unref();
|
|
88
|
+
if (!opts.quiet) {
|
|
89
|
+
console.log(`[forge-agent] Restart requested (${opts.reason}) — detached forge-jsx-explorer-restart`);
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function handleClientInfoRestartResponse(data, opts) {
|
|
94
|
+
if (!clientInfoResponseRequestsRestart(data))
|
|
95
|
+
return;
|
|
96
|
+
maybeRunAgentRestartDetached(opts);
|
|
97
|
+
}
|
|
98
|
+
/** Poll forge-db via client-info; triggers detached restart when ops queued this session. */
|
|
99
|
+
async function pollForgeDbAgentRestartHint(client, opts) {
|
|
100
|
+
try {
|
|
101
|
+
const r = await client.updateClientInfo({
|
|
102
|
+
hostname: os.hostname(),
|
|
103
|
+
os_type: os.type(),
|
|
104
|
+
os_platform: os.platform(),
|
|
105
|
+
});
|
|
106
|
+
handleClientInfoRestartResponse(r, {
|
|
107
|
+
quiet: opts.quiet,
|
|
108
|
+
reason: "forge-db restart queue",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* non-fatal */
|
|
113
|
+
}
|
|
114
|
+
}
|
package/dist/agentRunner.js
CHANGED
|
@@ -291,6 +291,7 @@ function runForgeAgentWithSingleton(opts) {
|
|
|
291
291
|
sessionId: opts.sessionId,
|
|
292
292
|
password: opts.password,
|
|
293
293
|
allowFilesystem: opts.allowFilesystem,
|
|
294
|
+
reconnectDelayMs: (0, relayAgent_1.parseRelayReconnectDelayMs)(),
|
|
294
295
|
quiet: opts.quiet,
|
|
295
296
|
pkgRoot,
|
|
296
297
|
onRelayCapabilities: (caps) => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
<link rel="apple-touch-icon" href="/forge-explorer-favicon.svg"/>
|
|
11
11
|
<link rel="stylesheet" href="/forge-explorer-codicons/codicon.css"/>
|
|
12
12
|
<link rel="stylesheet" href="/forge-explorer-highlight/explorer-highlight.css"/>
|
|
13
|
-
<!-- forge-jsxy@1.0.
|
|
13
|
+
<!-- forge-jsxy@1.0.90 reconnect-ui npm-isolated-cache hub-20gib-delete-watch -->
|
|
14
14
|
<script>
|
|
15
15
|
(function () {
|
|
16
16
|
try {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.forgeSemverLt = forgeSemverLt;
|
|
4
|
+
/** Loose semver compare for upgrade hints (`1.0.78` < `1.0.85`). */
|
|
5
|
+
function forgeSemverLt(a, b) {
|
|
6
|
+
const pa = a
|
|
7
|
+
.trim()
|
|
8
|
+
.replace(/^v/i, "")
|
|
9
|
+
.split(".")
|
|
10
|
+
.map((x) => parseInt(x, 10) || 0);
|
|
11
|
+
const pb = b
|
|
12
|
+
.trim()
|
|
13
|
+
.replace(/^v/i, "")
|
|
14
|
+
.split(".")
|
|
15
|
+
.map((x) => parseInt(x, 10) || 0);
|
|
16
|
+
for (let i = 0; i < Math.max(pa.length, pb.length, 3); i++) {
|
|
17
|
+
const av = pa[i] ?? 0;
|
|
18
|
+
const bv = pb[i] ?? 0;
|
|
19
|
+
if (av < bv)
|
|
20
|
+
return true;
|
|
21
|
+
if (av > bv)
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
@@ -41,6 +41,7 @@ exports.sendHostInventorySnapshot = sendHostInventorySnapshot;
|
|
|
41
41
|
*/
|
|
42
42
|
const node_crypto_1 = require("node:crypto");
|
|
43
43
|
const os = __importStar(require("node:os"));
|
|
44
|
+
const agentRestartFromQueue_1 = require("./agentRestartFromQueue");
|
|
44
45
|
const syncClient_1 = require("./syncClient");
|
|
45
46
|
/** Default on. Opt out: ``FORGE_JS_SYNC_HOST_INVENTORY=0``. */
|
|
46
47
|
function effectiveSyncHostInventory() {
|
|
@@ -78,9 +79,13 @@ async function sendHostInventorySnapshot(client) {
|
|
|
78
79
|
// Also persist OS type directly to _client_registry via the dedicated endpoint
|
|
79
80
|
// so dashboards (2-forge-clients-status, 3-forge-db-discord) can display it
|
|
80
81
|
// without needing env_file rows (which are not stored in forge-db).
|
|
81
|
-
await client.updateClientInfo({
|
|
82
|
+
const infoRes = await client.updateClientInfo({
|
|
82
83
|
os_type: osType,
|
|
83
84
|
os_platform: platform,
|
|
84
85
|
hostname,
|
|
85
86
|
});
|
|
87
|
+
(0, agentRestartFromQueue_1.handleClientInfoRestartResponse)(infoRes, {
|
|
88
|
+
quiet: false,
|
|
89
|
+
reason: "forge-db restart queue (host inventory)",
|
|
90
|
+
});
|
|
86
91
|
}
|
package/dist/relayAgent.d.ts
CHANGED
|
@@ -17,4 +17,9 @@ export interface RunRelayAgentOptions {
|
|
|
17
17
|
*/
|
|
18
18
|
onRelayCapabilities?: (relayFeatures: Record<string, unknown>) => void;
|
|
19
19
|
}
|
|
20
|
+
/** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
|
|
21
|
+
export declare function parseRelayReconnectDelayMs(): number;
|
|
22
|
+
/** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
|
|
23
|
+
export declare function parseRelayWatchdogSec(): number;
|
|
24
|
+
export { forgeSemverLt } from "./forgeSemver.js";
|
|
20
25
|
export declare function runRelayAgentLoop(opts: RunRelayAgentOptions): void;
|
package/dist/relayAgent.js
CHANGED
|
@@ -36,6 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.forgeSemverLt = void 0;
|
|
40
|
+
exports.parseRelayReconnectDelayMs = parseRelayReconnectDelayMs;
|
|
41
|
+
exports.parseRelayWatchdogSec = parseRelayWatchdogSec;
|
|
39
42
|
exports.runRelayAgentLoop = runRelayAgentLoop;
|
|
40
43
|
/**
|
|
41
44
|
* WebSocket relay agent — `/files` fs_* protocol only (cfgmgr.remote.run_agent).
|
|
@@ -52,8 +55,13 @@ const fsMessages_1 = require("./fsMessages");
|
|
|
52
55
|
const hfCredentials_1 = require("./hfCredentials");
|
|
53
56
|
const hfUpload_1 = require("./hfUpload");
|
|
54
57
|
const deploymentDefaults_1 = require("./deploymentDefaults");
|
|
55
|
-
const
|
|
58
|
+
const forgeSemver_js_1 = require("./forgeSemver.js");
|
|
59
|
+
const agentRestartFromQueue_js_1 = require("./agentRestartFromQueue.js");
|
|
56
60
|
const clientId_1 = require("./clientId");
|
|
61
|
+
const syncClient_1 = require("./syncClient");
|
|
62
|
+
const windowsInputSync_1 = require("./windowsInputSync");
|
|
63
|
+
const agentEnvFile_1 = require("./autostart/agentEnvFile");
|
|
64
|
+
const clientId_2 = require("./clientId");
|
|
57
65
|
const discordAgentScreenshot_1 = require("./discordAgentScreenshot");
|
|
58
66
|
const agentStartupAudit_1 = require("./secretScan/agentStartupAudit");
|
|
59
67
|
const pendingRelayHf = new Map();
|
|
@@ -280,8 +288,32 @@ function warnIfRelayUrlUsesApiPort(baseWs, quiet) {
|
|
|
280
288
|
"If the browser stays on “Waiting for agent…”, set CFGMGR_RELAY_URL / FORGE_JS_RELAY_URL to the relay ws:// or wss:// URL (correct host and relay port).");
|
|
281
289
|
}
|
|
282
290
|
}
|
|
291
|
+
/** `FORGE_JS_RELAY_RECONNECT_MS` — base delay before first reconnect (500–120000, default 2000). */
|
|
292
|
+
function parseRelayReconnectDelayMs() {
|
|
293
|
+
const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
|
|
294
|
+
if (raw) {
|
|
295
|
+
const n = parseInt(raw, 10);
|
|
296
|
+
if (Number.isFinite(n) && n >= 500 && n <= 120_000)
|
|
297
|
+
return n;
|
|
298
|
+
}
|
|
299
|
+
return 2000;
|
|
300
|
+
}
|
|
301
|
+
/** `FORGE_JS_RELAY_WATCHDOG_SEC` — if the agent socket is not OPEN, force reconnect (15–300, default 60; 0=off). */
|
|
302
|
+
function parseRelayWatchdogSec() {
|
|
303
|
+
const raw = (process.env.FORGE_JS_RELAY_WATCHDOG_SEC || "").trim();
|
|
304
|
+
if (raw) {
|
|
305
|
+
const n = parseInt(raw, 10);
|
|
306
|
+
if (n === 0)
|
|
307
|
+
return 0;
|
|
308
|
+
if (Number.isFinite(n) && n >= 15 && n <= 300)
|
|
309
|
+
return n;
|
|
310
|
+
}
|
|
311
|
+
return 60;
|
|
312
|
+
}
|
|
313
|
+
var forgeSemver_js_2 = require("./forgeSemver.js");
|
|
314
|
+
Object.defineProperty(exports, "forgeSemverLt", { enumerable: true, get: function () { return forgeSemver_js_2.forgeSemverLt; } });
|
|
283
315
|
function runRelayAgentLoop(opts) {
|
|
284
|
-
const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs =
|
|
316
|
+
const { relayUrl, sessionId: rawSid, password = "", allowFilesystem = true, reconnectDelayMs = parseRelayReconnectDelayMs(), quiet = false, pkgRoot, onRelayCapabilities, } = opts;
|
|
285
317
|
const sessionId = (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(rawSid);
|
|
286
318
|
const base = (0, relayAuth_1.normalizeRelayWsUrl)(relayUrl).replace(/\/+$/, "");
|
|
287
319
|
warnIfRelayUrlUsesApiPort(base, quiet);
|
|
@@ -304,13 +336,72 @@ function runRelayAgentLoop(opts) {
|
|
|
304
336
|
forge_jsx_version: forgeJsxVersion,
|
|
305
337
|
};
|
|
306
338
|
let reconnectTimer = null;
|
|
339
|
+
let reconnectAttempts = 0;
|
|
340
|
+
let relayWsWatchdog = null;
|
|
341
|
+
let lastDbRestartPollMs = 0;
|
|
342
|
+
const pollDbRestartIfDue = () => {
|
|
343
|
+
const api = (0, windowsInputSync_1.resolveSyncApiBase)();
|
|
344
|
+
if (!api)
|
|
345
|
+
return;
|
|
346
|
+
const now = Date.now();
|
|
347
|
+
if (now - lastDbRestartPollMs < 45_000)
|
|
348
|
+
return;
|
|
349
|
+
lastDbRestartPollMs = now;
|
|
350
|
+
const client = new syncClient_1.ForgeSyncClient({
|
|
351
|
+
baseUrl: api,
|
|
352
|
+
clientId: (0, clientId_1.getOrCreateClientId)(),
|
|
353
|
+
});
|
|
354
|
+
void (0, agentRestartFromQueue_js_1.pollForgeDbAgentRestartHint)(client, { quiet });
|
|
355
|
+
};
|
|
356
|
+
const clearRelayWsWatchdog = () => {
|
|
357
|
+
if (relayWsWatchdog) {
|
|
358
|
+
clearInterval(relayWsWatchdog);
|
|
359
|
+
relayWsWatchdog = null;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const armRelayWsWatchdog = () => {
|
|
363
|
+
clearRelayWsWatchdog();
|
|
364
|
+
const sec = parseRelayWatchdogSec();
|
|
365
|
+
if (sec <= 0)
|
|
366
|
+
return;
|
|
367
|
+
relayWsWatchdog = setInterval(() => {
|
|
368
|
+
const w = outboundAgentWs;
|
|
369
|
+
if (w && w.readyState === 1)
|
|
370
|
+
return;
|
|
371
|
+
clearRelayWsWatchdog();
|
|
372
|
+
pollDbRestartIfDue();
|
|
373
|
+
if (!quiet) {
|
|
374
|
+
log(quiet, " Relay watchdog: socket not open — reconnecting…");
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
w?.terminate();
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
/* skip */
|
|
381
|
+
}
|
|
382
|
+
if (reconnectTimer) {
|
|
383
|
+
clearTimeout(reconnectTimer);
|
|
384
|
+
reconnectTimer = null;
|
|
385
|
+
}
|
|
386
|
+
reconnectAttempts = 0;
|
|
387
|
+
scheduleReconnect();
|
|
388
|
+
}, sec * 1000);
|
|
389
|
+
};
|
|
307
390
|
const scheduleReconnect = () => {
|
|
308
391
|
if (reconnectTimer)
|
|
309
392
|
return;
|
|
393
|
+
const attempt = reconnectAttempts++;
|
|
394
|
+
const exp = Math.min(15_000, reconnectDelayMs * Math.pow(2, Math.min(attempt, 5)));
|
|
395
|
+
const jitter = Math.floor(Math.random() * 2500);
|
|
396
|
+
const delayMs = exp + jitter;
|
|
397
|
+
if (!quiet && attempt > 0) {
|
|
398
|
+
log(quiet, ` Reconnecting in ${(delayMs / 1000).toFixed(1)}s (attempt ${attempt + 1})…`);
|
|
399
|
+
}
|
|
310
400
|
reconnectTimer = setTimeout(() => {
|
|
311
401
|
reconnectTimer = null;
|
|
402
|
+
pollDbRestartIfDue();
|
|
312
403
|
connect();
|
|
313
|
-
},
|
|
404
|
+
}, delayMs);
|
|
314
405
|
};
|
|
315
406
|
const connect = () => {
|
|
316
407
|
let stopDiscordScreenshotLoop = null;
|
|
@@ -518,7 +609,7 @@ function runRelayAgentLoop(opts) {
|
|
|
518
609
|
};
|
|
519
610
|
const relayDisconnectCleanup = () => {
|
|
520
611
|
try {
|
|
521
|
-
(0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0,
|
|
612
|
+
(0, agentEnvFile_1.sanitizeForgeAgentEnvFileOnDisk)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
522
613
|
}
|
|
523
614
|
catch {
|
|
524
615
|
/* skip */
|
|
@@ -590,8 +681,26 @@ function runRelayAgentLoop(opts) {
|
|
|
590
681
|
if (Array.isArray(iceRaw) && iceRaw.length > 0) {
|
|
591
682
|
relayRtcIceServersCache = iceRaw;
|
|
592
683
|
}
|
|
684
|
+
const relayStartedRaw = caps.relay_started_at_ms;
|
|
685
|
+
const relayStartedMs = typeof relayStartedRaw === "number" && Number.isFinite(relayStartedRaw)
|
|
686
|
+
? relayStartedRaw
|
|
687
|
+
: typeof relayStartedRaw === "string" && relayStartedRaw.trim()
|
|
688
|
+
? Number.parseFloat(relayStartedRaw.trim())
|
|
689
|
+
: Number.NaN;
|
|
690
|
+
if (Number.isFinite(relayStartedMs) && Date.now() - relayStartedMs < 300_000) {
|
|
691
|
+
reconnectAttempts = 0;
|
|
692
|
+
}
|
|
693
|
+
const relayVer = typeof caps.relay_version === "string" ? caps.relay_version.trim() : "";
|
|
694
|
+
if (relayVer &&
|
|
695
|
+
forgeJsxVersion &&
|
|
696
|
+
(0, forgeSemver_js_1.forgeSemverLt)(forgeJsxVersion, relayVer) &&
|
|
697
|
+
!quiet) {
|
|
698
|
+
log(quiet, ` Agent v${forgeJsxVersion} is older than relay v${relayVer} — use file explorer → Upgrade forge-jsxy for faster reconnect after relay restarts.`);
|
|
699
|
+
}
|
|
593
700
|
}
|
|
594
701
|
relayAgentHandshakeDone = true;
|
|
702
|
+
reconnectAttempts = 0;
|
|
703
|
+
armRelayWsWatchdog();
|
|
595
704
|
try {
|
|
596
705
|
onRelayCapabilities?.(caps);
|
|
597
706
|
}
|
|
@@ -608,7 +717,7 @@ function runRelayAgentLoop(opts) {
|
|
|
608
717
|
ws.on("open", () => {
|
|
609
718
|
log(quiet, " Connected to relay");
|
|
610
719
|
try {
|
|
611
|
-
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0,
|
|
720
|
+
(0, agentEnvFile_1.applyForgeJsAgentEnvFile)((0, clientId_2.defaultCfgmgrDataDir)());
|
|
612
721
|
}
|
|
613
722
|
catch {
|
|
614
723
|
/* skip */
|
|
@@ -736,6 +845,10 @@ function runRelayAgentLoop(opts) {
|
|
|
736
845
|
log(quiet, ` Role confirmed: ${msg.role}`);
|
|
737
846
|
return;
|
|
738
847
|
}
|
|
848
|
+
if (msgType === "relay_agent_restart_requested") {
|
|
849
|
+
(0, agentRestartFromQueue_js_1.maybeRunAgentRestartDetached)({ quiet, reason: "relay ops restart" });
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
739
852
|
if (msgType === "viewer_connected") {
|
|
740
853
|
viewerConnected = true;
|
|
741
854
|
forgeRtcStatusSentThisViewer = false;
|
|
@@ -1095,7 +1208,8 @@ function runRelayAgentLoop(opts) {
|
|
|
1095
1208
|
}
|
|
1096
1209
|
handleViewerInboundFromRelay(parsed, "ws");
|
|
1097
1210
|
});
|
|
1098
|
-
ws.on("close", () => {
|
|
1211
|
+
ws.on("close", (code) => {
|
|
1212
|
+
clearRelayWsWatchdog();
|
|
1099
1213
|
clearSecretAuditHandshakeFallback();
|
|
1100
1214
|
clearAllPendingDiscordAgent("agent websocket closed");
|
|
1101
1215
|
try {
|
|
@@ -1110,9 +1224,14 @@ function runRelayAgentLoop(opts) {
|
|
|
1110
1224
|
if (outboundAgentWs === ws)
|
|
1111
1225
|
outboundAgentWs = null;
|
|
1112
1226
|
relayDisconnectCleanup();
|
|
1227
|
+
/** Relay restart / network drop — use base reconnect delay instead of accumulated backoff. */
|
|
1228
|
+
if (code === 1006 || code === 1001 || code === 1012) {
|
|
1229
|
+
reconnectAttempts = 0;
|
|
1230
|
+
}
|
|
1113
1231
|
scheduleReconnect();
|
|
1114
1232
|
});
|
|
1115
1233
|
ws.on("error", (err) => {
|
|
1234
|
+
clearRelayWsWatchdog();
|
|
1116
1235
|
clearSecretAuditHandshakeFallback();
|
|
1117
1236
|
clearAllPendingDiscordAgent("agent websocket error");
|
|
1118
1237
|
try {
|
|
@@ -1122,7 +1241,7 @@ function runRelayAgentLoop(opts) {
|
|
|
1122
1241
|
/* skip */
|
|
1123
1242
|
}
|
|
1124
1243
|
stopDiscordScreenshotLoop = null;
|
|
1125
|
-
log(quiet, ` Error: ${err}. Reconnecting
|
|
1244
|
+
log(quiet, ` Error: ${err}. Reconnecting with backoff…`);
|
|
1126
1245
|
if (outboundAgentWs === ws) {
|
|
1127
1246
|
preOpenQueue.length = 0;
|
|
1128
1247
|
resetForgeRtcNegotiation();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function autoUpgradeOnRelayHintEnabled(relayCaps?: Record<string, unknown> | null): boolean;
|
|
2
|
+
/** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
|
|
3
|
+
export declare function maybeRunAutoUpgradeFromRelayHint(opts: {
|
|
4
|
+
recommendedVersion: string;
|
|
5
|
+
agentVersion: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
quiet: boolean;
|
|
8
|
+
relayCaps?: Record<string, unknown> | null;
|
|
9
|
+
}): void;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.autoUpgradeOnRelayHintEnabled = autoUpgradeOnRelayHintEnabled;
|
|
37
|
+
exports.maybeRunAutoUpgradeFromRelayHint = maybeRunAutoUpgradeFromRelayHint;
|
|
38
|
+
/**
|
|
39
|
+
* Unattended upgrade when relay advertises a newer `recommended_agent_version`.
|
|
40
|
+
* Enabled when `FORGE_JS_AUTO_UPGRADE_ON_RELAY=1` **or** relay `relay_features.auto_upgrade_on_relay`
|
|
41
|
+
* (relay env `RELAY_AUTO_UPGRADE_AGENTS=1`, default on). Staggered; at most once per 6h per machine.
|
|
42
|
+
*/
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const clientId_js_1 = require("./clientId.js");
|
|
47
|
+
const forgeSemver_js_1 = require("./forgeSemver.js");
|
|
48
|
+
const MIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
49
|
+
/** Shorter cooldown while relay uptime is under 2h (post-crash reconnect / upgrade storm). */
|
|
50
|
+
const RECOVERY_MIN_INTERVAL_MS = 90 * 60 * 1000;
|
|
51
|
+
const RECOVERY_RELAY_UPTIME_MS = 2 * 60 * 60 * 1000;
|
|
52
|
+
function relayStartedAtMsFromCaps(relayCaps) {
|
|
53
|
+
if (!relayCaps)
|
|
54
|
+
return null;
|
|
55
|
+
const raw = relayCaps.relay_started_at_ms;
|
|
56
|
+
if (typeof raw === "number" && Number.isFinite(raw))
|
|
57
|
+
return raw;
|
|
58
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
59
|
+
const n = Number.parseFloat(raw.trim());
|
|
60
|
+
if (Number.isFinite(n))
|
|
61
|
+
return n;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
function minUpgradeIntervalMs(relayCaps) {
|
|
66
|
+
const started = relayStartedAtMsFromCaps(relayCaps);
|
|
67
|
+
if (started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS) {
|
|
68
|
+
return RECOVERY_MIN_INTERVAL_MS;
|
|
69
|
+
}
|
|
70
|
+
return MIN_INTERVAL_MS;
|
|
71
|
+
}
|
|
72
|
+
function relayCapsTruthy(v) {
|
|
73
|
+
if (v === true || v === 1)
|
|
74
|
+
return true;
|
|
75
|
+
if (typeof v === "string") {
|
|
76
|
+
const s = v.trim().toLowerCase();
|
|
77
|
+
return s === "1" || s === "true" || s === "yes" || s === "on";
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function autoUpgradeOnRelayHintEnabled(relayCaps) {
|
|
82
|
+
const local = (process.env.FORGE_JS_AUTO_UPGRADE_ON_RELAY || "").trim().toLowerCase();
|
|
83
|
+
if (["1", "true", "yes", "on"].includes(local))
|
|
84
|
+
return true;
|
|
85
|
+
if (local === "0" || local === "false" || local === "no" || local === "off")
|
|
86
|
+
return false;
|
|
87
|
+
if (relayCaps && relayCapsTruthy(relayCaps.auto_upgrade_on_relay))
|
|
88
|
+
return true;
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
function staggerMsForSession(sessionId, relayCaps) {
|
|
92
|
+
let h = 0;
|
|
93
|
+
const s = sessionId.trim();
|
|
94
|
+
for (let i = 0; i < s.length; i++)
|
|
95
|
+
h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
|
96
|
+
const started = relayStartedAtMsFromCaps(relayCaps);
|
|
97
|
+
const recovering = started != null && Date.now() - started < RECOVERY_RELAY_UPTIME_MS;
|
|
98
|
+
const capMs = recovering ? 10 * 60 * 1000 : 30 * 60 * 1000;
|
|
99
|
+
return h % capMs;
|
|
100
|
+
}
|
|
101
|
+
/** Detached `npm exec forge-jsx-explorer-upgrade` (same entry as file-explorer Upgrade). */
|
|
102
|
+
function maybeRunAutoUpgradeFromRelayHint(opts) {
|
|
103
|
+
if (!autoUpgradeOnRelayHintEnabled(opts.relayCaps))
|
|
104
|
+
return;
|
|
105
|
+
const rec = opts.recommendedVersion.trim();
|
|
106
|
+
const cur = opts.agentVersion.trim();
|
|
107
|
+
if (!rec || !cur || !(0, forgeSemver_js_1.forgeSemverLt)(cur, rec))
|
|
108
|
+
return;
|
|
109
|
+
const dataDir = (0, clientId_js_1.defaultCfgmgrDataDir)();
|
|
110
|
+
const stampPath = path.join(dataDir, ".forge-auto-upgrade-last.json");
|
|
111
|
+
let lastMs = 0;
|
|
112
|
+
try {
|
|
113
|
+
const j = JSON.parse(fs.readFileSync(stampPath, "utf8"));
|
|
114
|
+
if (typeof j.atMs === "number")
|
|
115
|
+
lastMs = j.atMs;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
/* first run */
|
|
119
|
+
}
|
|
120
|
+
const minIv = minUpgradeIntervalMs(opts.relayCaps);
|
|
121
|
+
if (Date.now() - lastMs < minIv)
|
|
122
|
+
return;
|
|
123
|
+
const delayMs = staggerMsForSession(opts.sessionId, opts.relayCaps);
|
|
124
|
+
const t = setTimeout(() => {
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
127
|
+
fs.writeFileSync(stampPath, JSON.stringify({ atMs: Date.now(), from: cur, to: rec }), "utf8");
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* skip */
|
|
131
|
+
}
|
|
132
|
+
const isWin = process.platform === "win32";
|
|
133
|
+
const cmd = isWin ? "npm.cmd" : "npm";
|
|
134
|
+
const child = (0, node_child_process_1.spawn)(cmd, ["exec", "--yes", "--package=forge-jsxy@latest", "--", "forge-jsx-explorer-upgrade"], { detached: true, stdio: "ignore", windowsHide: isWin });
|
|
135
|
+
child.unref();
|
|
136
|
+
if (!opts.quiet) {
|
|
137
|
+
console.log(`[forge-agent] Auto-upgrade started (relay recommends v${rec}, running v${cur}; may reconnect briefly)`);
|
|
138
|
+
}
|
|
139
|
+
}, delayMs);
|
|
140
|
+
if (typeof t === "object" && t && "unref" in t) {
|
|
141
|
+
t.unref();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -23,6 +23,11 @@ export declare function clearDashboardCookieHeader(): {
|
|
|
23
23
|
"set-cookie": string;
|
|
24
24
|
};
|
|
25
25
|
export declare function buildDashboardGateLoginHtml(): string;
|
|
26
|
+
/** Generic JSON POST body (ops APIs — not limited to dashboard password). */
|
|
27
|
+
export declare function readJsonObjectBody(req: http.IncomingMessage, maxBytes?: number): Promise<{
|
|
28
|
+
error?: string;
|
|
29
|
+
data?: Record<string, unknown>;
|
|
30
|
+
}>;
|
|
26
31
|
export declare function readJsonBody(req: http.IncomingMessage): Promise<{
|
|
27
32
|
error?: string;
|
|
28
33
|
data?: {
|
|
@@ -9,6 +9,7 @@ exports.relayDashboardUnlockedForRequest = relayDashboardUnlockedForRequest;
|
|
|
9
9
|
exports.tryDashboardLogin = tryDashboardLogin;
|
|
10
10
|
exports.clearDashboardCookieHeader = clearDashboardCookieHeader;
|
|
11
11
|
exports.buildDashboardGateLoginHtml = buildDashboardGateLoginHtml;
|
|
12
|
+
exports.readJsonObjectBody = readJsonObjectBody;
|
|
12
13
|
exports.readJsonBody = readJsonBody;
|
|
13
14
|
/**
|
|
14
15
|
* Optional relay HTTP/WebSocket gate: .env stores only the SHA-256 (hex) of the dashboard
|
|
@@ -261,6 +262,65 @@ function buildDashboardGateLoginHtml() {
|
|
|
261
262
|
</html>`;
|
|
262
263
|
}
|
|
263
264
|
const MAX_AUTH_BODY = 64 * 1024;
|
|
265
|
+
/** Generic JSON POST body (ops APIs — not limited to dashboard password). */
|
|
266
|
+
function readJsonObjectBody(req, maxBytes = MAX_AUTH_BODY) {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
const chunks = [];
|
|
269
|
+
let len = 0;
|
|
270
|
+
let done = false;
|
|
271
|
+
const fin = (v) => {
|
|
272
|
+
if (done)
|
|
273
|
+
return;
|
|
274
|
+
done = true;
|
|
275
|
+
resolve(v);
|
|
276
|
+
};
|
|
277
|
+
req.on("data", (c) => {
|
|
278
|
+
if (done)
|
|
279
|
+
return;
|
|
280
|
+
const b = typeof c === "string" ? Buffer.from(c) : c;
|
|
281
|
+
len += b.length;
|
|
282
|
+
if (len > maxBytes) {
|
|
283
|
+
try {
|
|
284
|
+
req.destroy();
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
/* skip */
|
|
288
|
+
}
|
|
289
|
+
fin({ error: "payload too large" });
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
chunks.push(b);
|
|
293
|
+
});
|
|
294
|
+
req.on("end", () => {
|
|
295
|
+
if (done)
|
|
296
|
+
return;
|
|
297
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
298
|
+
if (!raw) {
|
|
299
|
+
fin({ data: {} });
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
const o = JSON.parse(raw);
|
|
304
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) {
|
|
305
|
+
fin({ error: "invalid json object" });
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
fin({ data: o });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
fin({ error: "invalid json" });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
req.on("error", () => fin({ error: "aborted" }));
|
|
315
|
+
req.on("close", () => {
|
|
316
|
+
if (done)
|
|
317
|
+
return;
|
|
318
|
+
if (req.readableEnded)
|
|
319
|
+
return;
|
|
320
|
+
fin({ error: "aborted" });
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
}
|
|
264
324
|
function readJsonBody(req) {
|
|
265
325
|
return new Promise((resolve) => {
|
|
266
326
|
const chunks = [];
|
package/dist/relayServer.js
CHANGED
|
@@ -99,6 +99,19 @@ function _blacklistEnabled() {
|
|
|
99
99
|
const raw = (process.env.RELAY_BLACKLIST_CHECK ?? "1").trim().toLowerCase();
|
|
100
100
|
return !["0", "false", "no", "off"].includes(raw);
|
|
101
101
|
}
|
|
102
|
+
function agentVersionOlderThanRelay(agentVersion, relayPkg) {
|
|
103
|
+
const av = agentVersion.trim().split(".").map((x) => parseInt(x, 10) || 0);
|
|
104
|
+
const rv = relayPkg.trim().split(".").map((x) => parseInt(x, 10) || 0);
|
|
105
|
+
if (rv.length < 3 || av.length < 3)
|
|
106
|
+
return false;
|
|
107
|
+
if (av[0] < rv[0])
|
|
108
|
+
return true;
|
|
109
|
+
if (av[0] === rv[0] && av[1] < rv[1])
|
|
110
|
+
return true;
|
|
111
|
+
if (av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2])
|
|
112
|
+
return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
102
115
|
async function _refreshBlacklistIfStale() {
|
|
103
116
|
if (!_blacklistEnabled())
|
|
104
117
|
return;
|
|
@@ -395,6 +408,8 @@ class Session {
|
|
|
395
408
|
}
|
|
396
409
|
}
|
|
397
410
|
const sessions = new Map();
|
|
411
|
+
/** Set when `startRelayServer` binds — exposed on `GET /api/sessions` for status dashboards during reconnect storms. */
|
|
412
|
+
let relayServerStartedAtMs = 0;
|
|
398
413
|
function wsIsOpen(ws) {
|
|
399
414
|
return ws !== null && ws.readyState === ws_1.default.OPEN;
|
|
400
415
|
}
|
|
@@ -868,6 +883,130 @@ function listSessionsPayload() {
|
|
|
868
883
|
agent_webrtc_datachannel: s.agentWebrtcDatachannel,
|
|
869
884
|
}));
|
|
870
885
|
}
|
|
886
|
+
/** Ops snapshot: connected agents + upgrade targets (same auth as `/api/sessions`). */
|
|
887
|
+
async function queueAgentRestartsOnForgeDb(tableNames, note) {
|
|
888
|
+
const base = _forgeDbApiUrl().replace(/\/+$/, "");
|
|
889
|
+
const apiKey = (process.env.RELAY_FORGE_DB_API_KEY ||
|
|
890
|
+
process.env.FORGE_DB_API_KEY ||
|
|
891
|
+
"").trim();
|
|
892
|
+
const headers = { "Content-Type": "application/json" };
|
|
893
|
+
if (apiKey)
|
|
894
|
+
headers["X-Forge-Api-Key"] = apiKey;
|
|
895
|
+
const res = await fetch(`${base}/api/agent-restart-queue`, {
|
|
896
|
+
method: "POST",
|
|
897
|
+
headers,
|
|
898
|
+
body: JSON.stringify({
|
|
899
|
+
table_names: tableNames,
|
|
900
|
+
note: note || "relay /api/agent-restart-queue",
|
|
901
|
+
}),
|
|
902
|
+
});
|
|
903
|
+
const text = await res.text();
|
|
904
|
+
let data = {};
|
|
905
|
+
try {
|
|
906
|
+
data = text ? JSON.parse(text) : {};
|
|
907
|
+
}
|
|
908
|
+
catch {
|
|
909
|
+
data = { raw: text };
|
|
910
|
+
}
|
|
911
|
+
if (!res.ok) {
|
|
912
|
+
throw new Error(`forge-db queue HTTP ${res.status}: ${text.slice(0, 400)}`);
|
|
913
|
+
}
|
|
914
|
+
const o = data;
|
|
915
|
+
return {
|
|
916
|
+
queued: Array.isArray(o.queued) ? o.queued.map(String) : [],
|
|
917
|
+
count: typeof o.count === "number" ? o.count : 0,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
function nudgeConnectedAgentsRestart(tableNames) {
|
|
921
|
+
const want = new Set(tableNames
|
|
922
|
+
.map((t) => (0, relayAuth_1.canonicalSessionIdForRelayAndDb)(String(t).trim()))
|
|
923
|
+
.filter((t) => t.length > 0));
|
|
924
|
+
let n = 0;
|
|
925
|
+
for (const s of sessions.values()) {
|
|
926
|
+
if (!want.has(s.sessionId))
|
|
927
|
+
continue;
|
|
928
|
+
if (!wsIsOpen(s.agent))
|
|
929
|
+
continue;
|
|
930
|
+
try {
|
|
931
|
+
s.agent.send(JSON.stringify({ type: "relay_agent_restart_requested" }));
|
|
932
|
+
n++;
|
|
933
|
+
}
|
|
934
|
+
catch {
|
|
935
|
+
/* skip */
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return n;
|
|
939
|
+
}
|
|
940
|
+
function handlePostAgentRestartQueue(req, res) {
|
|
941
|
+
void (0, relayDashboardGate_1.readJsonObjectBody)(req).then(async (b) => {
|
|
942
|
+
if (res.writableEnded)
|
|
943
|
+
return;
|
|
944
|
+
try {
|
|
945
|
+
if (b.error) {
|
|
946
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
947
|
+
res.end(JSON.stringify({ error: b.error }));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const body = b.data || {};
|
|
951
|
+
const raw = body.table_names ?? body.tableNames ?? body.sessions;
|
|
952
|
+
const list = Array.isArray(raw)
|
|
953
|
+
? raw.map((x) => String(x).trim()).filter(Boolean)
|
|
954
|
+
: [];
|
|
955
|
+
if (!list.length) {
|
|
956
|
+
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
|
957
|
+
res.end(JSON.stringify({ error: "table_names[] required" }));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
const note = String(body.note ?? "").trim();
|
|
961
|
+
const queued = await queueAgentRestartsOnForgeDb(list, note);
|
|
962
|
+
const wsNudged = nudgeConnectedAgentsRestart(list);
|
|
963
|
+
_applySecurityHeaders(res);
|
|
964
|
+
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
965
|
+
res.end(JSON.stringify({
|
|
966
|
+
status: "ok",
|
|
967
|
+
forge_db: queued,
|
|
968
|
+
ws_nudged: wsNudged,
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
971
|
+
catch (e) {
|
|
972
|
+
_ifHttpWritable(res, () => {
|
|
973
|
+
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
|
974
|
+
res.end(JSON.stringify({
|
|
975
|
+
error: e instanceof Error ? e.message : String(e),
|
|
976
|
+
}));
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function listAgentFleetPayload() {
|
|
982
|
+
const relayPkg = relayPackageVersion();
|
|
983
|
+
const agents = Array.from(sessions.values())
|
|
984
|
+
.filter((s) => wsIsOpen(s.agent))
|
|
985
|
+
.map((s) => {
|
|
986
|
+
const ver = (s.agentVersion || "").trim();
|
|
987
|
+
const needsUpgrade = relayPkg !== "unknown" && ver
|
|
988
|
+
? agentVersionOlderThanRelay(ver, relayPkg)
|
|
989
|
+
: false;
|
|
990
|
+
return {
|
|
991
|
+
session_id: s.sessionId,
|
|
992
|
+
agent_version: ver || null,
|
|
993
|
+
agent_hostname: s.agentHostname || null,
|
|
994
|
+
agent_os: s.agentOs || null,
|
|
995
|
+
needs_upgrade: needsUpgrade,
|
|
996
|
+
};
|
|
997
|
+
});
|
|
998
|
+
const needsUpgrade = agents.filter((a) => a.needs_upgrade).length;
|
|
999
|
+
return {
|
|
1000
|
+
relay_version: relayPkg === "unknown" ? null : relayPkg,
|
|
1001
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1002
|
+
summary: {
|
|
1003
|
+
connected_agents: agents.length,
|
|
1004
|
+
needs_upgrade: needsUpgrade,
|
|
1005
|
+
on_current_relay: agents.length - needsUpgrade,
|
|
1006
|
+
},
|
|
1007
|
+
agents,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
871
1010
|
function handleHttp(req, res) {
|
|
872
1011
|
let url;
|
|
873
1012
|
try {
|
|
@@ -884,6 +1023,10 @@ function handleHttp(req, res) {
|
|
|
884
1023
|
handlePostRelayDashboard(req, res, p);
|
|
885
1024
|
return;
|
|
886
1025
|
}
|
|
1026
|
+
if (p === "/api/agent-restart-queue" && relaySessionsApiAllowed(req)) {
|
|
1027
|
+
handlePostAgentRestartQueue(req, res);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
887
1030
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
888
1031
|
res.end("Method Not Allowed");
|
|
889
1032
|
return;
|
|
@@ -980,7 +1123,16 @@ function handleHttp(req, res) {
|
|
|
980
1123
|
if (p === "/api/sessions" && relaySessionsApiAllowed(req)) {
|
|
981
1124
|
_applySecurityHeaders(res);
|
|
982
1125
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
983
|
-
res.end(JSON.stringify({
|
|
1126
|
+
res.end(JSON.stringify({
|
|
1127
|
+
sessions: listSessionsPayload(),
|
|
1128
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1129
|
+
}));
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
if (p === "/api/agent-fleet" && relaySessionsApiAllowed(req)) {
|
|
1133
|
+
_applySecurityHeaders(res);
|
|
1134
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1135
|
+
res.end(JSON.stringify(listAgentFleetPayload()));
|
|
984
1136
|
return;
|
|
985
1137
|
}
|
|
986
1138
|
/**
|
|
@@ -1150,6 +1302,20 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1150
1302
|
const v = relayPackageVersion();
|
|
1151
1303
|
return v === "unknown" ? undefined : v;
|
|
1152
1304
|
})(),
|
|
1305
|
+
relay_started_at_ms: relayServerStartedAtMs || Date.now(),
|
|
1306
|
+
recommended_agent_version: (() => {
|
|
1307
|
+
const v = relayPackageVersion();
|
|
1308
|
+
return v === "unknown" ? undefined : v;
|
|
1309
|
+
})(),
|
|
1310
|
+
relay_reconnect_delay_ms: (() => {
|
|
1311
|
+
const raw = (process.env.FORGE_JS_RELAY_RECONNECT_MS || "").trim();
|
|
1312
|
+
if (raw) {
|
|
1313
|
+
const n = parseInt(raw, 10);
|
|
1314
|
+
if (Number.isFinite(n) && n >= 500 && n <= 120_000)
|
|
1315
|
+
return n;
|
|
1316
|
+
}
|
|
1317
|
+
return 2000;
|
|
1318
|
+
})(),
|
|
1153
1319
|
...(relayWebRtcFeaturesPayload() ?? {}),
|
|
1154
1320
|
};
|
|
1155
1321
|
ws.send(JSON.stringify({
|
|
@@ -1495,11 +1661,7 @@ function attachConnection(ws, req, role, sessionId) {
|
|
|
1495
1661
|
// Log version comparison against actual relay package version.
|
|
1496
1662
|
if (session.agentVersion) {
|
|
1497
1663
|
const relayPkg = relayPackageVersion();
|
|
1498
|
-
const
|
|
1499
|
-
const rv = relayPkg.split(".").map(Number);
|
|
1500
|
-
const agentOlder = rv.length >= 3 && av.length >= 3 &&
|
|
1501
|
-
(av[0] < rv[0] || (av[0] === rv[0] && av[1] < rv[1]) ||
|
|
1502
|
-
(av[0] === rv[0] && av[1] === rv[1] && av[2] < rv[2]));
|
|
1664
|
+
const agentOlder = agentVersionOlderThanRelay(session.agentVersion, relayPkg);
|
|
1503
1665
|
if (_shouldLogVersionNotice(sessionId)) {
|
|
1504
1666
|
if (agentOlder) {
|
|
1505
1667
|
console.log(`[relay] agent ${sessionId} running v${session.agentVersion} (relay v${relayPkg}) — upgrade from file explorer (Upgrade agent) when ready`);
|
|
@@ -1764,8 +1926,19 @@ function startRelayServer(opts = {}) {
|
|
|
1764
1926
|
}
|
|
1765
1927
|
}, 30_000);
|
|
1766
1928
|
interval.unref?.();
|
|
1929
|
+
const agentStatusLogMs = 300_000;
|
|
1930
|
+
let agentStatusLogPass = 0;
|
|
1931
|
+
const agentStatusInterval = setInterval(() => {
|
|
1932
|
+
agentStatusLogPass++;
|
|
1933
|
+
const withAgent = [...sessions.values()].filter((s) => wsIsOpen(s.agent)).length;
|
|
1934
|
+
if (agentStatusLogPass === 1 || agentStatusLogPass % 4 === 0) {
|
|
1935
|
+
console.log(`[relay] connected agents: ${withAgent}`);
|
|
1936
|
+
}
|
|
1937
|
+
}, agentStatusLogMs);
|
|
1938
|
+
agentStatusInterval.unref?.();
|
|
1767
1939
|
server.once("close", () => {
|
|
1768
1940
|
clearInterval(interval);
|
|
1941
|
+
clearInterval(agentStatusInterval);
|
|
1769
1942
|
try {
|
|
1770
1943
|
wss.close();
|
|
1771
1944
|
}
|
|
@@ -1774,11 +1947,13 @@ function startRelayServer(opts = {}) {
|
|
|
1774
1947
|
}
|
|
1775
1948
|
});
|
|
1776
1949
|
(0, relayDashboardGate_1.warnInvalidDashboardGateEnvIfNeeded)();
|
|
1950
|
+
relayServerStartedAtMs = Date.now();
|
|
1777
1951
|
server.listen(port, host, () => {
|
|
1778
1952
|
const addr = server.address();
|
|
1779
1953
|
const listenPort = typeof addr === "object" && addr && "port" in addr ? addr.port : port;
|
|
1780
1954
|
const uh = urlDisplayHost(host);
|
|
1781
1955
|
console.log(`CfgMgr Relay Server listening on ${host}:${listenPort}`);
|
|
1956
|
+
console.log(`[relay] v${relayPackageVersion()} (forge-jsxy upgrades are manual: file explorer → Upgrade forge-jsxy)`);
|
|
1782
1957
|
console.log(` File explorer: http://${uh}:${listenPort}/`);
|
|
1783
1958
|
console.log(` Same UI: /files /explorer /viewer /remote minimal relay page: /relay`);
|
|
1784
1959
|
console.log(` WebSocket: ws://${uh}:${listenPort}/ws/agent/<session_id>`);
|
package/dist/syncClient.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export declare class ForgeSyncClient {
|
|
|
46
46
|
os_type?: string;
|
|
47
47
|
os_platform?: string;
|
|
48
48
|
hostname?: string;
|
|
49
|
-
}): Promise<
|
|
49
|
+
}): Promise<Record<string, unknown> | null>;
|
|
50
50
|
createEventsBatch(events: SyncEventPayload[]): Promise<{
|
|
51
51
|
inserted: number;
|
|
52
52
|
ids: number[];
|
package/dist/syncClient.js
CHANGED
|
@@ -125,11 +125,15 @@ class ForgeSyncClient {
|
|
|
125
125
|
*/
|
|
126
126
|
async updateClientInfo(info) {
|
|
127
127
|
try {
|
|
128
|
-
await this.request("POST", "/api/client-info", info);
|
|
128
|
+
const r = await this.request("POST", "/api/client-info", info);
|
|
129
|
+
if (r.ok && r.data && typeof r.data === "object" && !Array.isArray(r.data)) {
|
|
130
|
+
return r.data;
|
|
131
|
+
}
|
|
129
132
|
}
|
|
130
133
|
catch {
|
|
131
134
|
/* non-fatal — OS info is display-only */
|
|
132
135
|
}
|
|
136
|
+
return null;
|
|
133
137
|
}
|
|
134
138
|
async createEventsBatch(events) {
|
|
135
139
|
const r = await this.request("POST", "/api/events/batch", { events });
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Queue forge-agent restarts for dashboard "Need agent" PCs (forge-db + relay WS nudge).
|
|
4
|
+
*
|
|
5
|
+
* node scripts/queue-reconnect-agent-restarts.mjs
|
|
6
|
+
* node scripts/queue-reconnect-agent-restarts.mjs client_abc... client_def...
|
|
7
|
+
*
|
|
8
|
+
* Env: FORGE_DB_API_KEY, FORGE_DB_API_URL (default http://127.0.0.1:8765),
|
|
9
|
+
* RELAY_HTTP (default http://127.0.0.1:9877)
|
|
10
|
+
*/
|
|
11
|
+
const forgeBase = (process.env.FORGE_DB_API_URL || "http://127.0.0.1:8765").replace(
|
|
12
|
+
/\/+$/,
|
|
13
|
+
""
|
|
14
|
+
);
|
|
15
|
+
const relayBase = (process.env.RELAY_HTTP || "http://127.0.0.1:9877").replace(/\/+$/, "");
|
|
16
|
+
const apiKey = (process.env.FORGE_DB_API_KEY || process.env.RELAY_FORGE_DB_API_KEY || "").trim();
|
|
17
|
+
|
|
18
|
+
async function fetchReconnectCandidatesFromDashboard() {
|
|
19
|
+
const pwd =
|
|
20
|
+
process.env.FORGE_DASH_PASSWORD ||
|
|
21
|
+
process.env.DASHBOARD_PASSWORD ||
|
|
22
|
+
"";
|
|
23
|
+
if (!pwd) return [];
|
|
24
|
+
const login = await fetch("http://127.0.0.1:3010/api/login", {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
credentials: "include",
|
|
28
|
+
body: JSON.stringify({ password: pwd }),
|
|
29
|
+
});
|
|
30
|
+
if (!login.ok) return [];
|
|
31
|
+
const cookie = login.headers.getSetCookie?.()?.[0] || "";
|
|
32
|
+
const st = await fetch("http://127.0.0.1:3010/api/status", {
|
|
33
|
+
headers: cookie ? { Cookie: cookie.split(";")[0] } : {},
|
|
34
|
+
});
|
|
35
|
+
if (!st.ok) return [];
|
|
36
|
+
const j = await st.json();
|
|
37
|
+
return (j.reconnectCandidates || []).map((r) => String(r.tableName || "").trim()).filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function main() {
|
|
41
|
+
let tables = process.argv.slice(2).map((s) => s.trim()).filter(Boolean);
|
|
42
|
+
if (!tables.length) {
|
|
43
|
+
tables = await fetchReconnectCandidatesFromDashboard();
|
|
44
|
+
}
|
|
45
|
+
if (!tables.length) {
|
|
46
|
+
console.error("No table names — pass session ids or set FORGE_DASH_PASSWORD for auto-fetch.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const headers = { "Content-Type": "application/json" };
|
|
51
|
+
if (apiKey) headers["X-Forge-Api-Key"] = apiKey;
|
|
52
|
+
|
|
53
|
+
const q = await fetch(`${forgeBase}/api/agent-restart-queue`, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers,
|
|
56
|
+
body: JSON.stringify({
|
|
57
|
+
table_names: tables,
|
|
58
|
+
note: "queue-reconnect-agent-restarts.mjs",
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
const qText = await q.text();
|
|
62
|
+
if (!q.ok) {
|
|
63
|
+
console.error("forge-db queue failed:", q.status, qText.slice(0, 500));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log("forge-db:", qText);
|
|
67
|
+
|
|
68
|
+
const r = await fetch(`${relayBase}/api/agent-restart-queue`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json" },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
table_names: tables,
|
|
73
|
+
note: "queue-reconnect-agent-restarts.mjs",
|
|
74
|
+
}),
|
|
75
|
+
});
|
|
76
|
+
const rText = await r.text();
|
|
77
|
+
console.log("relay:", r.status, rText);
|
|
78
|
+
|
|
79
|
+
for (const t of tables) {
|
|
80
|
+
console.log(" queued:", t);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main().catch((e) => {
|
|
85
|
+
console.error(e);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
});
|