Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion apps/desktop/src/main/__tests__/app-update-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ describe('AppUpdateService', () => {
state: 'error',
currentVersion: '1.0.0',
latestVersion: '1.1.0',
operation: 'download',
operation: 'verify',
message: 'release provenance did not match',
});
assert.equal(updater.quitAndInstallCalls, 0);
Expand Down Expand Up @@ -504,6 +504,32 @@ describe('AppUpdateService', () => {
assert.equal(statuses.filter((entry) => entry.state === 'error').length, 1);
});

test('a later check failure replaces the earlier error instead of latching it', async () => {
const updater = new FakeUpdater();
let failure = 'first failure';
updater.checkForUpdates = async () => {
updater.checkCalls += 1;
throw new Error(failure);
};
const { clock, service } = createHarness({ updater });

const first = service.checkForUpdatesNow();
await new Promise<void>((resolve) => setImmediate(resolve));
await clock.runNext();
await first;
const firstStatus = service.getStatus();
assert.equal(firstStatus.state === 'error' ? firstStatus.message : undefined, 'first failure');

failure = 'second failure';
const second = service.checkForUpdatesNow();
await new Promise<void>((resolve) => setImmediate(resolve));
await clock.runNext();
await second;
const status = service.getStatus();
assert.equal(status.state, 'error');
assert.equal(status.state === 'error' ? status.message : undefined, 'second failure');
});

test('settles an in-flight retry when disposed during its backoff', async () => {
const updater = new FakeUpdater();
const statuses: AppUpdateStatus[] = [];
Expand Down
40 changes: 25 additions & 15 deletions apps/desktop/src/main/app-update-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,13 +261,23 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
const publishError = (
operation: Extract<AppUpdateStatus, { state: 'error' }>['operation'],
error: unknown,
): AppUpdateStatus => publish({
state: 'error',
currentVersion: deps.currentVersion,
latestVersion: latestVersion(),
operation,
message: error instanceof Error ? error.message : String(error),
});
): AppUpdateStatus => {
const message = error instanceof Error ? error.message : String(error);
// A later failure replaces an earlier one (#4708): latching the first error
// left the panel reporting a stale cause forever. Identical repeats stay
// idempotent so one failed check, which can report through both the error
// listener and the retry catch, still publishes a single status.
if (status.state === 'error' && status.operation === operation && status.message === message) {
return status;
}
return publish({
state: 'error',
currentVersion: deps.currentVersion,
latestVersion: latestVersion(),
operation,
message,
});
};

const disarmInstallQuitWatchdog = (): void => {
deps.nativeUpdater.off('before-quit-for-update', armInstallQuitWatchdog);
Expand Down Expand Up @@ -384,7 +394,7 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
});
})
.catch((error) => {
if (activeVerification === verification) publishError('download', error);
if (activeVerification === verification) publishError('verify', error);
})
.finally(() => {
if (activeVerification === verification) activeVerification = undefined;
Expand All @@ -393,9 +403,11 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
updater.on('error', (error) => {
const operation = status.state === 'installing'
? 'install'
: status.state === 'available' || status.state === 'downloading' || status.state === 'verifying'
? 'download'
: 'check';
: status.state === 'verifying'
? 'verify'
: status.state === 'available' || status.state === 'downloading'
? 'download'
: 'check';
if (operation === 'install') rollbackInstallHandoff();
// A check failure with retry attempts still owed is transient: hold it
// back and let the scheduled retry produce the final word. Download and
Expand Down Expand Up @@ -437,7 +449,7 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
}
if (checkAttemptsRemaining <= 0) {
checkAttemptsRemaining = 0;
return status.state === 'error' ? status : publishError('check', error);
return publishError('check', error);
}
checkAttemptsRemaining -= 1;
// Back off briefly, then re-check. The 'error' listener holds the
Expand All @@ -452,9 +464,7 @@ export function createAppUpdateService(deps: AppUpdateServiceDeps): AppUpdateSer
resolve(status);
return;
}
resolve(attempt().catch((retryError) =>
status.state === 'error' ? status : publishError('check', retryError),
));
resolve(attempt().catch((retryError) => publishError('check', retryError)));
}, UPDATE_CHECK_RETRY_DELAY_MS);
});
}).finally(() => {
Expand Down
23 changes: 19 additions & 4 deletions apps/desktop/src/renderer/locales/settings-preferences-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ export type SettingsPreferencesCopy = {
updateFetchingHint: (version: string) => string;
updateDownloadedHint: (version: string) => string;
updateInstallingHint: (version: string) => string;
updateFailed: Record<'check' | 'download' | 'install', string>;
updateFailed: Record<'check' | 'download' | 'verify' | 'install', string>;
/** Provenance in one line: project, foundation status, licence. */
openSourceSummary: string;
sourceCode: string;
Expand Down Expand Up @@ -373,7 +373,12 @@ const settingsPreferencesCopyByLocaleZhCn: SettingsPreferencesCopy = {
updateFetchingHint: (version) => `v${version},完成后可在这里重启安装。`,
updateDownloadedHint: (version) => `v${version},重启 Maka 即可完成安装。`,
updateInstallingHint: (version) => `v${version},请稍候。`,
updateFailed: { check: '检查更新失败', download: '下载更新失败', install: '安装更新失败' },
updateFailed: {
check: '检查更新失败',
download: '下载更新失败',
verify: '验证更新失败',
install: '安装更新失败',
},
openSourceSummary: 'Apache Maka (incubating) · Apache License 2.0',
sourceCode: '源码', releaseNotes: '发行说明',
},
Expand Down Expand Up @@ -471,7 +476,12 @@ const settingsPreferencesCopyByLocaleZhTw: SettingsPreferencesCopy = {
updateFetchingHint: (version) => `v${version},完成後可在這裡重新啟動安裝。`,
updateDownloadedHint: (version) => `v${version},重新啟動 Maka 即可完成安裝。`,
updateInstallingHint: (version) => `v${version},請稍候。`,
updateFailed: { check: '檢查更新失敗', download: '下載更新失敗', install: '安裝更新失敗' },
updateFailed: {
check: '檢查更新失敗',
download: '下載更新失敗',
verify: '驗證更新失敗',
install: '安裝更新失敗',
},
openSourceSummary: 'Apache Maka (incubating) · Apache License 2.0',
sourceCode: '原始碼', releaseNotes: '發行說明',
},
Expand Down Expand Up @@ -540,7 +550,12 @@ const settingsPreferencesCopyByLocaleEn: SettingsPreferencesCopy = {
updateFetchingHint: (version) => `v${version}; restart from here once it finishes.`,
updateDownloadedHint: (version) => `v${version}; restart Maka to finish installing.`,
updateInstallingHint: (version) => `v${version}; this takes a moment.`,
updateFailed: { check: 'Could not check for updates', download: 'Could not download the update', install: 'Could not install the update' },
updateFailed: {
check: 'Could not check for updates',
download: 'Could not download the update',
verify: 'Could not verify the update',
install: 'Could not install the update',
},
openSourceSummary: 'Apache Maka (incubating) · Apache License 2.0',
sourceCode: 'Source code', releaseNotes: 'Release notes',
},
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/shared/app-update.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export type AppUpdateStatus =
state: 'error';
currentVersion: string;
message: string;
operation: 'check' | 'download' | 'install';
operation: 'check' | 'download' | 'verify' | 'install';
latestVersion?: string;
};

Expand Down