visualproof 0.3.0 → 0.4.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/README.md +52 -11
- package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
- package/dist/assets/tips/phone-fingers.light.webp +0 -0
- package/dist/assets/tips/tour-agent.webp +0 -0
- package/dist/assets/tips/tour-arrange.webp +0 -0
- package/dist/assets/tips/tour-artwork.webp +0 -0
- package/dist/assets/tips/tour-checks.webp +0 -0
- package/dist/assets/tips/tour-export.webp +0 -0
- package/dist/assets/tips/tour-keys.webp +0 -0
- package/dist/assets/tips/tour-library.webp +0 -0
- package/dist/assets/tips/tour-project.webp +0 -0
- package/dist/assets/tips/tour-review.webp +0 -0
- package/dist/assets/tips/tour-share.webp +0 -0
- package/dist/assets/tips/tour-source.webp +0 -0
- package/dist/assets/tips/tour-target.webp +0 -0
- package/dist/assets/tips/tour-view.webp +0 -0
- package/dist/assets/tips/tour.json +289 -0
- package/dist/assets/tips/tour.webp +0 -0
- package/dist/mcp/visualproof-mcp.mjs +3 -1
- package/dist/server/agent.js +10 -5
- package/dist/server/children.js +85 -0
- package/dist/server/doctor.js +105 -42
- package/dist/server/events.js +10 -0
- package/dist/server/export.js +11 -4
- package/dist/server/extract.js +3 -2
- package/dist/server/figures.js +3 -2
- package/dist/server/film.js +380 -0
- package/dist/server/history.js +11 -1
- package/dist/server/hover.js +8 -5
- package/dist/server/live.js +66 -9
- package/dist/server/main.js +217 -16
- package/dist/server/media.js +114 -0
- package/dist/server/pipeline.js +6 -1
- package/dist/server/poppler.js +3 -2
- package/dist/server/readonlygate.js +4 -4
- package/dist/server/sourcestore.js +3 -2
- package/dist/server/status.js +3 -2
- package/dist/server/supervise.js +219 -0
- package/dist/server/updates.js +951 -0
- package/dist/server/usage.js +3 -1
- package/dist/server/version.js +183 -0
- package/dist/shared/timeline.js +5 -0
- package/dist/web/bundle/app.js +2298 -254
- package/dist/web/index.html +13 -1
- package/dist/web/src/canvas.js +22 -0
- package/dist/web/style.css +189 -1
- package/docs/agent-api/export.md +2 -2
- package/docs/agent-api/history.md +3 -2
- package/package.json +3 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The supervisor (UpdateSpec decision 5): `visualproof serve` from an installed package runs the
|
|
2
|
+
// server as a child of this small process, which stays while the server restarts into another
|
|
3
|
+
// version. It runs the newest of the installed package and the patches of its minor downloaded
|
|
4
|
+
// into the cache (server/updates.ts, codeToRun), passes signals on, and ends with the server's
|
|
5
|
+
// status — except where the server asks to be started again (status 75, a restart into another
|
|
6
|
+
// version) or where a version downloaded fails as it starts: that one is set aside, and the one
|
|
7
|
+
// before it started again (decision 7).
|
|
8
|
+
//
|
|
9
|
+
// The child is told, by its environment, that it runs supervised (VISUALPROOF_SUPERVISED), where the
|
|
10
|
+
// installed package is (VISUALPROOF_INSTALLED: the one it watches for an `npm install`), and what was
|
|
11
|
+
// set aside before it started (VISUALPROOF_SET_ASIDE), which it records in the timeline. It says over
|
|
12
|
+
// IPC when it listens (`{vp: "listening", version}`), so that a version which stops before then is
|
|
13
|
+
// known to have failed, and when what ends it is not its code (`{vp: "environment"}`: the site is
|
|
14
|
+
// busy, the port taken).
|
|
15
|
+
import { spawn } from "node:child_process";
|
|
16
|
+
import { constants as osConstants } from "node:os";
|
|
17
|
+
import { codeToRun, Versions } from "./updates.js";
|
|
18
|
+
/** the status a server ends with to be started again, in another version */
|
|
19
|
+
export const RESTART_STATUS = 75;
|
|
20
|
+
/** a version downloaded that ends within this long of starting is set aside */
|
|
21
|
+
export const FAILS_WITHIN = 10 * 60_000;
|
|
22
|
+
/** more starts than this in RESTART_WINDOW, and the supervisor gives up */
|
|
23
|
+
const MAX_STARTS = 6;
|
|
24
|
+
const RESTART_WINDOW = 10 * 60_000;
|
|
25
|
+
const SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
26
|
+
const defaultRun = (main, argv, env) => spawn(process.execPath, [main, ...argv], { stdio: ["inherit", "inherit", "inherit", "ipc"], env });
|
|
27
|
+
/** The status a process ended by `signal` is said to end with, as shells say it: 128 + its number. */
|
|
28
|
+
export function signalStatus(signal) {
|
|
29
|
+
return 128 + (osConstants.signals[signal] ?? 0);
|
|
30
|
+
}
|
|
31
|
+
/** How a version's failure counts (UpdateSpec decision 7): a SIGKILL never (the kernel's when
|
|
32
|
+
* memory runs out, an operator's kill -9); a SIGABRT — V8's when the heap reaches its limit — only
|
|
33
|
+
* before the version served: a version that runs out of memory as it starts, where the one before
|
|
34
|
+
* it did not, is the version's fault; one that runs out once it serves may be a reader's request.
|
|
35
|
+
* Either way it is started again (a version killed at every start ends in the too-many-starts stop
|
|
36
|
+
* below, which pauses it). */
|
|
37
|
+
function resource(signal, listened) {
|
|
38
|
+
return signal === "SIGKILL" || (signal === "SIGABRT" && listened);
|
|
39
|
+
}
|
|
40
|
+
/** failures soon after it served are counted for a day after the last one */
|
|
41
|
+
const STRIKES_FOR = 24 * 3600_000;
|
|
42
|
+
/** Run the server until it ends for good; the status it ended with — 0 once a stop was asked,
|
|
43
|
+
* whatever the server does meanwhile. A version from the cache is set aside (UpdateSpec decision 7)
|
|
44
|
+
* where it fails before it serves (for every site: it is about the version), or twice soon after it
|
|
45
|
+
* served (for this site: it may be about its project; a second site's sets it aside for all);
|
|
46
|
+
* where the version started after one was set aside fails as soon, the fault was not the first
|
|
47
|
+
* one's: it is taken back (what was recorded kept), this site's patches up to it are paused — it
|
|
48
|
+
* runs the installed package until a newer patch is out — and the supervisor ends. So does it
|
|
49
|
+
* after too many starts, pausing the patch it was starting. */
|
|
50
|
+
export function supervise(o) {
|
|
51
|
+
const log = o.log ?? ((l) => console.error(`visualproof: ${l}`));
|
|
52
|
+
const run = o.run ?? defaultRun;
|
|
53
|
+
const now = o.now ?? Date.now;
|
|
54
|
+
const site = o.site ?? null;
|
|
55
|
+
const versions = new Versions(o.cacheRoot);
|
|
56
|
+
const starts = [];
|
|
57
|
+
const backoff = o.backoff ?? ((n) => Math.min(300_000, 5000 * 2 ** (n - 1)));
|
|
58
|
+
/** kills in a row that do not count (out of memory): each start after one waits longer, and is
|
|
59
|
+
* not one of the starts that make the supervisor give up */
|
|
60
|
+
let kills = 0;
|
|
61
|
+
/** failures soon after it served, by version, and when the last one was */
|
|
62
|
+
const strikes = new Map();
|
|
63
|
+
/** versions set aside whose record could not be written (a full disk): set aside all the same */
|
|
64
|
+
const unrecorded = new Set();
|
|
65
|
+
let child = null;
|
|
66
|
+
let stopping = false;
|
|
67
|
+
/** the version set aside last, which the next child is told of */
|
|
68
|
+
let setAside = null;
|
|
69
|
+
const forward = (sig) => {
|
|
70
|
+
stopping = true;
|
|
71
|
+
child?.kill(sig);
|
|
72
|
+
};
|
|
73
|
+
for (const s of SIGNALS)
|
|
74
|
+
process.on(s, forward);
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
let ended = false;
|
|
77
|
+
const done = (status) => {
|
|
78
|
+
if (ended)
|
|
79
|
+
return;
|
|
80
|
+
ended = true;
|
|
81
|
+
for (const s of SIGNALS)
|
|
82
|
+
process.off(s, forward);
|
|
83
|
+
// a stop was asked: whatever the server did meanwhile (a restart, a signal before its own
|
|
84
|
+
// handlers), the stop is what happened
|
|
85
|
+
resolve(stopping ? 0 : status);
|
|
86
|
+
};
|
|
87
|
+
const pause = (v, why) => {
|
|
88
|
+
if (!site)
|
|
89
|
+
return;
|
|
90
|
+
if (versions.pause(site, v, why))
|
|
91
|
+
log(`this site's patches up to ${v} are not taken until a newer one is out (${why}): the installed package runs`);
|
|
92
|
+
else
|
|
93
|
+
log(`this site's patches could not be paused (${versions.root} cannot be written): ${why}`);
|
|
94
|
+
};
|
|
95
|
+
const start = (uncounted = false) => {
|
|
96
|
+
if (stopping)
|
|
97
|
+
return done(0);
|
|
98
|
+
const t = now();
|
|
99
|
+
if (!uncounted)
|
|
100
|
+
starts.push(t);
|
|
101
|
+
while (starts.length && t - starts[0] > RESTART_WINDOW)
|
|
102
|
+
starts.shift();
|
|
103
|
+
let code = codeToRun(o.installedRoot, versions, { mode: o.mode, site, also: unrecorded });
|
|
104
|
+
// marked before it is started, so that no prune removes it meanwhile; chosen again where one
|
|
105
|
+
// did just before the mark
|
|
106
|
+
if (code.cached) {
|
|
107
|
+
versions.markRunning(code.version, process.pid);
|
|
108
|
+
if (!versions.ready().includes(code.version)) {
|
|
109
|
+
versions.unmarkRunning(code.version, process.pid);
|
|
110
|
+
code = codeToRun(o.installedRoot, versions, { mode: o.mode, site, also: unrecorded });
|
|
111
|
+
if (code.cached)
|
|
112
|
+
versions.markRunning(code.version, process.pid);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (starts.length > MAX_STARTS) {
|
|
116
|
+
log(`the server was started ${starts.length} times in ten minutes: giving up`);
|
|
117
|
+
if (code.cached) {
|
|
118
|
+
versions.unmarkRunning(code.version, process.pid);
|
|
119
|
+
pause(code.version, `VisualProof ${code.version} was started ${starts.length} times in ten minutes`);
|
|
120
|
+
}
|
|
121
|
+
return done(1);
|
|
122
|
+
}
|
|
123
|
+
const env = { ...process.env, VISUALPROOF_SUPERVISED: "1", VISUALPROOF_INSTALLED: o.installedRoot };
|
|
124
|
+
// what it set aside without a record (a full disk): the server never restarts into it
|
|
125
|
+
if (unrecorded.size)
|
|
126
|
+
env.VISUALPROOF_UNRECORDED = JSON.stringify([...unrecorded]);
|
|
127
|
+
else
|
|
128
|
+
delete env.VISUALPROOF_UNRECORDED;
|
|
129
|
+
// the version set aside just before this one started: told to it (the timeline), and taken
|
|
130
|
+
// back if this one fails as soon
|
|
131
|
+
const after = setAside;
|
|
132
|
+
if (after)
|
|
133
|
+
env.VISUALPROOF_SET_ASIDE = JSON.stringify(after);
|
|
134
|
+
else
|
|
135
|
+
delete env.VISUALPROOF_SET_ASIDE;
|
|
136
|
+
setAside = null;
|
|
137
|
+
let listened = null;
|
|
138
|
+
// what ended it is not its code (the site busy, the port taken): never set aside for that
|
|
139
|
+
let environment = false;
|
|
140
|
+
const c = run(code.main, o.argv, env);
|
|
141
|
+
child = c;
|
|
142
|
+
// a version of the cache's, which no other server removes while this one runs it: marked as
|
|
143
|
+
// it starts, by its process
|
|
144
|
+
const pid = c.pid;
|
|
145
|
+
if (code.cached) {
|
|
146
|
+
if (pid)
|
|
147
|
+
versions.markRunning(code.version, pid);
|
|
148
|
+
versions.unmarkRunning(code.version, process.pid);
|
|
149
|
+
}
|
|
150
|
+
c.on("message", (m) => {
|
|
151
|
+
const said = m && typeof m === "object" ? m.vp : null;
|
|
152
|
+
if (said === "listening") {
|
|
153
|
+
listened = now();
|
|
154
|
+
kills = 0;
|
|
155
|
+
}
|
|
156
|
+
else if (said === "environment")
|
|
157
|
+
environment = true;
|
|
158
|
+
});
|
|
159
|
+
c.once("error", (e) => {
|
|
160
|
+
log(`the server could not be started: ${e.message}`);
|
|
161
|
+
done(1);
|
|
162
|
+
});
|
|
163
|
+
c.once("exit", (status, signal) => {
|
|
164
|
+
child = null;
|
|
165
|
+
if (code.cached && pid)
|
|
166
|
+
versions.unmarkRunning(code.version, pid);
|
|
167
|
+
const ends = status ?? (signal ? signalStatus(signal) : 1);
|
|
168
|
+
if (stopping)
|
|
169
|
+
return done(0);
|
|
170
|
+
if (status === RESTART_STATUS)
|
|
171
|
+
return start();
|
|
172
|
+
if (status === 0 || environment)
|
|
173
|
+
return done(ends);
|
|
174
|
+
const how = signal ? `was ended by ${signal}` : `exited with status ${status}`;
|
|
175
|
+
const soon = listened === null || now() - listened < FAILS_WITHIN;
|
|
176
|
+
const counts = !resource(signal, listened !== null);
|
|
177
|
+
if (after && soon && counts) {
|
|
178
|
+
// the version before it fails as soon: the fault was not the one set aside's
|
|
179
|
+
versions.unmarkBad(after.version);
|
|
180
|
+
unrecorded.delete(after.version);
|
|
181
|
+
log(`VisualProof ${code.version} ${how} as well: ${after.version} is taken back from those set aside (the fault is not its); stopping`);
|
|
182
|
+
pause(after.version, `${after.version}, and the version before it, failed as they started`);
|
|
183
|
+
return done(ends);
|
|
184
|
+
}
|
|
185
|
+
if (!code.cached || !soon)
|
|
186
|
+
return done(ends);
|
|
187
|
+
const aside = (why, where) => {
|
|
188
|
+
if (!versions.markBad(code.version, why, where)) {
|
|
189
|
+
unrecorded.add(code.version);
|
|
190
|
+
log(`VisualProof ${code.version} ${why}: set aside, though ${versions.root} could not record it (a full disk?)`);
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
log(`VisualProof ${code.version} ${why}: set aside${where ? " for this site" : ""}; starting the version before it`);
|
|
194
|
+
setAside = { version: code.version, why };
|
|
195
|
+
start();
|
|
196
|
+
};
|
|
197
|
+
if (!counts) {
|
|
198
|
+
kills++;
|
|
199
|
+
const wait = backoff(kills);
|
|
200
|
+
log(`VisualProof ${code.version} ${how}${listened === null ? " as it started" : " soon after it started"} (out of memory?): started again${wait ? ` in ${Math.round(wait / 1000)} s` : ""}`);
|
|
201
|
+
if (!wait)
|
|
202
|
+
return start(true);
|
|
203
|
+
setTimeout(() => start(true), wait);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (listened === null)
|
|
207
|
+
return aside(`${how} before it served`, null);
|
|
208
|
+
const was = strikes.get(code.version);
|
|
209
|
+
const n = was && now() - was.at < STRIKES_FOR ? was.n + 1 : 1;
|
|
210
|
+
strikes.set(code.version, { n, at: now() });
|
|
211
|
+
if (n >= 2)
|
|
212
|
+
return aside(`${how} twice soon after it started`, site ?? "");
|
|
213
|
+
log(`VisualProof ${code.version} ${how} soon after it started: started again`);
|
|
214
|
+
start();
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
start();
|
|
218
|
+
});
|
|
219
|
+
}
|