browser-broker 0.1.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/.env.example +173 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/RELEASES.md +97 -0
- package/dist/package.json +58 -0
- package/dist/src/adapter/conformance/case.js +1 -0
- package/dist/src/adapter/conformance/cases.js +429 -0
- package/dist/src/adapter/conformance/discovery.js +156 -0
- package/dist/src/adapter/conformance/driver.js +1 -0
- package/dist/src/adapter/conformance/drivers.js +36 -0
- package/dist/src/adapter/conformance/run.js +224 -0
- package/dist/src/adapter/conformance/service-subject.js +165 -0
- package/dist/src/adapter/contract.js +24 -0
- package/dist/src/adapter/operations.js +114 -0
- package/dist/src/adapter/service-seam.js +1 -0
- package/dist/src/artifacts/names.js +229 -0
- package/dist/src/artifacts/store.js +174 -0
- package/dist/src/bin/broker-tool.js +63 -0
- package/dist/src/bin/broker.js +111 -0
- package/dist/src/browser/adoption.js +143 -0
- package/dist/src/browser/automation-probe.js +113 -0
- package/dist/src/browser/conformance/case.js +1 -0
- package/dist/src/browser/conformance/cases.js +192 -0
- package/dist/src/browser/conformance/run.js +102 -0
- package/dist/src/browser/conformance/subjects.js +19 -0
- package/dist/src/browser/discovery.js +226 -0
- package/dist/src/browser/driver.js +195 -0
- package/dist/src/browser/fake.js +585 -0
- package/dist/src/browser/launch.js +504 -0
- package/dist/src/browser/real.js +1425 -0
- package/dist/src/browser/setup.js +161 -0
- package/dist/src/capture/accounting.js +59 -0
- package/dist/src/capture/image.js +112 -0
- package/dist/src/capture/ladder.js +72 -0
- package/dist/src/capture/legibility.js +195 -0
- package/dist/src/capture/pipeline.js +153 -0
- package/dist/src/capture/tiers.js +166 -0
- package/dist/src/cli/adapter.js +233 -0
- package/dist/src/cli/commands.js +270 -0
- package/dist/src/cli/conformance-driver.js +119 -0
- package/dist/src/cli/diffs.js +122 -0
- package/dist/src/cli/image.js +274 -0
- package/dist/src/cli/index.js +895 -0
- package/dist/src/cli/login-command.js +401 -0
- package/dist/src/cli/operations-commands.js +186 -0
- package/dist/src/cli/reconcile-command.js +137 -0
- package/dist/src/cli/sign-in.js +134 -0
- package/dist/src/cli/telemetry.js +222 -0
- package/dist/src/config/environment.js +446 -0
- package/dist/src/diff/artifact-path.js +77 -0
- package/dist/src/diff/crops.js +102 -0
- package/dist/src/diff/geometry.js +122 -0
- package/dist/src/diff/image.js +132 -0
- package/dist/src/diff/mask.js +46 -0
- package/dist/src/diff/regions.js +263 -0
- package/dist/src/diff/settings.js +135 -0
- package/dist/src/doctor/checks.js +588 -0
- package/dist/src/doctor/report.js +152 -0
- package/dist/src/doctor/session.js +161 -0
- package/dist/src/errors.js +36 -0
- package/dist/src/feedback/read.js +119 -0
- package/dist/src/feedback/record.js +199 -0
- package/dist/src/operations/addresses.js +175 -0
- package/dist/src/operations/derive.js +109 -0
- package/dist/src/operations/ledger.js +194 -0
- package/dist/src/operations/status.js +197 -0
- package/dist/src/operations/telemetry.js +280 -0
- package/dist/src/report/document.js +419 -0
- package/dist/src/report/escape.js +68 -0
- package/dist/src/report/snapshot.js +97 -0
- package/dist/src/service/arbitration.js +537 -0
- package/dist/src/service/artifacts.js +85 -0
- package/dist/src/service/bridge.js +577 -0
- package/dist/src/service/broker.js +120 -0
- package/dist/src/service/browser-session.js +269 -0
- package/dist/src/service/capacity.js +62 -0
- package/dist/src/service/capture-seam.js +83 -0
- package/dist/src/service/capture-store.js +91 -0
- package/dist/src/service/comparison-store.js +101 -0
- package/dist/src/service/comparison.js +173 -0
- package/dist/src/service/events.js +93 -0
- package/dist/src/service/keys.js +68 -0
- package/dist/src/service/leases.js +147 -0
- package/dist/src/service/nudge.js +66 -0
- package/dist/src/service/operations/claim.js +692 -0
- package/dist/src/service/operations/give-back.js +131 -0
- package/dist/src/service/operations/pages.js +771 -0
- package/dist/src/service/operations/sign-in.js +915 -0
- package/dist/src/service/operations/status.js +62 -0
- package/dist/src/service/ownership.js +93 -0
- package/dist/src/service/pages.js +616 -0
- package/dist/src/service/pending-seeds.js +20 -0
- package/dist/src/service/queue.js +233 -0
- package/dist/src/service/reconcile.js +220 -0
- package/dist/src/service/refusals.js +262 -0
- package/dist/src/service/runtime.js +131 -0
- package/dist/src/service/signin-recovery.js +148 -0
- package/dist/src/service/storage-seed.js +239 -0
- package/dist/src/service/tabs.js +123 -0
- package/dist/src/store/budget.js +99 -0
- package/dist/src/store/location.js +42 -0
- package/dist/src/store/network-path.js +182 -0
- package/dist/src/store/network-volume.js +92 -0
- package/dist/src/store/open.js +226 -0
- package/dist/src/store/schema/step-001-initial.js +523 -0
- package/dist/src/store/schema/step-002-tab-budget.js +53 -0
- package/dist/src/store/schema/step-003-queue-order.js +110 -0
- package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
- package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
- package/dist/src/store/schema/step-006-signin-events.js +104 -0
- package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
- package/dist/src/store/schema/step-008-signin-owner.js +76 -0
- package/dist/src/store/schema/step-009-named-browsers.js +138 -0
- package/dist/src/store/schema/step-010-signin-request.js +135 -0
- package/dist/src/store/schema/step.js +172 -0
- package/dist/src/store/schema/steps.js +58 -0
- package/dist/src/store/transaction.js +37 -0
- package/dist/src/tool/adapter.js +90 -0
- package/dist/src/tool/conformance-driver.js +184 -0
- package/dist/src/tool/protocol.js +310 -0
- package/dist/src/tool/session.js +351 -0
- package/dist/src/tool/tools.js +310 -0
- package/package.json +58 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { BrokerError } from "../errors.js";
|
|
4
|
+
import { SIGN_IN_OWNER_UNKNOWN_REMEDY } from "../service/signin-recovery.js";
|
|
5
|
+
import { refuseNetworkLocation } from "../store/network-path.js";
|
|
6
|
+
import { EXPECTED_VERSION } from "../store/schema/steps.js";
|
|
7
|
+
/**
|
|
8
|
+
* Exit codes, one per failing precondition group (`SCHEMA.md` §5.5: "Exits
|
|
9
|
+
* with a distinct code on any failure").
|
|
10
|
+
*
|
|
11
|
+
* **Distinct rather than sequential**, and the grouping is the substance: a
|
|
12
|
+
* caller using this as a readiness check branches on *what* is wrong, and the
|
|
13
|
+
* useful distinctions are the ones that call for different responses — a
|
|
14
|
+
* store on a network filesystem needs configuration changed, a browser that
|
|
15
|
+
* does not answer needs a browser started, and a budget disagreement needs
|
|
16
|
+
* an environment reconciled.
|
|
17
|
+
*
|
|
18
|
+
* Zero is every precondition either passing or being unevaluable. **An
|
|
19
|
+
* `unknown` does not fail the command**, for the reason given on
|
|
20
|
+
* {@link CheckStatus}: a fresh install with no browser launched is not a
|
|
21
|
+
* broken install, and a readiness check that refused one would be unusable
|
|
22
|
+
* on the machine it is most needed on.
|
|
23
|
+
*
|
|
24
|
+
* When several groups fail, the **lowest** code is reported, so the exit code
|
|
25
|
+
* is stable rather than depending on check order — and the full report on the
|
|
26
|
+
* output stream is where the rest of them are. A single number cannot carry
|
|
27
|
+
* more than one failure and pretending otherwise is how a code becomes
|
|
28
|
+
* meaningless.
|
|
29
|
+
*/
|
|
30
|
+
export const DOCTOR_EXIT = {
|
|
31
|
+
ok: 0,
|
|
32
|
+
/** The store: where it is, what it is on, what version it is at. */
|
|
33
|
+
store: 10,
|
|
34
|
+
/** The automation tool is absent or unusable. */
|
|
35
|
+
automation: 11,
|
|
36
|
+
/** An artifact or profile root is not writable. */
|
|
37
|
+
roots: 12,
|
|
38
|
+
/** A browser's discovery record does not check out. */
|
|
39
|
+
browsers: 13,
|
|
40
|
+
/** The capture-surface check. */
|
|
41
|
+
capture: 14,
|
|
42
|
+
/** A keeper tab is missing. */
|
|
43
|
+
keeper: 15,
|
|
44
|
+
/** The stored tab budget disagrees with this process's environment. */
|
|
45
|
+
budget: 16,
|
|
46
|
+
/**
|
|
47
|
+
* The sign-in state of the persistent profile.
|
|
48
|
+
*
|
|
49
|
+
* It has a group so the report can carry it, and it is **the one group no
|
|
50
|
+
* check ever fails into**: a profile without a session is the ordinary
|
|
51
|
+
* state of a fresh install rather than a fault. The code exists so the
|
|
52
|
+
* grouping is total rather than because anything returns it.
|
|
53
|
+
*/
|
|
54
|
+
session: 17,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* The exit code for a set of results.
|
|
58
|
+
*
|
|
59
|
+
* Lowest failing group wins, per the note on {@link DOCTOR_EXIT}. Ordering by
|
|
60
|
+
* the code's numeric value rather than by the order checks happened to run in
|
|
61
|
+
* is what makes it reproducible.
|
|
62
|
+
*/
|
|
63
|
+
export function exitCodeFor(results) {
|
|
64
|
+
const failed = results
|
|
65
|
+
.filter((result) => result.status === 'failed')
|
|
66
|
+
.map((result) => DOCTOR_EXIT[result.group]);
|
|
67
|
+
return failed.length === 0 ? DOCTOR_EXIT.ok : Math.min(...failed);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Where the store is, and that it is not somewhere it may not be (§1.0).
|
|
71
|
+
*
|
|
72
|
+
* **It calls the same refusal the store open calls**, rather than
|
|
73
|
+
* reimplementing the test. That is the whole reason this check is worth
|
|
74
|
+
* anything: a doctor with its own opinion about what a network path is would
|
|
75
|
+
* eventually disagree with the thing that actually refuses to start, and the
|
|
76
|
+
* disagreement would be discovered by somebody whose doctor said the
|
|
77
|
+
* installation was fine and whose service would not run.
|
|
78
|
+
*
|
|
79
|
+
* The refusal throws rather than returning, so it is caught and turned into a
|
|
80
|
+
* report line — reporting rather than refusing is this command's whole
|
|
81
|
+
* contract.
|
|
82
|
+
*/
|
|
83
|
+
export function checkStoreLocation(environment, checks) {
|
|
84
|
+
const id = 'store.not_on_network_filesystem';
|
|
85
|
+
const title = 'The store is not on a network filesystem';
|
|
86
|
+
try {
|
|
87
|
+
if (environment.configuredDatabasePath !== undefined) {
|
|
88
|
+
refuseNetworkLocation(environment.configuredDatabasePath, checks);
|
|
89
|
+
}
|
|
90
|
+
refuseNetworkLocation(environment.databasePath, checks);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
if (error instanceof BrokerError) {
|
|
94
|
+
return {
|
|
95
|
+
group: 'store',
|
|
96
|
+
id,
|
|
97
|
+
title,
|
|
98
|
+
status: 'failed',
|
|
99
|
+
// The refusal's own message, which names the variable and what to set
|
|
100
|
+
// it to — and which deliberately does not echo the path back. The
|
|
101
|
+
// path is a real location on a real machine and this report is a
|
|
102
|
+
// thing people paste into messages.
|
|
103
|
+
detail: error.message,
|
|
104
|
+
remedy: 'Point BROKER_DB at a local filesystem. Several processes arbitrate against this store at once, and the write-ahead log requires every one of them to be on the same host.',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
group: 'store',
|
|
111
|
+
id,
|
|
112
|
+
title,
|
|
113
|
+
status: 'ok',
|
|
114
|
+
detail: 'The configured store location is local.',
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** That the store file exists and can be read and written. */
|
|
118
|
+
export function checkStorePresent(environment) {
|
|
119
|
+
const location = environment.databasePath;
|
|
120
|
+
if (!fs.existsSync(location)) {
|
|
121
|
+
return {
|
|
122
|
+
group: 'store',
|
|
123
|
+
id: 'store.present',
|
|
124
|
+
title: 'The store exists',
|
|
125
|
+
status: 'unknown',
|
|
126
|
+
// Not a failure: the store is created on first spawn, so its absence
|
|
127
|
+
// means nothing has run yet rather than that something is broken.
|
|
128
|
+
detail: 'No store file at the configured location. It is created on the first spawn.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
fs.accessSync(location, fs.constants.R_OK | fs.constants.W_OK);
|
|
133
|
+
return {
|
|
134
|
+
group: 'store',
|
|
135
|
+
id: 'store.present',
|
|
136
|
+
title: 'The store exists',
|
|
137
|
+
status: 'ok',
|
|
138
|
+
detail: 'The store file is present and both readable and writable.',
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {
|
|
143
|
+
group: 'store',
|
|
144
|
+
id: 'store.present',
|
|
145
|
+
title: 'The store exists',
|
|
146
|
+
status: 'failed',
|
|
147
|
+
detail: 'The store file is present but cannot be both read and written.',
|
|
148
|
+
remedy: 'Check the file’s ownership and permissions for the account running this command.',
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* That the store is at the version this build expects.
|
|
154
|
+
*
|
|
155
|
+
* Reports and does not step. A build ahead of its store is the ordinary
|
|
156
|
+
* upgrade case and is fixed by the next spawn; a **store ahead of the build**
|
|
157
|
+
* is the dangerous direction, because the running code does not know what the
|
|
158
|
+
* newer schema means, so the two are distinguished in the message.
|
|
159
|
+
*/
|
|
160
|
+
export function checkSchemaVersion(found) {
|
|
161
|
+
if (found === null) {
|
|
162
|
+
return {
|
|
163
|
+
group: 'store',
|
|
164
|
+
id: 'store.version',
|
|
165
|
+
title: 'The store is at the version this build expects',
|
|
166
|
+
status: 'unknown',
|
|
167
|
+
detail: `No store to read a version from. This build expects version ${String(EXPECTED_VERSION)}.`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (found === EXPECTED_VERSION) {
|
|
171
|
+
return {
|
|
172
|
+
group: 'store',
|
|
173
|
+
id: 'store.version',
|
|
174
|
+
title: 'The store is at the version this build expects',
|
|
175
|
+
status: 'ok',
|
|
176
|
+
detail: `Version ${String(found)}.`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const ahead = found > EXPECTED_VERSION;
|
|
180
|
+
return {
|
|
181
|
+
group: 'store',
|
|
182
|
+
id: 'store.version',
|
|
183
|
+
title: 'The store is at the version this build expects',
|
|
184
|
+
status: 'failed',
|
|
185
|
+
detail: ahead
|
|
186
|
+
? `The store is at version ${String(found)} and this build expects ${String(EXPECTED_VERSION)}. The store was written by a newer build.`
|
|
187
|
+
: `The store is at version ${String(found)} and this build expects ${String(EXPECTED_VERSION)}.`,
|
|
188
|
+
remedy: ahead
|
|
189
|
+
? 'Run the newer build against this store, or point BROKER_DB at a store this build understands. This build cannot safely read a schema it does not know.'
|
|
190
|
+
: 'Any spawn steps the schema. Run the service once; this command reports and does not step.',
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function checkAutomation(probe) {
|
|
194
|
+
if (probe.present === undefined) {
|
|
195
|
+
return {
|
|
196
|
+
group: 'automation',
|
|
197
|
+
id: 'automation.present',
|
|
198
|
+
title: 'The automation tool is present',
|
|
199
|
+
status: 'unknown',
|
|
200
|
+
detail: probe.detail ??
|
|
201
|
+
'No automation probe was supplied, so there is nothing to report a version for.',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (!probe.present) {
|
|
205
|
+
return {
|
|
206
|
+
group: 'automation',
|
|
207
|
+
id: 'automation.present',
|
|
208
|
+
title: 'The automation tool is present',
|
|
209
|
+
status: 'failed',
|
|
210
|
+
detail: probe.detail ?? 'No automation tool could be found for this process to use.',
|
|
211
|
+
remedy: 'Install a browser binary for the automation driver this build depends on — see the README’s install section.',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
group: 'automation',
|
|
216
|
+
id: 'automation.present',
|
|
217
|
+
title: 'The automation tool is present',
|
|
218
|
+
status: 'ok',
|
|
219
|
+
detail: `Present${probe.version === undefined ? '' : `, version ${probe.version}`}.`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* That a root is writable, proved by writing.
|
|
224
|
+
*
|
|
225
|
+
* `fs.access` with the write bit answers a different question on more than
|
|
226
|
+
* one platform — it consults permission bits that a filesystem, a container
|
|
227
|
+
* mount or an access-control list can override, so it reports writable for
|
|
228
|
+
* directories that refuse the write. The only answer that is not a guess is
|
|
229
|
+
* the write itself.
|
|
230
|
+
*
|
|
231
|
+
* **What it wrote is removed.** This is the one place the doctor touches
|
|
232
|
+
* anything outside the store, and it is named in this module's header for
|
|
233
|
+
* that reason. It does **not** create the directory: creating it is the setup
|
|
234
|
+
* handshake's job, and a doctor that created what it was asked to check would
|
|
235
|
+
* report on a state it had just produced.
|
|
236
|
+
*/
|
|
237
|
+
export function checkRootWritable(id, title, root) {
|
|
238
|
+
if (!fs.existsSync(root)) {
|
|
239
|
+
return {
|
|
240
|
+
group: 'roots',
|
|
241
|
+
id,
|
|
242
|
+
title,
|
|
243
|
+
status: 'unknown',
|
|
244
|
+
detail: 'The directory does not exist. Every spawn creates it; this command does not.',
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const probe = path.join(root, `.broker-doctor-${String(process.pid)}-${String(Date.now())}`);
|
|
248
|
+
try {
|
|
249
|
+
fs.writeFileSync(probe, '');
|
|
250
|
+
return { group: 'roots', id, title, status: 'ok', detail: 'The directory accepted a write.' };
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return {
|
|
254
|
+
group: 'roots',
|
|
255
|
+
id,
|
|
256
|
+
title,
|
|
257
|
+
status: 'failed',
|
|
258
|
+
detail: 'The directory exists but refused a write.',
|
|
259
|
+
remedy: 'Check the directory’s ownership and permissions for the account running this command.',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
// Best effort: a probe left behind is untidy and harmless, and throwing
|
|
264
|
+
// out of a cleanup would turn a passing check into a crash.
|
|
265
|
+
try {
|
|
266
|
+
fs.rmSync(probe, { force: true });
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
/* the probe outlives us; the check itself already answered */
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function checkDiscoveryRecord(browser, probe) {
|
|
274
|
+
const id = `browser.${browser}.discovery`;
|
|
275
|
+
const title = `The ${browser} browser’s discovery record checks out`;
|
|
276
|
+
if (!probe.recorded) {
|
|
277
|
+
return {
|
|
278
|
+
group: 'browsers',
|
|
279
|
+
id,
|
|
280
|
+
title,
|
|
281
|
+
status: 'unknown',
|
|
282
|
+
// Not a failure: no record means no browser has been launched, which is
|
|
283
|
+
// the state of every fresh install.
|
|
284
|
+
detail: 'No discovery record. This browser has not been launched.',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (probe.answered !== true) {
|
|
288
|
+
return {
|
|
289
|
+
group: 'browsers',
|
|
290
|
+
id,
|
|
291
|
+
title,
|
|
292
|
+
status: 'failed',
|
|
293
|
+
detail: 'A discovery record is present but the endpoint it names did not answer.',
|
|
294
|
+
remedy: 'The record survives the browser it describes, so a stale one is expected after a browser exits. The next caller will take the launch race and start one.',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
if (probe.expectedUuid === undefined || probe.reportedUuid === undefined) {
|
|
298
|
+
return {
|
|
299
|
+
group: 'browsers',
|
|
300
|
+
id,
|
|
301
|
+
title,
|
|
302
|
+
status: 'failed',
|
|
303
|
+
// Answering is not enough, and this is the case that would silently
|
|
304
|
+
// pass a port-only check.
|
|
305
|
+
detail: 'The endpoint answered but the browser’s own identifier could not be compared, so what answered cannot be shown to be the expected browser.',
|
|
306
|
+
remedy: 'Treat this browser as not running. Attaching to something that cannot be identified is worse than failing to attach, because it succeeds.',
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (probe.expectedUuid !== probe.reportedUuid) {
|
|
310
|
+
return {
|
|
311
|
+
group: 'browsers',
|
|
312
|
+
id,
|
|
313
|
+
title,
|
|
314
|
+
status: 'failed',
|
|
315
|
+
detail: 'The endpoint answered, but as a different browser than the record names — the port has been reused.',
|
|
316
|
+
remedy: 'Treat this browser as not running. The next caller will take the launch race and start one against the recorded profile.',
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
group: 'browsers',
|
|
321
|
+
id,
|
|
322
|
+
title,
|
|
323
|
+
status: 'ok',
|
|
324
|
+
detail: 'The endpoint answered and identified itself as the browser the record names.',
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The capture-surface check.
|
|
329
|
+
*
|
|
330
|
+
* The setting exists and the check that enforces it belongs to the capture
|
|
331
|
+
* pipeline's row. Reported here from the first version so the report has the
|
|
332
|
+
* entry §5.5 lists, and reported as `unknown` rather than invented, because a
|
|
333
|
+
* check that always passes is worse than one that says it has nothing to
|
|
334
|
+
* check.
|
|
335
|
+
*/
|
|
336
|
+
export function checkCaptureSurface(configured) {
|
|
337
|
+
if (configured === undefined) {
|
|
338
|
+
return {
|
|
339
|
+
group: 'capture',
|
|
340
|
+
id: 'capture.surface',
|
|
341
|
+
title: 'The capture surface is configured',
|
|
342
|
+
status: 'unknown',
|
|
343
|
+
detail: 'This build does not read a capture-surface setting yet, so there is nothing to check it against.',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
group: 'capture',
|
|
348
|
+
id: 'capture.surface',
|
|
349
|
+
title: 'The capture surface is configured',
|
|
350
|
+
status: 'ok',
|
|
351
|
+
detail: `Configured as ${configured}.`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* The keeper tab (§3.15).
|
|
356
|
+
*
|
|
357
|
+
* **One blank page per browser that is never leased, never addressable and
|
|
358
|
+
* never counted against the budget** — and it is a correctness mechanism, not
|
|
359
|
+
* tidiness: a headed browser dies within about half a second of its last tab
|
|
360
|
+
* closing, so without it the ordinary release path destroys the shared
|
|
361
|
+
* authenticated session.
|
|
362
|
+
*
|
|
363
|
+
* §3.15 puts this check on `broker doctor` explicitly, "which checks it is
|
|
364
|
+
* present before either browser is allowed to serve". Answering needs a live
|
|
365
|
+
* browser to ask, so a browser that is not running reports `unknown` rather
|
|
366
|
+
* than failing.
|
|
367
|
+
*/
|
|
368
|
+
export function checkKeeperTab(browser, present) {
|
|
369
|
+
const id = `browser.${browser}.keeper_tab`;
|
|
370
|
+
const title = `The ${browser} browser has its keeper tab`;
|
|
371
|
+
if (present === undefined) {
|
|
372
|
+
return {
|
|
373
|
+
group: 'keeper',
|
|
374
|
+
id,
|
|
375
|
+
title,
|
|
376
|
+
status: 'unknown',
|
|
377
|
+
detail: 'This browser is not reachable, so there is nothing to ask about a keeper tab.',
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return present
|
|
381
|
+
? { group: 'keeper', id, title, status: 'ok', detail: 'The keeper tab is present.' }
|
|
382
|
+
: {
|
|
383
|
+
group: 'keeper',
|
|
384
|
+
id,
|
|
385
|
+
title,
|
|
386
|
+
status: 'failed',
|
|
387
|
+
detail: 'No keeper tab. The last lease to be released would close the final tab.',
|
|
388
|
+
remedy: 'A spawn establishes the keeper tab. This command reports and does not create one — run the service against this browser.',
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* **Whether the stored tab budget agrees with this process's environment**
|
|
393
|
+
* (§5.5, §1.10).
|
|
394
|
+
*
|
|
395
|
+
* This is the precondition §1.10 argues for at length, and it is worth
|
|
396
|
+
* restating because it is the one whose failure is invisible without it:
|
|
397
|
+
* several processes arbitrate against the budget at once, so in one process's
|
|
398
|
+
* environment it can be fifteen and in another's thirty. **Each admits
|
|
399
|
+
* callers against its own belief, each is internally consistent, and the
|
|
400
|
+
* ceiling silently stops being a ceiling.** Nothing reports it. The count is
|
|
401
|
+
* correct in every process and the machine is over budget anyway.
|
|
402
|
+
*
|
|
403
|
+
* So a disagreement is a **failure**, not a warning — and neither number is
|
|
404
|
+
* adopted. A process running against a bound it was not configured for is a
|
|
405
|
+
* configuration error somebody needs to see, and overwriting the stored value
|
|
406
|
+
* would let whichever process started most recently move a bound the others
|
|
407
|
+
* are mid-arbitration against.
|
|
408
|
+
*/
|
|
409
|
+
export function checkTabBudget(stored, configured) {
|
|
410
|
+
const id = 'config.tab_budget_agrees';
|
|
411
|
+
const title = 'The stored tab budget agrees with this process’s environment';
|
|
412
|
+
if (stored === null) {
|
|
413
|
+
return {
|
|
414
|
+
group: 'budget',
|
|
415
|
+
id,
|
|
416
|
+
title,
|
|
417
|
+
status: 'unknown',
|
|
418
|
+
detail: 'No budget has been recorded in this store. The first process to open it records the value it believes.',
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
if (configured === null) {
|
|
422
|
+
return {
|
|
423
|
+
group: 'budget',
|
|
424
|
+
id,
|
|
425
|
+
title,
|
|
426
|
+
status: 'unknown',
|
|
427
|
+
detail: `The store records ${String(stored)}, and this build does not read a tab-budget variable yet.`,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
if (stored !== configured) {
|
|
431
|
+
return {
|
|
432
|
+
group: 'budget',
|
|
433
|
+
id,
|
|
434
|
+
title,
|
|
435
|
+
status: 'failed',
|
|
436
|
+
detail: `The store records ${String(stored)} and this process’s environment says ${String(configured)}.`,
|
|
437
|
+
remedy: 'Reconcile the environment with the stored value. Two processes arbitrating against different bounds means the ceiling is not a ceiling, and neither value is adopted automatically.',
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
group: 'budget',
|
|
442
|
+
id,
|
|
443
|
+
title,
|
|
444
|
+
status: 'ok',
|
|
445
|
+
detail: `Both say ${String(stored)}.`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* **Whether the signed-in browser's profile looks signed in** (§5.5.1).
|
|
450
|
+
*
|
|
451
|
+
* ── Why this check exists, and why it never fails ───────────────────────
|
|
452
|
+
*
|
|
453
|
+
* Signing in is the one step a person performs by hand, and until this check
|
|
454
|
+
* existed there was **no way to confirm it worked** short of opening a
|
|
455
|
+
* browser and looking — which is the one thing this command may not do.
|
|
456
|
+
*
|
|
457
|
+
* **It reports and never fails**, which is a deliberate choice rather than an
|
|
458
|
+
* omission. A profile with no session in it is not a broken installation: it
|
|
459
|
+
* is every installation, up until the moment somebody signs in. Failing here
|
|
460
|
+
* would make `broker doctor` exit non-zero on a correct fresh install, and a
|
|
461
|
+
* readiness check that cannot go green on a working machine is one people
|
|
462
|
+
* learn to ignore. What it does instead is **say which of the two states it
|
|
463
|
+
* found**, which is the question being asked.
|
|
464
|
+
*
|
|
465
|
+
* ── Why a zero count is not reported as "not signed in" ─────────────────
|
|
466
|
+
*
|
|
467
|
+
* `session.ts` carries the measurements; the part that matters here is that
|
|
468
|
+
* the evidence is **positive-only**. Stored cookies mean a session was
|
|
469
|
+
* written down. No stored cookies mean no evidence was found — which is also
|
|
470
|
+
* what a site keeping its session in local storage looks like, and what a
|
|
471
|
+
* profile whose browser has not flushed yet looks like. So those come back as
|
|
472
|
+
* `unknown` **with the reason**, never as a confident negative.
|
|
473
|
+
*/
|
|
474
|
+
/**
|
|
475
|
+
* **Whether a sign-in has been abandoned** (§5.5.1).
|
|
476
|
+
*
|
|
477
|
+
* ── Why this is the one sign-in check that can fail ─────────────────────
|
|
478
|
+
*
|
|
479
|
+
* Its neighbour {@link checkSignInSession} never fails, because a profile with
|
|
480
|
+
* no session in it is every installation up until somebody signs in. This one
|
|
481
|
+
* is the opposite kind of fact: a browser stuck in `signing-in` with **no
|
|
482
|
+
* process holding it** is not a state any working installation is in. It
|
|
483
|
+
* refuses every caller, and before it was recoverable it did so permanently.
|
|
484
|
+
* So it is a genuine failure and is reported as one.
|
|
485
|
+
*
|
|
486
|
+
* ── The three answers, and why only one of them is a failure ────────────
|
|
487
|
+
*
|
|
488
|
+
* - **Owner gone** — failed. Nobody is signing in and every caller is being
|
|
489
|
+
* turned away. The remedy is the recovery path, named explicitly, because a
|
|
490
|
+
* check that reports a fault without saying what to do has moved the work
|
|
491
|
+
* rather than done it.
|
|
492
|
+
* - **Owner running** — `ok`. Somebody is signing in right now, which is the
|
|
493
|
+
* command working exactly as designed. Reported rather than silent, so a
|
|
494
|
+
* person who cannot understand why callers are being refused can see the
|
|
495
|
+
* reason on this report instead of guessing.
|
|
496
|
+
* - **Owner unknown** — `unknown`, never failed. A row written before the
|
|
497
|
+
* owner column existed records nobody, and calling that a fault would fail
|
|
498
|
+
* the report on an installation that may be perfectly healthy. The detail
|
|
499
|
+
* says what was found and what to do about it.
|
|
500
|
+
*/
|
|
501
|
+
export function checkAbandonedSignIn(browser, owner) {
|
|
502
|
+
const id = `browser.${browser}.signin_owner`;
|
|
503
|
+
const title = `No sign-in has been abandoned on the ${browser} browser`;
|
|
504
|
+
if (owner.kind === 'not-signing-in') {
|
|
505
|
+
return {
|
|
506
|
+
group: 'session',
|
|
507
|
+
id,
|
|
508
|
+
title,
|
|
509
|
+
status: 'ok',
|
|
510
|
+
detail: 'The browser is not being signed into, so there is nothing to have been abandoned.',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (owner.kind === 'owner-running') {
|
|
514
|
+
return {
|
|
515
|
+
group: 'session',
|
|
516
|
+
id,
|
|
517
|
+
title,
|
|
518
|
+
status: 'ok',
|
|
519
|
+
detail: `A sign-in is in progress and the process holding it (${String(owner.pid)}) is running. ` +
|
|
520
|
+
'Callers are being refused while it lasts; that is a pause rather than a fault.',
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
if (owner.kind === 'owner-unknown') {
|
|
524
|
+
return {
|
|
525
|
+
group: 'session',
|
|
526
|
+
id,
|
|
527
|
+
title,
|
|
528
|
+
status: 'unknown',
|
|
529
|
+
detail: 'The browser is recorded as being signed into, but this store does not say which process began it — ' +
|
|
530
|
+
'so whether anybody is still signing in cannot be determined from here.',
|
|
531
|
+
remedy: SIGN_IN_OWNER_UNKNOWN_REMEDY,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
group: 'session',
|
|
536
|
+
id,
|
|
537
|
+
title,
|
|
538
|
+
status: 'failed',
|
|
539
|
+
detail: `The browser is stuck being signed into: the process that began it (${String(owner.pid)}) has gone. ` +
|
|
540
|
+
'Every caller asking for this browser is being refused, and nothing will clear it on its own.',
|
|
541
|
+
remedy: 'Run `broker login` — it reclaims a sign-in whose process is gone and then hands you the window. ' +
|
|
542
|
+
'If you do not want to sign in, running it and closing the browser window it opens returns the browser to service.',
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
export function checkSignInSession(browser, probe) {
|
|
546
|
+
const id = `browser.${browser}.session`;
|
|
547
|
+
const title = `The ${browser} browser’s profile carries a sign-in`;
|
|
548
|
+
if (probe.evidence === 'session-present') {
|
|
549
|
+
return {
|
|
550
|
+
group: 'session',
|
|
551
|
+
id,
|
|
552
|
+
title,
|
|
553
|
+
status: 'ok',
|
|
554
|
+
detail: `The profile holds ${String(probe.cookieCount ?? 0)} stored cookie(s), so a session was established and written down.`,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (probe.evidence === 'no-profile') {
|
|
558
|
+
return {
|
|
559
|
+
group: 'session',
|
|
560
|
+
id,
|
|
561
|
+
title,
|
|
562
|
+
status: 'unknown',
|
|
563
|
+
detail: probe.reason ??
|
|
564
|
+
'There is no profile directory yet, so there is nothing to look for a session in.',
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
if (probe.evidence === 'undetermined') {
|
|
568
|
+
return {
|
|
569
|
+
group: 'session',
|
|
570
|
+
id,
|
|
571
|
+
title,
|
|
572
|
+
status: 'unknown',
|
|
573
|
+
detail: probe.reason ?? 'The profile could not be inspected, so nothing is concluded.',
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// `no-session-found`. Reported as unknown rather than failed, and the
|
|
577
|
+
// detail says exactly how strong the evidence is — the absence of a stored
|
|
578
|
+
// cookie is not proof that nobody is signed in.
|
|
579
|
+
return {
|
|
580
|
+
group: 'session',
|
|
581
|
+
id,
|
|
582
|
+
title,
|
|
583
|
+
status: 'unknown',
|
|
584
|
+
detail: probe.reason ??
|
|
585
|
+
'No stored cookies were found in the profile, which is what a profile nobody has signed into looks like.',
|
|
586
|
+
remedy: `Run \`broker login\` to sign in to the ${browser} browser by hand. It is the one step a person performs, and it happens once.`,
|
|
587
|
+
};
|
|
588
|
+
}
|