maxpool 1.5.39 → 1.5.40
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/package.json +1 -1
- package/src/index.js +29 -6
- package/src/restart-controller.js +4 -1
- package/src/updater.js +84 -28
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loginOAuth, fetchProfile, refreshAccessToken, isTokenExpiringSoon, toke
|
|
|
12
12
|
import { TUI } from './tui.js';
|
|
13
13
|
import { RestartController } from './restart-controller.js';
|
|
14
14
|
import { resolveAccounts } from './account-config.js';
|
|
15
|
-
import { maybeCheckForUpdate, getCurrentVersion } from './updater.js';
|
|
15
|
+
import { maybeCheckForUpdate, getCurrentVersion, markApplied } from './updater.js';
|
|
16
16
|
import {
|
|
17
17
|
runReloadBaton,
|
|
18
18
|
RELOAD_SWAPPED, RELOAD_ROLLED_BACK,
|
|
@@ -1025,11 +1025,31 @@ async function serverWorkerCommand() {
|
|
|
1025
1025
|
// would permanently disable all future update detection. It only refreshes
|
|
1026
1026
|
// versionInfo (the persistent TUI banner is the reminder, so no repeated log spam);
|
|
1027
1027
|
// autoUpdate progress lines still surface. unref so it never blocks a clean exit.
|
|
1028
|
+
const notifyUpdate = msg => (tui?._addLog ? tui._addLog(msg) : console.log(`[Maxpool] ${msg}`));
|
|
1029
|
+
// Fully-automatic apply (opt-in autoApply): mark the version attempted, THEN seamlessly
|
|
1030
|
+
// reload into the freshly-installed code (sessions survive). Marking here — at the
|
|
1031
|
+
// moment we act — is what makes the quarantine truthful: a boot-broken release is
|
|
1032
|
+
// attempted once, then blocked until an even newer version appears (no reload loop, no
|
|
1033
|
+
// stranded download). Shared by the startup one-shot AND the periodic timer.
|
|
1034
|
+
const applyUpdateIfReady = r => {
|
|
1035
|
+
if (r?.applicable && config?.autoApply && restartController) {
|
|
1036
|
+
markApplied(r.installedVersion);
|
|
1037
|
+
notifyUpdate('Applying update — seamless reload…');
|
|
1038
|
+
restartController.requestRestart();
|
|
1039
|
+
}
|
|
1040
|
+
};
|
|
1028
1041
|
const updateIntervalMs = Math.max(60_000, Number(process.env.MAXPOOL_UPDATE_CHECK_INTERVAL_MS) || 6 * 60 * 60 * 1000);
|
|
1029
1042
|
updateTimer = setInterval(() => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1043
|
+
// ONLY the lease-holding primary self-installs — the timer is unconditional across
|
|
1044
|
+
// EVERY worker (incl. a headless reload worker), so gate on hasLease here to avoid
|
|
1045
|
+
// two `npm i -g` racing during a reload overlap (global-package corruption). A
|
|
1046
|
+
// headless reload worker never holds the lease, so it never installs/auto-applies.
|
|
1047
|
+
if (config?.updateCheck === false || !hasLease) return;
|
|
1048
|
+
// announce:false — the persistent TUI banner is the passive reminder; only real
|
|
1049
|
+
// actions (installing / applying) log here, so a pending update never churns the log.
|
|
1050
|
+
maybeCheckForUpdate(config, notifyUpdate, info => { accountManager.versionInfo = info; }, { announce: false })
|
|
1051
|
+
.then(applyUpdateIfReady)
|
|
1052
|
+
.catch(() => {});
|
|
1033
1053
|
}, updateIntervalMs);
|
|
1034
1054
|
updateTimer.unref();
|
|
1035
1055
|
|
|
@@ -1079,8 +1099,11 @@ async function serverWorkerCommand() {
|
|
|
1079
1099
|
// A reload-spawned/takeover worker must NEVER re-probe (1x not 2x traffic).
|
|
1080
1100
|
if (!viaTakeover && !isReloadWorker) {
|
|
1081
1101
|
if (process.env.MAXPOOL_TEST_LOG_UPDATE_CHECK === '1') console.log('[Maxpool] UPDATE_CHECK_FIRED');
|
|
1082
|
-
|
|
1083
|
-
|
|
1102
|
+
// Cold-start-behind: download AND (autoApply) self-apply via the SAME helper as the
|
|
1103
|
+
// periodic path — so the version is applied, not marked-attempted-then-stranded.
|
|
1104
|
+
maybeCheckForUpdate(config, notifyUpdate, info => { accountManager.versionInfo = info; })
|
|
1105
|
+
.then(applyUpdateIfReady)
|
|
1106
|
+
.catch(() => {});
|
|
1084
1107
|
} else if (process.env.MAXPOOL_TEST_LOG_UPDATE_CHECK === '1') {
|
|
1085
1108
|
console.log('[Maxpool] UPDATE_CHECK_SKIPPED (reload)');
|
|
1086
1109
|
}
|
|
@@ -67,7 +67,10 @@ export class RestartController {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
requestRestart() {
|
|
70
|
-
|
|
70
|
+
// Idempotent against a restart ALREADY in progress — restarting OR pending (drain).
|
|
71
|
+
// Without the `pending` guard a second call (e.g. auto-apply firing while a manual
|
|
72
|
+
// restart is draining) would orphan the first _drainTimer and re-log.
|
|
73
|
+
if (this.restarting || this.pending) return;
|
|
71
74
|
this.pauseAdmission();
|
|
72
75
|
if (this.upstreamRequests.size === 0) {
|
|
73
76
|
this._restart();
|
package/src/updater.js
CHANGED
|
@@ -68,43 +68,99 @@ export async function selfUpdate({ timeoutMs = 120_000 } = {}) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// The version of the CODE currently EXECUTING, captured ONCE before any self-install.
|
|
72
|
+
// getCurrentVersion() reads package.json FROM DISK, which `npm i -g` rewrites — so
|
|
73
|
+
// after a background self-install the disk version != the running version. Every
|
|
74
|
+
// "am I behind?" and loop-guard decision keys on THIS fixed value, not the disk read.
|
|
75
|
+
let _bootVersion;
|
|
76
|
+
// The newest version this process has already ATTEMPTED to auto-apply. The loop guard:
|
|
77
|
+
// a version that installs fine but fails to BOOT rolls back to the old worker, which is
|
|
78
|
+
// still running _bootVersion with its timer armed — without this it would re-detect the
|
|
79
|
+
// on-disk version every check and re-apply forever (a boot-broken release → infinite
|
|
80
|
+
// reload loop, ~30s of refused admission each cycle). We apply only versions strictly
|
|
81
|
+
// newer than max(_bootVersion, _lastAttemptedTarget), so a rolled-back target is
|
|
82
|
+
// quarantined until an even newer release appears.
|
|
83
|
+
let _lastAttemptedTarget = null;
|
|
84
|
+
|
|
85
|
+
/** Test-only: reset the module's version-tracking state between cases. */
|
|
86
|
+
export function __resetUpdaterState() { _bootVersion = undefined; _lastAttemptedTarget = null; }
|
|
87
|
+
|
|
88
|
+
/** Mark a version as ATTEMPTED-to-apply. The caller calls this at the moment it triggers
|
|
89
|
+
* the reload — BEFORE the reload — so a rolled-back target is quarantined (advance-only).
|
|
90
|
+
* Kept as the caller's action (not a side effect of maybeCheckForUpdate) so a caller that
|
|
91
|
+
* ignores `applicable` can never strand a version or poison the quarantine floor. */
|
|
92
|
+
export function markApplied(version) {
|
|
93
|
+
if (version && (!_lastAttemptedTarget || compareVersions(version, _lastAttemptedTarget) > 0)) {
|
|
94
|
+
_lastAttemptedTarget = version;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
71
98
|
/**
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
99
|
+
* Check for an update; with `config.autoUpdate` also self-install; with
|
|
100
|
+
* `config.autoApply` SIGNAL that the caller should seamlessly reload to APPLY it
|
|
101
|
+
* (returns `applicable:true` — the CALLER then markApplied()+reloads). Failures swallowed.
|
|
102
|
+
* Deps are injectable for tests.
|
|
103
|
+
*
|
|
104
|
+
* `onVersionInfo` is ALWAYS invoked with { current, latest, hasUpdate, checkedAt } —
|
|
105
|
+
* current + hasUpdate keyed on the RUNNING version (so the banner stays accurate after a
|
|
106
|
+
* background download, until the reload). `deps.announce===false` suppresses the passive
|
|
107
|
+
* "Update available" line (the periodic path — the persistent banner already shows it).
|
|
76
108
|
*
|
|
77
|
-
*
|
|
78
|
-
* checkedAt } — even when up-to-date, offline, or updateCheck is off — so the TUI
|
|
79
|
-
* header / status can show the running version + whether an update is available.
|
|
109
|
+
* Returns { hasUpdate, applicable, installedVersion? }.
|
|
80
110
|
*/
|
|
81
|
-
export async function maybeCheckForUpdate(config, notify, onVersionInfo) {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
111
|
+
export async function maybeCheckForUpdate(config, notify, onVersionInfo, deps = {}) {
|
|
112
|
+
const _get = deps.getCurrentVersion || getCurrentVersion;
|
|
113
|
+
const _check = deps.checkForUpdate || checkForUpdate;
|
|
114
|
+
const _self = deps.selfUpdate || selfUpdate;
|
|
115
|
+
const announce = deps.announce !== false;
|
|
116
|
+
|
|
117
|
+
if (_bootVersion === undefined) _bootVersion = await _get();
|
|
118
|
+
const current = _bootVersion;
|
|
119
|
+
const result = config?.updateCheck === false ? null : await _check(current);
|
|
120
|
+
const hasUpdate = Boolean(result?.hasUpdate);
|
|
86
121
|
|
|
87
122
|
if (onVersionInfo) {
|
|
88
123
|
try {
|
|
89
|
-
onVersionInfo({
|
|
90
|
-
|
|
91
|
-
latest: result?.latest ?? null,
|
|
92
|
-
hasUpdate: Boolean(result?.hasUpdate),
|
|
93
|
-
checkedAt: Date.now(),
|
|
94
|
-
});
|
|
95
|
-
} catch { /* the indicator is best-effort; never break startup */ }
|
|
124
|
+
onVersionInfo({ current, latest: result?.latest ?? null, hasUpdate, checkedAt: Date.now() });
|
|
125
|
+
} catch { /* indicator is best-effort; never break startup */ }
|
|
96
126
|
}
|
|
97
127
|
|
|
98
|
-
if (!
|
|
128
|
+
if (!hasUpdate) return { hasUpdate: false, applicable: false };
|
|
129
|
+
if (announce) notify(`Update available: ${result.current} → ${result.latest}`);
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
|
|
131
|
+
if (!config?.autoUpdate) {
|
|
132
|
+
if (announce) notify(`Run 'npm i -g ${PACKAGE}' to update, or set "autoUpdate": true in your config.`);
|
|
133
|
+
return { hasUpdate: true, applicable: false };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Skip the reinstall if the latest is ALREADY on disk (a prior check downloaded it) —
|
|
137
|
+
// otherwise every periodic check re-runs `npm i -g` + re-logs until a manual restart.
|
|
138
|
+
const onDisk = await _get();
|
|
139
|
+
if (!onDisk || compareVersions(onDisk, result.latest) < 0) {
|
|
102
140
|
notify(`Auto-updating to ${result.latest}…`);
|
|
103
|
-
const r = await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
const r = await _self();
|
|
142
|
+
if (!r.ok) {
|
|
143
|
+
notify(`Auto-update failed: ${r.error}. Run: npm i -g ${PACKAGE}`);
|
|
144
|
+
return { hasUpdate: true, applicable: false };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const installed = await _get();
|
|
149
|
+
// Loop/quarantine guard — a version is "applicable" only if it is strictly newer than
|
|
150
|
+
// the newest we've already booted-as OR already ATTEMPTED to apply (markApplied). This
|
|
151
|
+
// function has NO side effect on that floor: the CALLER marks it when it triggers the
|
|
152
|
+
// reload, so a caller that ignores `applicable` can't strand a version or poison state.
|
|
153
|
+
const floor = (_lastAttemptedTarget && compareVersions(_lastAttemptedTarget, current) > 0)
|
|
154
|
+
? _lastAttemptedTarget : current;
|
|
155
|
+
const advanced = Boolean(installed) && compareVersions(installed, current) > 0;
|
|
156
|
+
const applicable = advanced && compareVersions(installed, floor) > 0;
|
|
157
|
+
|
|
158
|
+
// Passive notices only. The "Applying now…" line + the reload are the caller's (it
|
|
159
|
+
// logs them exactly when it acts), so nothing here can claim an apply that didn't happen.
|
|
160
|
+
if (advanced && !applicable) {
|
|
161
|
+
if (announce) notify(`Update ${installed} already attempted — staying on ${current}; will retry only a newer release.`);
|
|
162
|
+
} else if (advanced && !config?.autoApply) {
|
|
163
|
+
if (announce) notify(`Updated to ${installed}. Restart maxpool to apply (sessions are not interrupted).`);
|
|
109
164
|
}
|
|
165
|
+
return { hasUpdate: true, installedVersion: installed, applicable };
|
|
110
166
|
}
|