amicus 4.9.4 → 4.9.6
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +171 -3
- package/README.md +1 -1
- package/docs/ROADMAP.md +6 -4
- package/docs/architecture-map.md +31 -3
- package/docs/configuration.md +97 -0
- package/docs/troubleshooting.md +130 -0
- package/docs/usage.md +1 -1
- package/package.json +2 -1
- package/scripts/postinstall.js +28 -3
- package/src/sidecar/electron-cache.js +41 -1
- package/src/sidecar/electron-custody.js +180 -0
- package/src/sidecar/electron-ensure.js +25 -4
- package/src/sidecar/electron-env-scrub.js +233 -0
- package/src/sidecar/electron-install.js +149 -159
- package/src/sidecar/electron-layout.js +300 -0
- package/src/sidecar/electron-native-plan.js +157 -0
- package/src/sidecar/electron-native-rescue.js +231 -0
- package/src/sidecar/electron-provision.js +232 -0
- package/src/sidecar/electron-refuse.js +253 -0
- package/src/sidecar/electron-repair-cache.js +212 -0
- package/src/sidecar/electron-rescue-notice.js +78 -0
- package/src/sidecar/electron-trust.js +226 -0
- package/src/sidecar/unzip.js +52 -2
- package/src/sidecar/zip-entry-write.js +268 -0
- package/src/sidecar/zip-from-buffer.js +220 -0
- package/src/sidecar/zip-name-scan.js +141 -0
- package/src/sidecar/zip-stall-bound.js +144 -0
- package/src/utils/doctor-electron-mcp-check.js +9 -2
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Electron self-heal primitive (#53, #59).
|
|
3
3
|
*
|
|
4
|
-
* Electron is an optionalDependency (^28.0.0). A flaky / interrupted extract
|
|
5
|
-
*
|
|
6
|
-
* path.txt on disk while dist/<exe> is MISSING, so the GUI silently fails.
|
|
4
|
+
* Electron is an optionalDependency (^28.0.0). A flaky / interrupted extract —
|
|
5
|
+
* or Windows Defender quarantining electron.exe — can leave the package's
|
|
6
|
+
* path.txt on disk while dist/<exe> is MISSING, so the GUI silently fails. This
|
|
7
|
+
* module is the keystone the rest of the self-heal cluster (#54-#57) imports; it
|
|
8
|
+
* wires itself into no caller, and everything that downloads, extracts, spawns or
|
|
9
|
+
* locks is dependency-INJECTABLE so tests never hit the network or extract a real
|
|
10
|
+
* binary.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Layout (npm `electron`): path.txt -> the exe basename, dist/<exe> -> the binary.
|
|
13
|
+
* #59: ELECTRON_OVERRIDE_DIST_PATH moves the exe to <override>/<exe> (mirrors
|
|
14
|
+
* electron/index.js + install.js semantics). Cache layout (@electron/get):
|
|
15
|
+
* <cacheRoot>/<sha256>/electron-v<ver>-<platform>-<arch>.zip
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
17
|
+
* AT THE SIZE GATE, so pieces live next door: `./electron-custody` reads the
|
|
18
|
+
* artifact into memory once, `./zip-from-buffer` extracts what was read,
|
|
19
|
+
* `./electron-layout` holds `platformExe`/`writePathTxt`/`extractBytesToDist`
|
|
20
|
+
* (`platformExe` re-exported here for `ei.platformExe`), `./electron-refuse`
|
|
21
|
+
* holds the refusal messages, `./electron-repair-cache` the whole cached-artifact
|
|
22
|
+
* route, `./electron-provision` the pinned download. The arrow points one way out
|
|
23
|
+
* of this file and never back.
|
|
19
24
|
*/
|
|
20
25
|
|
|
21
26
|
'use strict';
|
|
22
27
|
|
|
23
28
|
const fsDefault = require('fs');
|
|
24
29
|
const path = require('path');
|
|
25
|
-
const { spawnSync } = require('child_process');
|
|
26
30
|
|
|
27
|
-
const {
|
|
31
|
+
const { cachedZip } = require('./electron-cache');
|
|
32
|
+
const { isSafeArtifactName } = require('./electron-custody');
|
|
28
33
|
const { avHint, verifyExtractOutcome: verifyQuarantine } = require('./electron-quarantine');
|
|
29
34
|
const { acquireRepairLock } = require('./electron-lock');
|
|
30
|
-
const {
|
|
35
|
+
const { platformExe } = require('./electron-layout');
|
|
36
|
+
const { controlledProvision } = require('./electron-provision');
|
|
37
|
+
const { repairFromCache } = require('./electron-repair-cache');
|
|
38
|
+
const { isUnsafeArchive, refuseUnsafeArchive } = require('./electron-refuse');
|
|
39
|
+
const { artifactFileName, electronTrustPolicy, resolveAnchor } = require('./electron-trust');
|
|
40
|
+
const { extractZipBuffer } = require('./zip-from-buffer');
|
|
41
|
+
const { collapseExcerpt } = require('../utils/text-sanitize');
|
|
31
42
|
|
|
32
43
|
/** Self-heal progress line to stderr (visible during first-GUI provision). */
|
|
33
44
|
function stderrLog(msg) {
|
|
@@ -43,19 +54,6 @@ function defaultElectronDir() {
|
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
|
|
46
|
-
/** Platform exe basename, matching electron's getPlatformPath(). */
|
|
47
|
-
function platformExe(platform) {
|
|
48
|
-
switch (platform) {
|
|
49
|
-
case 'mas':
|
|
50
|
-
case 'darwin':
|
|
51
|
-
return path.join('Electron.app', 'Contents', 'MacOS', 'Electron');
|
|
52
|
-
case 'win32':
|
|
53
|
-
return 'electron.exe';
|
|
54
|
-
default:
|
|
55
|
-
return 'electron';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
57
|
/**
|
|
60
58
|
* Resolve the on-disk electron exe path from the package layout.
|
|
61
59
|
* Mirrors ELECTRON_OVERRIDE_DIST_PATH semantics (#59): when set, the exe is
|
|
@@ -93,73 +91,6 @@ function isElectronUsable({ electronDir = defaultElectronDir(), env = process.en
|
|
|
93
91
|
}
|
|
94
92
|
}
|
|
95
93
|
|
|
96
|
-
/**
|
|
97
|
-
* Locate a previously-downloaded electron zip in the env-configurable cache
|
|
98
|
-
* roots. Walks <root>/<sha>/electron-v<ver>-<platform>-<arch>.zip.
|
|
99
|
-
* @returns {string|null} absolute zip path, or null when no cache hit.
|
|
100
|
-
*/
|
|
101
|
-
function cachedZip({ version, platform = process.platform, arch = process.arch, env = process.env, fs = fsDefault } = {}) {
|
|
102
|
-
const zipName = `electron-v${version}-${platform}-${arch}.zip`;
|
|
103
|
-
for (const root of resolveCacheRoots(env)) {
|
|
104
|
-
let shaDirs;
|
|
105
|
-
try {
|
|
106
|
-
shaDirs = fs.readdirSync(root);
|
|
107
|
-
} catch {
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
for (const sha of shaDirs) {
|
|
111
|
-
const candidate = path.join(root, sha, zipName);
|
|
112
|
-
try {
|
|
113
|
-
if (fs.existsSync(candidate)) {
|
|
114
|
-
return candidate;
|
|
115
|
-
}
|
|
116
|
-
} catch {
|
|
117
|
-
/* ignore unreadable subdir */
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Restore path.txt so electron/index.js resolves the freshly-extracted exe. */
|
|
125
|
-
function writePathTxt({ electronDir, platform, fs }) {
|
|
126
|
-
fs.writeFileSync(path.join(electronDir, 'path.txt'), platformExe(platform));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** Extract a cached zip into <electronDir>/dist offline. */
|
|
130
|
-
async function extractFromCache({ zip, electronDir, platform, extract, fs }) {
|
|
131
|
-
const distDir = path.join(electronDir, 'dist');
|
|
132
|
-
fs.mkdirSync(distDir, { recursive: true });
|
|
133
|
-
await extract(zip, { dir: distDir });
|
|
134
|
-
writePathTxt({ electronDir, platform, fs });
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** Best-effort cache root for downloadArtifact (first resolved root). */
|
|
138
|
-
function cacheRootFor(env = process.env) {
|
|
139
|
-
return resolveCacheRoots(env)[0];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* CONTROLLED provision: fetch the zip ourselves with the SAME @electron/get
|
|
144
|
-
* api install.js uses (downloadArtifact, force:true), extract offline, and let
|
|
145
|
-
* the caller verify isElectronUsable(). No blind install.js spawn.
|
|
146
|
-
* @returns {Promise<void>}
|
|
147
|
-
*/
|
|
148
|
-
async function controlledProvision({
|
|
149
|
-
electronDir, platform, arch, version, downloadArtifact, extract, fs, env = process.env, downloadMs = 480000,
|
|
150
|
-
}) {
|
|
151
|
-
const zip = await downloadArtifact({
|
|
152
|
-
version,
|
|
153
|
-
artifactName: 'electron',
|
|
154
|
-
force: true,
|
|
155
|
-
cacheRoot: cacheRootFor(env),
|
|
156
|
-
platform,
|
|
157
|
-
arch,
|
|
158
|
-
downloadOptions: { signal: AbortSignal.timeout(downloadMs) }, // 5.x native fetch: bound stalled downloads, free the lock
|
|
159
|
-
});
|
|
160
|
-
await extractFromCache({ zip, electronDir, platform, extract, fs });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
94
|
/** Bind the fs-aware probes for the post-extract AV-quarantine verify. */
|
|
164
95
|
function verifyExtractOutcome({ electronDir, platform, fs }) {
|
|
165
96
|
return verifyQuarantine({
|
|
@@ -169,30 +100,25 @@ function verifyExtractOutcome({ electronDir, platform, fs }) {
|
|
|
169
100
|
});
|
|
170
101
|
}
|
|
171
102
|
|
|
172
|
-
/** Drive electron's own install.js with force_no_cache semantics. */
|
|
173
|
-
function runInstaller({ electronDir, force, spawn }) {
|
|
174
|
-
const installScript = path.join(electronDir, 'install.js');
|
|
175
|
-
const env = { ...process.env };
|
|
176
|
-
if (force) {
|
|
177
|
-
env.force_no_cache = 'true';
|
|
178
|
-
}
|
|
179
|
-
return spawn(process.execPath, [installScript], { env, stdio: 'ignore' });
|
|
180
|
-
}
|
|
181
|
-
|
|
182
103
|
/**
|
|
183
104
|
* Heal a broken electron install.
|
|
184
105
|
*
|
|
185
106
|
* @param {object} opts
|
|
186
107
|
* @param {boolean} [opts.cacheOnly] never hit the network; return
|
|
187
108
|
* {deferred,reason} when there is no cached zip.
|
|
188
|
-
* @param {
|
|
189
|
-
* @param {
|
|
190
|
-
*
|
|
109
|
+
* @param {number} [opts.timeoutMs] best-effort download timeout.
|
|
110
|
+
* @param {object} [opts.deps] injected { cachedZip, extract, acquireLock, fs,
|
|
111
|
+
* selfElectronDir } — the last pins the digest anchor's top rung (null disables it).
|
|
112
|
+
*
|
|
113
|
+
* THERE IS NO `force` OPTION. There was, and it did nothing: it only ever set
|
|
114
|
+
* `force_no_cache` for the install.js spawn (now deleted), and `force` is DEAD
|
|
115
|
+
* in @electron/get 5.0.0 anyway — `effectiveCacheMode` never reads it, MEASURED.
|
|
116
|
+
* An accepted-but-inert flag is worse than no flag, so it is gone rather than
|
|
117
|
+
* documented.
|
|
191
118
|
* @returns {Promise<{repaired?:boolean, deferred?:boolean, contended?:boolean, reason?:string}>}
|
|
192
119
|
*/
|
|
193
120
|
async function repairElectron({
|
|
194
121
|
cacheOnly = false,
|
|
195
|
-
force = false,
|
|
196
122
|
timeoutMs,
|
|
197
123
|
electronDir = defaultElectronDir(),
|
|
198
124
|
platform = process.platform,
|
|
@@ -201,13 +127,22 @@ async function repairElectron({
|
|
|
201
127
|
deps = {},
|
|
202
128
|
} = {}) {
|
|
203
129
|
const fs = deps.fs || fsDefault;
|
|
204
|
-
// Default extract:
|
|
130
|
+
// Default extract: from the BUFFER amicus already hashed, never from a name.
|
|
131
|
+
// `unzip.js` is byte-for-byte unchanged; the one thing still taken from it is
|
|
132
|
+
// its native PLAN, and only behind the hatch (see ./electron-native-rescue).
|
|
133
|
+
//
|
|
134
|
+
// WHAT CAME WITH IT AND WHAT DID NOT. Its idle + hard-cap STALL BOUND is
|
|
135
|
+
// reimplemented in `zip-from-buffer.js` with the same numbers, so this path is
|
|
136
|
+
// bounded again (it was not, for three commits). Its NATIVE OS unzip fallback
|
|
137
|
+
// is NOT the default and never can be: `tar`/`Expand-Archive`/`ditto`/`unzip`
|
|
138
|
+
// all take a path, and feeding one either the artifact or a temp copy of our
|
|
139
|
+
// Buffer undoes the custody property outright. C2 re-wires it as a RESCUE for
|
|
140
|
+
// ONE failure class, reachable only with AMICUS_ALLOW_UNVERIFIED_ELECTRON=1 —
|
|
141
|
+
// the flag whose documented meaning is already "I accept bytes amicus cannot
|
|
142
|
+
// vouch for". `spawn` is threaded for it, and stays injectable for tests.
|
|
205
143
|
const extract = deps.extract
|
|
206
|
-
|| ((
|
|
207
|
-
|
|
208
|
-
// provision that reaches runInstaller without an explicit timeoutMs can't hang
|
|
209
|
-
// the holder — the caller's timeoutMs still wins when provided.
|
|
210
|
-
const spawn = deps.spawn || ((cmd, args, o) => spawnSync(cmd, args, { ...o, timeout: timeoutMs || 480000 }));
|
|
144
|
+
|| ((bytes, o) => extractZipBuffer(bytes, { ...o, deps: { fs, log: stderrLog } }));
|
|
145
|
+
const spawn = deps.spawn;
|
|
211
146
|
const findZip = deps.cachedZip || ((o) => cachedZip(o));
|
|
212
147
|
const acquireLock = deps.acquireLock || ((o) => acquireRepairLock({ ...o, fs }));
|
|
213
148
|
// Lazy: import the ESM-only @electron/get only on the network path, so cacheOnly
|
|
@@ -217,13 +152,26 @@ async function repairElectron({
|
|
|
217
152
|
: async () => (await import('@electron/get')).downloadArtifact;
|
|
218
153
|
|
|
219
154
|
if (!version) {
|
|
220
|
-
try {
|
|
221
|
-
version = require(path.join(electronDir, 'package.json')).version;
|
|
222
|
-
} catch {
|
|
223
|
-
version = undefined;
|
|
224
|
-
}
|
|
155
|
+
try { version = require(path.join(electronDir, 'package.json')).version; } catch { version = undefined; }
|
|
225
156
|
}
|
|
226
157
|
|
|
158
|
+
// The digest anchor and the trust policy, resolved ONCE for both routes. NOTE
|
|
159
|
+
// `version` is deliberately NOT passed to resolveAnchor: it may have just been
|
|
160
|
+
// read out of electronDir's own package.json above, and letting an untrusted
|
|
161
|
+
// directory pick which anchor judges its bytes is the ANCHORFROMTARGET hole.
|
|
162
|
+
const fileName = artifactFileName({ version, platform, arch });
|
|
163
|
+
// F#3: `version` may have just been read out of an UNTRUSTED <electronDir>/
|
|
164
|
+
// package.json (doctor --fix supplies none, for a dir it found by scanning npx
|
|
165
|
+
// caches), and `fileName` is joined into paths downstream. MEASURED before the
|
|
166
|
+
// check, with a planted "43.1.1/../../victim": a path two levels outside the
|
|
167
|
+
// intended directory was written and a pre-existing file there was destroyed.
|
|
168
|
+
// Nothing downstream ever sees a name that is not a plain filename.
|
|
169
|
+
if (!isSafeArtifactName(fileName)) {
|
|
170
|
+
return { repaired: false, integrity: 'unsafe-name', reason: `Refusing to provision electron: ${collapseExcerpt(fileName, 160)} is not a usable artifact name.` };
|
|
171
|
+
}
|
|
172
|
+
const policy = electronTrustPolicy(process.env);
|
|
173
|
+
const anchor = resolveAnchor({ electronDir, fs, selfElectronDir: deps.selfElectronDir });
|
|
174
|
+
|
|
227
175
|
// Single-flight: bail out gracefully if another caller is already repairing.
|
|
228
176
|
let lock;
|
|
229
177
|
try {
|
|
@@ -235,55 +183,97 @@ async function repairElectron({
|
|
|
235
183
|
throw e;
|
|
236
184
|
}
|
|
237
185
|
|
|
186
|
+
let refusal = null; // a cache refusal the caller must still hear about if the download also fails
|
|
238
187
|
try {
|
|
239
|
-
// Attempt 1: extract from cache (always preferred, fully offline).
|
|
188
|
+
// Attempt 1: extract from cache (always preferred, fully offline). The whole
|
|
189
|
+
// route lives in ./electron-repair-cache — read once into memory, hash THOSE
|
|
190
|
+
// bytes, extract THOSE bytes — and answers `done` when nothing is left to try.
|
|
240
191
|
const zip = findZip({ version, platform, arch, env: process.env, fs });
|
|
241
192
|
if (zip) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
try { fs.rmSync(zip, { force: true }); } catch { /* ignore */ }
|
|
250
|
-
if (cacheOnly) {
|
|
251
|
-
return {
|
|
252
|
-
repaired: false,
|
|
253
|
-
reason: `Cached electron zip for v${version} (${platform}-${arch}) was corrupt and removed; deferring re-download.${avHint(platform)}`,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
// else: drop into the controlled download below.
|
|
257
|
-
}
|
|
193
|
+
const attempt = await repairFromCache({
|
|
194
|
+
zip, fileName, anchor, policy, electronDir, platform, arch, version, cacheOnly,
|
|
195
|
+
extract, verifyOutcome: () => verifyExtractOutcome({ electronDir, platform, fs }),
|
|
196
|
+
fs, spawn, env: process.env, log: stderrLog,
|
|
197
|
+
});
|
|
198
|
+
if (attempt.done) { return attempt.result; }
|
|
199
|
+
refusal = attempt.refusal;
|
|
258
200
|
} else if (cacheOnly) {
|
|
259
|
-
return {
|
|
260
|
-
deferred: true,
|
|
261
|
-
reason: `No cached electron zip found for v${version} (${platform}-${arch}); deferring download.${avHint(platform)}`,
|
|
262
|
-
};
|
|
201
|
+
return { deferred: true, reason: `No cached electron zip found for v${version} (${platform}-${arch}); deferring download.${avHint(platform)}` };
|
|
263
202
|
}
|
|
264
203
|
|
|
265
|
-
// Attempt 2 (online): CONTROLLED download+extract instead of a blind
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
204
|
+
// Attempt 2 (online): CONTROLLED download+extract instead of a blind install.js
|
|
205
|
+
// spawn — the SAME @electron/get api install.js uses, extracted offline, then the
|
|
206
|
+
// REAL usability reported. A download that produced no usable exe is a FAILURE (#53).
|
|
207
|
+
// A non-null `provision` means download + extract returned without throwing;
|
|
208
|
+
// its `pinned:false` means the extracted bytes were vouched for only by the
|
|
209
|
+
// mirror — either no `checksums` went out (no anchor row, or the hatch dropped
|
|
210
|
+
// the pin) or amicus's own hash of the bytes it read did not say `verified`.
|
|
211
|
+
// F3 marks that, as both docs already promise it does.
|
|
212
|
+
let provision = null;
|
|
213
|
+
let provisionReason = null;
|
|
270
214
|
try {
|
|
271
215
|
const downloadArtifact = await resolveDownloadArtifact();
|
|
272
|
-
await controlledProvision({
|
|
273
|
-
electronDir, platform, arch, version, downloadArtifact, extract,
|
|
216
|
+
const result = await controlledProvision({
|
|
217
|
+
electronDir, platform, arch, version, anchor, downloadArtifact, extract,
|
|
218
|
+
fs, spawn, env: process.env, downloadMs: timeoutMs, policy, log: stderrLog,
|
|
274
219
|
});
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
|
|
220
|
+
// F3 (seat B4) — FAIL CLOSED ON A SHAPE NOBODY RECOGNISES. This used to be
|
|
221
|
+
// `|| { pinned: false }`, described as "an unrecognisable return marks,
|
|
222
|
+
// never claims a pin". It did worse than that: a function that returned
|
|
223
|
+
// NOTHING became a successful unpinned provision, so `verifyExtractOutcome`
|
|
224
|
+
// ran on a directory nothing had written and reported the missing exe as
|
|
225
|
+
// the AV-QUARANTINE signature — "electron.exe was removed right after it
|
|
226
|
+
// was extracted", advice about an extraction that never happened. An
|
|
227
|
+
// unrecognised return is a FAILURE, reported through the same path a throw
|
|
228
|
+
// takes, because that path already says the true thing.
|
|
229
|
+
if (!result || typeof result !== 'object' || typeof result.pinned !== 'boolean') {
|
|
230
|
+
throw Object.assign(
|
|
231
|
+
new Error(`the controlled provision returned no usable result (${result === undefined ? 'undefined' : typeof result})`),
|
|
232
|
+
{ code: 'PROVISION_NO_RESULT' },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
provision = result;
|
|
236
|
+
} catch (provisionErr) {
|
|
237
|
+
// C4: an unsafe archive is terminal — it is never retried through another
|
|
238
|
+
// extractor, and it is not reported as an ordinary failure.
|
|
239
|
+
if (isUnsafeArchive(provisionErr)) { return refuseUnsafeArchive({ err: provisionErr, fileName, log: stderrLog }); }
|
|
240
|
+
// B1: this is where the last-resort install.js spawn used to be. It is gone
|
|
241
|
+
// (see electron-provision.js), so the failure is REPORTED rather than
|
|
242
|
+
// routed around. `provision` stays null and `out.repaired` is the honest
|
|
243
|
+
// stat of the exe, but the REASON must survive — a bare {repaired:false}
|
|
244
|
+
// is what made a failed provision indistinguishable from "not provisioned".
|
|
245
|
+
provisionReason = collapseExcerpt((provisionErr && provisionErr.message) || String(provisionErr));
|
|
246
|
+
stderrLog(`[amicus] the controlled Electron download did not complete: ${provisionReason}`);
|
|
247
|
+
stderrLog('[amicus] Headless runs and the council work without the GUI.');
|
|
248
|
+
}
|
|
249
|
+
// F#2: the download hashed the bytes it read and refused them, or could not
|
|
250
|
+
// read them at all. Nothing was extracted, so there is no outcome to verify —
|
|
251
|
+
// return the refusal, carrying any cache refusal that preceded it.
|
|
252
|
+
if (provision && provision.refused) {
|
|
253
|
+
// A4/D4: this used to drop the cache refusal whenever the two shared an
|
|
254
|
+
// `integrity` class — so TWO mismatches (a poisoned cache entry AND a
|
|
255
|
+
// hostile mirror, the single most alarming pair this code can observe)
|
|
256
|
+
// reported only the second, and never told the user the cached artifact had
|
|
257
|
+
// also been refused and possibly deleted. Dedupe on the SENTENCE instead:
|
|
258
|
+
// identical text is noise, a different path or a different digest is not.
|
|
259
|
+
const carried = refusal && refusal.reason !== provision.refused.reason ? refusal.reason : null;
|
|
260
|
+
return { ...provision.refused, reason: [carried, provision.refused.reason].filter(Boolean).join(' ') };
|
|
281
261
|
}
|
|
282
262
|
// A NON-throwing controlled extract that left no usable exe is the
|
|
283
|
-
// AV-quarantine signature — surface it actionably (no false success, no
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
263
|
+
// AV-quarantine signature — surface it actionably (no false success, no loop).
|
|
264
|
+
const out = provision
|
|
265
|
+
? verifyExtractOutcome({ electronDir, platform, fs })
|
|
266
|
+
: { repaired: isElectronUsable({ electronDir, platform, fs }) };
|
|
267
|
+
// A refusal the download did not rescue must reach doctor and the postinstall
|
|
268
|
+
// notice; plain {repaired:false} is what made a REFUSED artifact read as an
|
|
269
|
+
// ordinary "not provisioned" everywhere outside the cacheOnly path.
|
|
270
|
+
if (!out.repaired && (refusal || provisionReason)) {
|
|
271
|
+
const parts = [refusal && refusal.reason, provisionReason && `The controlled download failed: ${provisionReason}.`, out.reason];
|
|
272
|
+
return { ...out, ...(refusal ? { integrity: refusal.integrity } : {}), reason: parts.filter(Boolean).join(' ') };
|
|
273
|
+
}
|
|
274
|
+
// F3: the SAME mark the cache route already applies, on the route that omitted it.
|
|
275
|
+
if (out.repaired && provision && !provision.pinned) { return { ...out, unverified: true }; }
|
|
276
|
+
return out;
|
|
287
277
|
} finally {
|
|
288
278
|
try { lock.release(); } catch { /* ignore */ }
|
|
289
279
|
}
|