mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] updater: name the failures a user can fix instead of telling them to try again later
This commit is contained in:
+3
-14
@@ -1523,19 +1523,8 @@ function sendToRenderer(channel, ...args) {
|
||||
// Maps a raw electron-updater error to a short, human message. The raw error
|
||||
// is always logged separately for debugging; users only ever see this. No
|
||||
// em/en dashes per repo style.
|
||||
function friendlyUpdateError(err) {
|
||||
const raw = ((err && err.message) || String(err) || '').toLowerCase();
|
||||
// Experimental on, but there is no pre-release to fetch: the provider 404s
|
||||
// looking for the pre-release channel feed. This is the screenshot case.
|
||||
if (autoUpdater && autoUpdater.allowPrerelease &&
|
||||
/404|not found|cannot find|no published|latest.*\.yml/.test(raw)) {
|
||||
return 'No experimental builds available right now. You are on the latest version.';
|
||||
}
|
||||
if (/net::|enotfound|etimedout|econnrefused|getaddrinfo|network/.test(raw)) {
|
||||
return 'Could not reach the update server. Check your connection and try again.';
|
||||
}
|
||||
return 'Update check failed. Please try again later.';
|
||||
}
|
||||
// Extracted to electron/updateErrorMessage.js so the mapping is unit-testable; see node --test there.
|
||||
const { friendlyUpdateError } = require('./updateErrorMessage');
|
||||
|
||||
// Phase 2 provenance: which exact commit produced this build. The build
|
||||
// scripts write electron/build-info.json (gitignored, regenerated each build)
|
||||
@@ -1645,7 +1634,7 @@ function setupAutoUpdater() {
|
||||
// but no pre-release exists": the GitHub provider 404s hunting a pre-release
|
||||
// feed, which is not a real failure, just "nothing newer to install".
|
||||
console.error('Auto-update error:', err);
|
||||
const friendly = friendlyUpdateError(err);
|
||||
const friendly = friendlyUpdateError(err, !!(autoUpdater && autoUpdater.allowPrerelease));
|
||||
cachedUpdateStatus = { status: 'error', info: null, error: friendly };
|
||||
sendToRenderer('update-error', friendly);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"dist:win": "electron-builder --win --x64 --publish never",
|
||||
"dist:win:publish": "electron-builder --win --x64 --publish always",
|
||||
"dist:all": "electron-builder --mac --win --linux",
|
||||
"test": "node --test affiliateTracking.test.js",
|
||||
"test": "node --test affiliateTracking.test.js updateErrorMessage.test.js",
|
||||
"test:mouseclamp": "bash native/mouseclamp/run-tests.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Turning an updater failure into something the user can act on.
|
||||
//
|
||||
// electron-updater reports check, download and install failures through one 'error' event, so the
|
||||
// message a user sees is whatever this mapping decides. The default used to be "Update check
|
||||
// failed. Please try again later." for everything that was not a Chromium net:: error, which is a
|
||||
// dead end: the two most common real causes (running from the DMG on macOS, a quarantined
|
||||
// Update.exe on Windows) never resolve themselves no matter how long you wait, and the user is told
|
||||
// to wait. Each branch below names a failure the user can actually fix.
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {unknown} err the error electron-updater emitted
|
||||
* @param {boolean} allowPrerelease whether the experimental channel is on
|
||||
* @returns {string} a message safe to show in Settings
|
||||
*/
|
||||
function friendlyUpdateError(err, allowPrerelease) {
|
||||
const raw = ((err && err.message) || String(err) || '').toLowerCase();
|
||||
// Experimental on, but there is no pre-release to fetch: the provider 404s looking for the
|
||||
// pre-release channel feed.
|
||||
if (allowPrerelease && /404|not found|cannot find|no published|latest.*\.yml/.test(raw)) {
|
||||
return 'No experimental builds available right now. You are on the latest version.';
|
||||
}
|
||||
if (/net::|enotfound|etimedout|econnrefused|getaddrinfo|network/.test(raw)) {
|
||||
return 'Could not reach the update server. Check your connection and try again.';
|
||||
}
|
||||
// Running from the DMG or a Gatekeeper-translocated copy in ~/Downloads. Squirrel.Mac refuses
|
||||
// deterministically, so this user can never update until the app moves.
|
||||
if (/read-only volume|read only volume|translocat/.test(raw)) {
|
||||
return 'Move OpenSwarm to your Applications folder, then check again. Updates cannot install while it runs from the disk image.';
|
||||
}
|
||||
// Windows Squirrel missing or quarantined by antivirus; retrying never fixes it either.
|
||||
if (/can not find squirrel|cannot find squirrel|update\.exe/.test(raw)) {
|
||||
return 'The updater is missing, which usually means antivirus quarantined it. Reinstall from openswarm.com to fix it.';
|
||||
}
|
||||
// Reachability failures whose text carries no net:: token, so they would otherwise look unexplained.
|
||||
if (/http error|econnreset|certificate|self.signed|\b403\b|\b429\b|econnaborted/.test(raw)) {
|
||||
return 'The update server refused the request. Check your VPN or network, then try again.';
|
||||
}
|
||||
if (/enospc|no space/.test(raw)) {
|
||||
return 'Not enough disk space to download the update. Free up some space and try again.';
|
||||
}
|
||||
return 'Update check failed. Please try again later.';
|
||||
}
|
||||
|
||||
module.exports = { friendlyUpdateError };
|
||||
@@ -0,0 +1,70 @@
|
||||
// A user who cannot update needs to be told what to DO.
|
||||
//
|
||||
// Every failure that was not a Chromium net:: error used to collapse into "Update check failed.
|
||||
// Please try again later." A real 1.5.9 user sat on that message with a perfectly healthy release
|
||||
// feed: the release, its ymls, checksums, signature and notarization all verified good. The cause
|
||||
// was local and permanent, and the app told them to wait.
|
||||
//
|
||||
// Run: cd electron && node --test updateErrorMessage.test.js
|
||||
|
||||
'use strict';
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { friendlyUpdateError } = require('./updateErrorMessage');
|
||||
|
||||
const GENERIC = 'Update check failed. Please try again later.';
|
||||
|
||||
test('running from the disk image tells the user to move the app', () => {
|
||||
const msg = friendlyUpdateError(new Error('Cannot update while running on a read-only volume'), false);
|
||||
assert.match(msg, /Applications folder/);
|
||||
assert.notEqual(msg, GENERIC);
|
||||
});
|
||||
|
||||
test('a Gatekeeper-translocated copy gets the same advice', () => {
|
||||
const msg = friendlyUpdateError(new Error('app is translocated'), false);
|
||||
assert.match(msg, /Applications folder/);
|
||||
});
|
||||
|
||||
test('a quarantined Windows updater says so instead of "try again"', () => {
|
||||
const msg = friendlyUpdateError(new Error('Can not find Squirrel'), false);
|
||||
assert.match(msg, /antivirus/);
|
||||
assert.notEqual(msg, GENERIC);
|
||||
});
|
||||
|
||||
test('an HTTP refusal reads as reachability, not as an unexplained failure', () => {
|
||||
for (const raw of ['HTTP error: Forbidden', 'ECONNRESET', 'unable to verify the first certificate']) {
|
||||
const msg = friendlyUpdateError(new Error(raw), false);
|
||||
assert.match(msg, /VPN or network/, `"${raw}" should read as reachability`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a full disk names the disk', () => {
|
||||
assert.match(friendlyUpdateError(new Error('ENOSPC: no space left on device'), false), /disk space/);
|
||||
});
|
||||
|
||||
test('network errors keep their existing message', () => {
|
||||
const msg = friendlyUpdateError(new Error('net::ERR_INTERNET_DISCONNECTED'), false);
|
||||
assert.match(msg, /Check your connection/);
|
||||
});
|
||||
|
||||
test('the experimental-channel 404 still wins when prerelease is on', () => {
|
||||
const msg = friendlyUpdateError(new Error('404 Not Found: latest-mac.yml'), true);
|
||||
assert.match(msg, /No experimental builds/);
|
||||
});
|
||||
|
||||
test('the same 404 is NOT the experimental message when prerelease is off', () => {
|
||||
const msg = friendlyUpdateError(new Error('404 Not Found: latest-mac.yml'), false);
|
||||
assert.doesNotMatch(msg, /No experimental builds/);
|
||||
});
|
||||
|
||||
test('a genuinely unknown failure still falls through to the generic message', () => {
|
||||
// The discriminating half. If everything matched something, the buckets would be meaningless.
|
||||
assert.equal(friendlyUpdateError(new Error('something nobody predicted'), false), GENERIC);
|
||||
});
|
||||
|
||||
test('a null error does not throw', () => {
|
||||
assert.equal(typeof friendlyUpdateError(null, false), 'string');
|
||||
assert.equal(typeof friendlyUpdateError(undefined, true), 'string');
|
||||
});
|
||||
Reference in New Issue
Block a user