Skip to content

Commit c060544

Browse files
authored
feat(auto-updater): Error when requesting updates from an outdated MacOS machine COMPASS-9081 (#6851)
* Upgrade get-os-info * Pass Darwin os version via query params * Add a new OutdatedOperatingSystem state * Parse a status 426 response * Adjust message when manual check fails because of an outdated OS * Add state transition tests * Propagate error through the UI * Fix menu idle state after failing update * Add a link to system requirements to the toast * Remove v prefix * Wrapping parsing of 426 HTTP responses in a try-catch * Add a link to system requirements to message box
1 parent e8ff053 commit c060544

File tree

8 files changed

+224
-39
lines changed

8 files changed

+224
-39
lines changed

package-lock.json

Lines changed: 15 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/compass/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@
227227
"@mongodb-js/connection-storage": "^0.26.8",
228228
"@mongodb-js/devtools-proxy-support": "^0.4.2",
229229
"@mongodb-js/eslint-config-compass": "^1.3.8",
230-
"@mongodb-js/get-os-info": "^0.3.24",
230+
"@mongodb-js/get-os-info": "^0.4.0",
231231
"@mongodb-js/mocha-config-compass": "^1.6.8",
232232
"@mongodb-js/mongodb-downloader": "^0.3.7",
233233
"@mongodb-js/my-queries-storage": "^0.22.8",

packages/compass/src/app/components/update-toasts.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,28 @@ export function onAutoupdateStarted({ newVersion }: { newVersion: string }) {
105105
title: `Compass ${newVersion} is downloading`,
106106
});
107107
}
108-
export function onAutoupdateFailed() {
108+
export function onAutoupdateFailed(reason?: 'outdated-operating-system') {
109109
openToast(updateToastId, {
110110
variant: 'warning',
111111
title: 'Failed to download Compass update',
112-
description: 'Downloading a newer Compass version failed',
112+
description:
113+
reason === 'outdated-operating-system' ? (
114+
<>
115+
<Body>
116+
The version of your operating system is no longer supported.
117+
</Body>
118+
<Link
119+
data-testid="system-requirements-link"
120+
as="a"
121+
target="_blank"
122+
href="https://www.mongodb.com/docs/compass/current/install/"
123+
>
124+
See Documentation on System Requirements
125+
</Link>
126+
</>
127+
) : (
128+
'Downloading a newer Compass version failed'
129+
),
113130
});
114131
}
115132
export function onAutoupdateSuccess({

packages/compass/src/app/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,12 @@ const app = {
371371
onAutoupdateStarted({ newVersion });
372372
}
373373
);
374-
ipcRenderer?.on('autoupdate:update-download-failed', onAutoupdateFailed);
374+
ipcRenderer?.on(
375+
'autoupdate:update-download-failed',
376+
(_, reason?: 'outdated-operating-system') => {
377+
onAutoupdateFailed(reason);
378+
}
379+
);
375380
ipcRenderer?.on(
376381
'autoupdate:update-download-success',
377382
(_, { newVersion }: { newVersion: string }) => {

packages/compass/src/main/auto-update-manager.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ describe('CompassAutoUpdateManager', function () {
103103
expect(url.searchParams.get('os_linux_dist')).to.exist;
104104
expect(url.searchParams.get('os_linux_release')).to.exist;
105105
}
106+
107+
const isDarwin = process.platform === 'darwin';
108+
if (isDarwin) {
109+
expect(url.searchParams.get('os_darwin_product_version')).to.exist;
110+
}
106111
});
107112

108113
it('should check for update and transition to update not available if backend returned nothing', async function () {
@@ -441,4 +446,82 @@ describe('CompassAutoUpdateManager', function () {
441446
expect(restartToastIpcPrompt).to.be.calledOnce;
442447
});
443448
});
449+
450+
describe('when operating system is outdated', () => {
451+
beforeEach(async () => {
452+
const fetchStub = sandbox.stub();
453+
CompassAutoUpdateManager['fetch'] = fetchStub;
454+
455+
const updateUrl = await CompassAutoUpdateManager.getUpdateCheckURL();
456+
fetchStub.callsFake((url) => {
457+
expect(url).equals(updateUrl.toString());
458+
return Promise.resolve(
459+
new Response(
460+
JSON.stringify({
461+
available: false,
462+
reason: 'outdated-operating-system',
463+
expectedVersion: '1.2.3',
464+
}),
465+
{ status: 426 }
466+
)
467+
);
468+
});
469+
});
470+
471+
it('return expected result when checking for update', async () => {
472+
const result = await CompassAutoUpdateManager.checkForUpdate();
473+
expect(result).to.deep.equal({
474+
available: false,
475+
reason: 'outdated-operating-system',
476+
expectedVersion: '1.2.3',
477+
});
478+
});
479+
480+
it('should transition to outdated operating system state (automatically)', async () => {
481+
expect(
482+
await setStateAndWaitForUpdate(
483+
AutoUpdateManagerState.Initial,
484+
AutoUpdateManagerState.CheckingForUpdatesForAutomaticCheck,
485+
AutoUpdateManagerState.OutdatedOperatingSystem
486+
)
487+
).to.be.true;
488+
});
489+
490+
it('should transition to outdated operating system state (manually)', async () => {
491+
expect(
492+
await setStateAndWaitForUpdate(
493+
AutoUpdateManagerState.Initial,
494+
AutoUpdateManagerState.CheckingForUpdatesForManualCheck,
495+
AutoUpdateManagerState.OutdatedOperatingSystem
496+
)
497+
).to.be.true;
498+
});
499+
500+
it('should broadcast a message on the main ipc channel', () => {
501+
const restartToastIpcPrompt = sandbox
502+
.stub(ipcMain!, 'broadcast')
503+
.callsFake((eventName, reason) => {
504+
expect(eventName).to.equal('autoupdate:update-download-failed');
505+
expect(reason).to.equal('outdated-operating-system');
506+
});
507+
508+
// Automatic check
509+
CompassAutoUpdateManager['state'] =
510+
AutoUpdateManagerState.CheckingForUpdatesForAutomaticCheck;
511+
CompassAutoUpdateManager.setState(
512+
AutoUpdateManagerState.OutdatedOperatingSystem
513+
);
514+
515+
expect(restartToastIpcPrompt).to.be.calledOnce;
516+
517+
// Manual check
518+
CompassAutoUpdateManager['state'] =
519+
AutoUpdateManagerState.CheckingForUpdatesForManualCheck;
520+
CompassAutoUpdateManager.setState(
521+
AutoUpdateManagerState.OutdatedOperatingSystem
522+
);
523+
524+
expect(restartToastIpcPrompt).to.be.calledTwice;
525+
});
526+
});
444527
});

packages/compass/src/main/auto-update-manager.ts

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export const enum AutoUpdateManagerState {
140140
Restarting = 'restarting',
141141
RestartDismissed = 'restart-dismissed',
142142
PromptToUpdateExternally = 'prompt-to-update-externally',
143+
OutdatedOperatingSystem = 'outdated-operating-system',
143144
}
144145

145146
type UpdateInfo = {
@@ -183,10 +184,31 @@ const checkForUpdates: StateEnterAction = async function checkForUpdates(
183184
updateManager.setState(AutoUpdateManagerState.UpdateAvailable, updateInfo);
184185
} else {
185186
if (fromState === AutoUpdateManagerState.UserPromptedManualCheck) {
186-
void dialog.showMessageBox({
187-
icon: COMPASS_ICON,
188-
message: 'There are currently no updates available.',
189-
});
187+
if (updateInfo.reason === 'outdated-operating-system') {
188+
void dialog
189+
.showMessageBox({
190+
icon: COMPASS_ICON,
191+
message: `The version of your operating system is no longer supported. Expected at least ${updateInfo.expectedVersion}.`,
192+
buttons: ['OK', 'Visit Documentation on System Requirements'],
193+
})
194+
.then(async (value) => {
195+
if (value.response === 1) {
196+
await shell.openExternal(
197+
'https://www.mongodb.com/docs/compass/current/install/'
198+
);
199+
}
200+
});
201+
} else {
202+
void dialog.showMessageBox({
203+
icon: COMPASS_ICON,
204+
message: 'There are currently no updates available.',
205+
});
206+
}
207+
}
208+
209+
if (updateInfo.reason === 'outdated-operating-system') {
210+
updateManager.setState(AutoUpdateManagerState.OutdatedOperatingSystem);
211+
return;
190212
}
191213

192214
this.maybeInterrupt();
@@ -270,6 +292,7 @@ const STATE_UPDATE: Record<
270292
AutoUpdateManagerState.NoUpdateAvailable,
271293
AutoUpdateManagerState.Disabled,
272294
AutoUpdateManagerState.UserPromptedManualCheck,
295+
AutoUpdateManagerState.OutdatedOperatingSystem,
273296
],
274297
enter: checkForUpdates,
275298
},
@@ -279,6 +302,7 @@ const STATE_UPDATE: Record<
279302
AutoUpdateManagerState.NoUpdateAvailable,
280303
AutoUpdateManagerState.Disabled,
281304
AutoUpdateManagerState.UserPromptedManualCheck,
305+
AutoUpdateManagerState.OutdatedOperatingSystem,
282306
],
283307
enter: checkForUpdates,
284308
},
@@ -471,6 +495,20 @@ const STATE_UPDATE: Record<
471495
);
472496
},
473497
},
498+
[AutoUpdateManagerState.OutdatedOperatingSystem]: {
499+
nextStates: [AutoUpdateManagerState.UserPromptedManualCheck],
500+
enter: () => {
501+
ipcMain?.broadcast(
502+
'autoupdate:update-download-failed',
503+
'outdated-operating-system'
504+
);
505+
log.info(
506+
mongoLogId(1_001_000_346),
507+
'AutoUpdateManager',
508+
'Outdated operating system'
509+
);
510+
},
511+
},
474512
[AutoUpdateManagerState.DownloadingError]: {
475513
nextStates: [AutoUpdateManagerState.UserPromptedManualCheck],
476514
enter: (_updateManager, _fromState, error) => {
@@ -586,6 +624,11 @@ type AutoUpdateResponse =
586624
| {
587625
available: false;
588626
reason?: never;
627+
}
628+
| {
629+
available: false;
630+
reason: 'outdated-operating-system';
631+
expectedVersion: string;
589632
};
590633

591634
const emitter = new EventEmitter();
@@ -618,15 +661,22 @@ class CompassAutoUpdateManager {
618661
os_release: release,
619662
os_linux_dist,
620663
os_linux_release,
664+
os_darwin_product_version,
621665
} = await getOsInfo();
622666
const url = new URL(
623667
`${endpoint}/api/v2/update/${product}/${channel}/${platform}-${arch}/${version}/check`
624668
);
625669

626-
release && url.searchParams.set('release', release);
627-
os_linux_dist && url.searchParams.set('os_linux_dist', os_linux_dist);
628-
os_linux_release &&
629-
url.searchParams.set('os_linux_release', os_linux_release);
670+
for (const [key, value] of Object.entries({
671+
release,
672+
os_linux_dist,
673+
os_linux_release,
674+
os_darwin_product_version,
675+
})) {
676+
if (typeof value === 'string') {
677+
url.searchParams.set(key, value);
678+
}
679+
}
630680

631681
return url;
632682
}
@@ -635,7 +685,44 @@ class CompassAutoUpdateManager {
635685
try {
636686
const response = await this.fetch((await this.getUpdateCheckURL()).href);
637687

638-
if (response.status !== 200) {
688+
if (response.status === 426) {
689+
try {
690+
const json = await response.json();
691+
assert(
692+
typeof json === 'object' && json !== null,
693+
'Expected response to be an object'
694+
);
695+
if ('reason' in json && json.reason === 'outdated-operating-system') {
696+
assert(
697+
'expectedVersion' in json,
698+
"Expected 'expectedVersion' in response"
699+
);
700+
const { expectedVersion } = json;
701+
assert(
702+
typeof expectedVersion === 'string',
703+
"Expected 'expectedVersion' in response"
704+
);
705+
return {
706+
available: false,
707+
reason: 'outdated-operating-system',
708+
expectedVersion,
709+
};
710+
} else {
711+
// Some future reason that no update is available
712+
return {
713+
available: false,
714+
};
715+
}
716+
} catch (err) {
717+
log.warn(
718+
mongoLogId(1_001_000_347),
719+
'AutoUpdateManager',
720+
'Failed to parse HTTP 426 (Upgrade Required) response',
721+
{ error: err instanceof Error ? err.message : 'Unknown error' }
722+
);
723+
return { available: false };
724+
}
725+
} else if (response.status !== 200) {
639726
return { available: false };
640727
}
641728

0 commit comments

Comments
 (0)