om-memory-system 5.8.0 → 5.9.1
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/bin/omms-launch.mjs +8 -4
- package/dist/cli/web-command.js +26 -10
- package/dist/cli/web-step-aside.d.ts +17 -0
- package/dist/cli/web-step-aside.js +34 -0
- package/dist/services/web-server.d.ts +1 -1
- package/dist/services/web-server.js +1 -1
- package/dist/services/web-update-runtime.d.ts +1 -1
- package/dist/services/web-update-runtime.js +2 -1
- package/dist/services/web-update.d.ts +14 -1
- package/dist/services/web-update.js +39 -1
- package/dist/web/assets/index-BX_zZiKa.js +170 -0
- package/dist/web/assets/index-Be6Uh64d.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-2BNRLGqe.css +0 -2
- package/dist/web/assets/index-B94xLmhL.js +0 -170
package/bin/omms-launch.mjs
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
//
|
|
6
6
|
// node omms-launch.mjs [--at-least-own-version] <om-memory-system arguments>
|
|
7
7
|
//
|
|
8
|
-
// Candidates: the
|
|
9
|
-
//
|
|
8
|
+
// Candidates: the global install beside the running Node.js, the copy named in
|
|
9
|
+
// ~/.omms/runtime.json, and the copy that holds this file. On a tie the first wins. With
|
|
10
10
|
// `--at-least-own-version`, the version in the `package.json` beside this file is
|
|
11
11
|
// the minimum. When no candidate reaches it, the launcher runs
|
|
12
12
|
// `npx --yes om-memory-system@<that version>`.
|
|
@@ -96,7 +96,11 @@ export function globalPackageRoots(execPath, platform = process.platform) {
|
|
|
96
96
|
return brew ? [direct, join(brew[1], "lib", "node_modules", "om-memory-system")] : [direct];
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* The global installs, the record's copy, and the own copy, in that order. Invalid
|
|
101
|
+
* ones are null. On a tie the global install wins, so a host's cache copy does not
|
|
102
|
+
* keep serving after the global install catches up.
|
|
103
|
+
*/
|
|
100
104
|
export function findCandidates({ dir, execPath, ownRoot }) {
|
|
101
105
|
let recorded = null;
|
|
102
106
|
try {
|
|
@@ -106,7 +110,7 @@ export function findCandidates({ dir, execPath, ownRoot }) {
|
|
|
106
110
|
/* No record, or an unreadable one: the other candidates still work. */
|
|
107
111
|
}
|
|
108
112
|
const globals = globalPackageRoots(execPath).map((root) => readCopy(root));
|
|
109
|
-
return [
|
|
113
|
+
return [...globals, recorded, ownRoot ? readCopy(ownRoot) : null];
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
/**
|
package/dist/cli/web-command.js
CHANGED
|
@@ -130,6 +130,17 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
|
|
|
130
130
|
process.once("SIGTERM", stop);
|
|
131
131
|
// The page's Stop and Restart buttons reach the same process through this callback.
|
|
132
132
|
const { createPowerAction } = await import("./web-power.js");
|
|
133
|
+
// A restart whose copy failed serves again from this process.
|
|
134
|
+
// Safe to call twice: it replaces the keeper and the signal handlers instead of adding more.
|
|
135
|
+
const resumeServer = async () => {
|
|
136
|
+
clearInterval(keeper);
|
|
137
|
+
keeper = setInterval(() => { }, 60_000);
|
|
138
|
+
process.off("SIGINT", stop);
|
|
139
|
+
process.off("SIGTERM", stop);
|
|
140
|
+
process.once("SIGINT", stop);
|
|
141
|
+
process.once("SIGTERM", stop);
|
|
142
|
+
await server.start();
|
|
143
|
+
};
|
|
133
144
|
const powerAction = createPowerAction({
|
|
134
145
|
stopServer: async () => {
|
|
135
146
|
clearInterval(keeper);
|
|
@@ -137,13 +148,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
|
|
|
137
148
|
process.off("SIGTERM", stop);
|
|
138
149
|
await server.stop();
|
|
139
150
|
},
|
|
140
|
-
|
|
141
|
-
resumeServer: async () => {
|
|
142
|
-
keeper = setInterval(() => { }, 60_000);
|
|
143
|
-
process.once("SIGINT", stop);
|
|
144
|
-
process.once("SIGTERM", stop);
|
|
145
|
-
await server.start();
|
|
146
|
-
},
|
|
151
|
+
resumeServer,
|
|
147
152
|
baseUrl: server.getUrl(),
|
|
148
153
|
loginItem: action === "--login-item",
|
|
149
154
|
autostart: options,
|
|
@@ -156,12 +161,23 @@ export async function runWebCommand(args, options = {}, online, handover = {}, u
|
|
|
156
161
|
const { existsSync } = await import("node:fs");
|
|
157
162
|
// Every restart onto the new copy runs the launcher, the login item's included.
|
|
158
163
|
const canRestart = () => existsSync(launcherPath(ommsDir()));
|
|
159
|
-
server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart));
|
|
160
|
-
// A newer
|
|
164
|
+
server.setWebUpdate(startWebUpdate(() => powerAction("update"), canRestart, () => server.powerActionRunning));
|
|
165
|
+
// A newer caller asked this web app to give up the port. If the caller's copy
|
|
166
|
+
// never takes it, restart onto the newest copy instead of leaving it empty.
|
|
167
|
+
const { leaveAfterStepAside } = await import("./web-step-aside.js");
|
|
168
|
+
const { log } = await import("../services/logger.js");
|
|
161
169
|
server.setOnStepAside(async () => {
|
|
162
170
|
clearInterval(keeper);
|
|
163
171
|
await server.stop();
|
|
164
|
-
|
|
172
|
+
await leaveAfterStepAside({
|
|
173
|
+
ownerAnswers: () => server.checkServerAvailable(),
|
|
174
|
+
restartNewest: () => powerAction("update"),
|
|
175
|
+
resume: resumeServer,
|
|
176
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
177
|
+
now: () => Date.now(),
|
|
178
|
+
exit: (code) => process.exit(code),
|
|
179
|
+
log,
|
|
180
|
+
});
|
|
165
181
|
});
|
|
166
182
|
return 0;
|
|
167
183
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LogLevel } from "../services/logger.js";
|
|
2
|
+
/** How long the old web app waits for the caller's copy to answer. */
|
|
3
|
+
export declare const NEW_OWNER_WAIT_MS = 30000;
|
|
4
|
+
export interface StepAsideDeps {
|
|
5
|
+
/** True when an OMMS web app answers on the configured port. */
|
|
6
|
+
ownerAnswers: () => Promise<boolean>;
|
|
7
|
+
/** Restart onto the newest copy through the launcher. Returns a failure code when it did not. */
|
|
8
|
+
restartNewest: () => Promise<string | undefined>;
|
|
9
|
+
/** Serve again from this process. */
|
|
10
|
+
resume: () => Promise<void>;
|
|
11
|
+
sleep: (ms: number) => Promise<void>;
|
|
12
|
+
now: () => number;
|
|
13
|
+
exit: (code: number) => void;
|
|
14
|
+
log: (message: string, data: Record<string, unknown>, level?: LogLevel) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Call after this web app stopped serving. It never leaves the port empty when it can help it. */
|
|
17
|
+
export declare function leaveAfterStepAside(deps: StepAsideDeps): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// What a standalone web app does after it stops serving for a newer caller. The
|
|
2
|
+
// caller starts the newer copy, but a caller can quit first (a host that restarts
|
|
3
|
+
// for its own update), which left the port empty until some other host started a
|
|
4
|
+
// web app. So the old web app waits for a new owner, and restarts onto the newest
|
|
5
|
+
// copy itself when none comes.
|
|
6
|
+
/** How long the old web app waits for the caller's copy to answer. */
|
|
7
|
+
export const NEW_OWNER_WAIT_MS = 30_000;
|
|
8
|
+
const POLL_MS = 500;
|
|
9
|
+
/** Call after this web app stopped serving. It never leaves the port empty when it can help it. */
|
|
10
|
+
export async function leaveAfterStepAside(deps) {
|
|
11
|
+
const began = deps.now();
|
|
12
|
+
while (deps.now() - began < NEW_OWNER_WAIT_MS) {
|
|
13
|
+
if (await deps.ownerAnswers()) {
|
|
14
|
+
deps.exit(0);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
await deps.sleep(POLL_MS);
|
|
18
|
+
}
|
|
19
|
+
deps.log("Web app step-aside", { code: "no-new-owner" }, "warn");
|
|
20
|
+
// A successful restart exits this process.
|
|
21
|
+
const code = await deps.restartNewest();
|
|
22
|
+
if (code === undefined)
|
|
23
|
+
return;
|
|
24
|
+
// A failed restart can already serve again from this process.
|
|
25
|
+
if (await deps.ownerAnswers())
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
await deps.resume();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
deps.log("Web app step-aside", { code: "resume-failed" }, "error");
|
|
32
|
+
deps.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -43,7 +43,7 @@ export declare class WebServer {
|
|
|
43
43
|
private readonly startedAt;
|
|
44
44
|
private onPowerActionCallback;
|
|
45
45
|
private webUpdate;
|
|
46
|
-
|
|
46
|
+
powerActionRunning: boolean;
|
|
47
47
|
private stepAsideTimer;
|
|
48
48
|
private holdOffTimer;
|
|
49
49
|
private portsExhaustedNotified;
|
|
@@ -173,7 +173,7 @@ export class WebServer {
|
|
|
173
173
|
startedAt = Date.now();
|
|
174
174
|
onPowerActionCallback = null;
|
|
175
175
|
webUpdate = null;
|
|
176
|
-
powerActionRunning = false;
|
|
176
|
+
powerActionRunning = false; // public: the automatic update stays quiet while one runs
|
|
177
177
|
stepAsideTimer = null;
|
|
178
178
|
holdOffTimer = null;
|
|
179
179
|
portsExhaustedNotified = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WebUpdate, type InstallDeps } from "./web-update.js";
|
|
2
2
|
/** The npm check and installer of a standalone web app, with the real process, npm, and clock. */
|
|
3
|
-
export declare function startWebUpdate(restart: () => Promise<string | undefined>, canRestart: () => boolean): WebUpdate;
|
|
3
|
+
export declare function startWebUpdate(restart: () => Promise<string | undefined>, canRestart: () => boolean, autoBlocked?: () => boolean): WebUpdate;
|
|
4
4
|
/** The real npm process, shared by the update button and `web update`. */
|
|
5
5
|
export declare function nodeInstallDeps(): InstallDeps;
|
|
@@ -5,7 +5,7 @@ import { log } from "./logger.js";
|
|
|
5
5
|
import { packageVersion } from "./package-version.js";
|
|
6
6
|
import { WebUpdate } from "./web-update.js";
|
|
7
7
|
/** The npm check and installer of a standalone web app, with the real process, npm, and clock. */
|
|
8
|
-
export function startWebUpdate(restart, canRestart) {
|
|
8
|
+
export function startWebUpdate(restart, canRestart, autoBlocked) {
|
|
9
9
|
const update = new WebUpdate({
|
|
10
10
|
version: packageVersion(),
|
|
11
11
|
enabled: process.env.OMMS_DISABLE_UPDATE_CHECK !== "1",
|
|
@@ -18,6 +18,7 @@ export function startWebUpdate(restart, canRestart) {
|
|
|
18
18
|
globalVersion: () => globalCommandVersion({ find: () => null }).version,
|
|
19
19
|
restart,
|
|
20
20
|
canRestart,
|
|
21
|
+
autoBlocked,
|
|
21
22
|
now: () => Date.now(),
|
|
22
23
|
});
|
|
23
24
|
update.start();
|
|
@@ -4,6 +4,8 @@ export declare const UPDATE_CHECK_INTERVAL_MS: number;
|
|
|
4
4
|
export declare const STATUS_RECHECK_AFTER_MS: number;
|
|
5
5
|
/** An install that takes longer is stopped, and the web app keeps serving. */
|
|
6
6
|
export declare const INSTALL_TIMEOUT_MS: number;
|
|
7
|
+
/** An automatic start waits this long after a failed automatic install. */
|
|
8
|
+
export declare const AUTO_RETRY_AFTER_MS: number;
|
|
7
9
|
export type UpdateState = "idle" | "installing" | "restarting" | "failed";
|
|
8
10
|
/** The `update` field of `GET /api/web/status`. */
|
|
9
11
|
export interface UpdateStatus {
|
|
@@ -35,10 +37,12 @@ export interface WebUpdateDeps {
|
|
|
35
37
|
shell: boolean;
|
|
36
38
|
path?: string;
|
|
37
39
|
}) => InstallChild;
|
|
38
|
-
/** The version of the global install
|
|
40
|
+
/** The version of the global install (also read before npm runs), or null. */
|
|
39
41
|
globalVersion: () => string | null;
|
|
40
42
|
/** False when no restart could serve the new copy, such as a missing launcher. Checked before npm runs. */
|
|
41
43
|
canRestart: () => boolean;
|
|
44
|
+
/** True while a Stop or Restart runs; the automatic start stays quiet then. */
|
|
45
|
+
autoBlocked?: () => boolean;
|
|
42
46
|
/** Stop npm and its children. On Windows the shell wrapper alone would leave npm running. */
|
|
43
47
|
killTree: (child: InstallChild) => Promise<void>;
|
|
44
48
|
/** Restart onto the newest copy. Returns a failure code when this web app keeps serving. */
|
|
@@ -68,12 +72,21 @@ export declare class WebUpdate {
|
|
|
68
72
|
private started;
|
|
69
73
|
private lastCheck;
|
|
70
74
|
private checking;
|
|
75
|
+
private autoRetryAt;
|
|
76
|
+
private autoAttempt;
|
|
71
77
|
constructor(deps: WebUpdateDeps);
|
|
72
78
|
/** Check now, then every 10 minutes. A second call does nothing. */
|
|
73
79
|
start(): void;
|
|
74
80
|
/** Read npm `latest`. A failed read keeps the last result. Calls during a read share it. */
|
|
75
81
|
check(): Promise<void>;
|
|
76
82
|
private read;
|
|
83
|
+
/**
|
|
84
|
+
* The older of the running copy and the global install. A web app can run from a
|
|
85
|
+
* host's cache copy that is already current while the global install stays behind.
|
|
86
|
+
*/
|
|
87
|
+
private installedVersion;
|
|
88
|
+
/** Start the install the Update button starts, once a check found a release. */
|
|
89
|
+
private maybeAutoInstall;
|
|
77
90
|
status(): UpdateStatus;
|
|
78
91
|
private canInstall;
|
|
79
92
|
/** Start the install and the restart. A request while one runs joins it. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { posix, win32 } from "node:path";
|
|
2
2
|
import { availableUpdate, latestNpmVersion } from "./update-check.js";
|
|
3
|
+
import { compareVersions } from "./version-compare.js";
|
|
3
4
|
// The web app's npm release check. It reads npm `latest` at start, every 10
|
|
4
5
|
// minutes, and when an open page reads the status and the last check is over a
|
|
5
6
|
// minute old. It keeps the result in memory for `GET /api/web/status`. It imports
|
|
@@ -9,6 +10,8 @@ export const UPDATE_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
|
9
10
|
export const STATUS_RECHECK_AFTER_MS = 60 * 1000;
|
|
10
11
|
/** An install that takes longer is stopped, and the web app keeps serving. */
|
|
11
12
|
export const INSTALL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
13
|
+
/** An automatic start waits this long after a failed automatic install. */
|
|
14
|
+
export const AUTO_RETRY_AFTER_MS = 30 * 60 * 1000;
|
|
12
15
|
/** Only a plain release reaches the npm arguments; the value comes from the registry. */
|
|
13
16
|
const PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
14
17
|
/** Enough of npm's error stream to find a code; the rest is dropped. */
|
|
@@ -29,6 +32,8 @@ export class WebUpdate {
|
|
|
29
32
|
started = false;
|
|
30
33
|
lastCheck = -Infinity;
|
|
31
34
|
checking = null;
|
|
35
|
+
autoRetryAt = -Infinity;
|
|
36
|
+
autoAttempt = false;
|
|
32
37
|
constructor(deps) {
|
|
33
38
|
this.deps = deps;
|
|
34
39
|
}
|
|
@@ -54,7 +59,36 @@ export class WebUpdate {
|
|
|
54
59
|
this.deps.log("Web app update check failed", { code: "unreachable" }, "error");
|
|
55
60
|
return;
|
|
56
61
|
}
|
|
57
|
-
this.available = availableUpdate(this.
|
|
62
|
+
this.available = availableUpdate(this.installedVersion(), latest);
|
|
63
|
+
this.maybeAutoInstall();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The older of the running copy and the global install. A web app can run from a
|
|
67
|
+
* host's cache copy that is already current while the global install stays behind.
|
|
68
|
+
*/
|
|
69
|
+
installedVersion() {
|
|
70
|
+
const global = this.deps.globalVersion();
|
|
71
|
+
return global !== null && compareVersions(global, this.deps.version) === -1
|
|
72
|
+
? global
|
|
73
|
+
: this.deps.version;
|
|
74
|
+
}
|
|
75
|
+
/** Start the install the Update button starts, once a check found a release. */
|
|
76
|
+
maybeAutoInstall() {
|
|
77
|
+
if (this.state === "installing" || this.state === "restarting")
|
|
78
|
+
return;
|
|
79
|
+
if (!this.available)
|
|
80
|
+
return;
|
|
81
|
+
if (this.deps.now() < this.autoRetryAt)
|
|
82
|
+
return;
|
|
83
|
+
// A check can finish while a Stop or Restart runs; the manual route has the same gate.
|
|
84
|
+
if (this.deps.autoBlocked?.())
|
|
85
|
+
return;
|
|
86
|
+
if (!this.canInstall())
|
|
87
|
+
return;
|
|
88
|
+
if (this.requestInstall() !== "accepted")
|
|
89
|
+
return;
|
|
90
|
+
this.autoAttempt = true;
|
|
91
|
+
this.deps.log("Web app auto-update started", { from: this.deps.version, to: this.available });
|
|
58
92
|
}
|
|
59
93
|
status() {
|
|
60
94
|
// An open page polls the status, so a release shows within about a minute.
|
|
@@ -78,6 +112,7 @@ export class WebUpdate {
|
|
|
78
112
|
return "no-update";
|
|
79
113
|
if (!this.canInstall())
|
|
80
114
|
return "cannot-install";
|
|
115
|
+
this.autoAttempt = false;
|
|
81
116
|
this.state = "installing";
|
|
82
117
|
this.code = null;
|
|
83
118
|
void this.install(this.available);
|
|
@@ -96,6 +131,9 @@ export class WebUpdate {
|
|
|
96
131
|
const fail = (code, exitCode) => {
|
|
97
132
|
this.state = "failed";
|
|
98
133
|
this.code = code;
|
|
134
|
+
// Only an automatic start waits before trying again; the button always works.
|
|
135
|
+
if (this.autoAttempt)
|
|
136
|
+
this.autoRetryAt = this.deps.now() + AUTO_RETRY_AFTER_MS;
|
|
99
137
|
record("failed", code, exitCode);
|
|
100
138
|
};
|
|
101
139
|
const { code, exitCode } = await runGlobalInstall(this.deps, target);
|