Skip to content

Commit 3634eab

Browse files
committed
feat: prevent Electron versions that are in use by some window from being removed
1 parent 11b54b9 commit 3634eab

File tree

2 files changed

+96
-2
lines changed

2 files changed

+96
-2
lines changed

src/renderer/components/settings-electron.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ interface ElectronSettingsProps {
3131
}
3232

3333
interface ElectronSettingsState {
34+
activeVersions: Set<string>;
3435
isDownloadingAll: boolean;
3536
isDeletingAll: boolean;
3637
}
@@ -60,11 +61,47 @@ export const ElectronSettings = observer(
6061
this.handleStateChange = this.handleStateChange.bind(this);
6162

6263
this.state = {
64+
activeVersions: new Set<string>([
65+
props.appState.getVersionLockName(props.appState.version),
66+
]),
6367
isDownloadingAll: false,
6468
isDeletingAll: false,
6569
};
6670
}
6771

72+
/**
73+
* Queries the currently active versions and update the local state.
74+
*
75+
* This currently gives a warning/error in development mode when the ElectronSettings component is unmounted
76+
* ("Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak
77+
* in your application"). This is a false positive, the warning has been removed in React 18+ (see
78+
* https://github.com/facebook/react/pull/22114).
79+
*
80+
* @TODO upgrade to React 18
81+
*/
82+
updateActiveVersions = () => {
83+
this.props.appState
84+
.getActiveVersions()
85+
.then((activeVersions) =>
86+
this.setState({ ...this.state, activeVersions }),
87+
)
88+
.catch((err) => {
89+
console.error(
90+
'Error updating the currently active Electron versions:',
91+
);
92+
console.error(err);
93+
});
94+
};
95+
96+
public componentDidMount() {
97+
this.updateActiveVersions();
98+
}
99+
100+
// Fired when other windows change their active Electron version
101+
public componentDidUpdate() {
102+
this.updateActiveVersions();
103+
}
104+
68105
public handleUpdateElectronVersions() {
69106
this.props.appState.updateElectronVersions();
70107
}
@@ -204,6 +241,7 @@ export const ElectronSettings = observer(
204241
</ButtonGroup>
205242
);
206243
}
244+
207245
private filterSection(): JSX.Element {
208246
const { appState } = this.props;
209247
return (
@@ -390,7 +428,7 @@ export const ElectronSettings = observer(
390428
break;
391429
}
392430

393-
if (version === appState.currentElectronVersion.version) {
431+
if (this.state.activeVersions.has(appState.getVersionLockName(version))) {
394432
return (
395433
<Tooltip
396434
position="auto"

src/renderer/state.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,34 @@ export class AppState {
207207
versions: this.baseVersions,
208208
});
209209

210+
// Lock on the active Electron version that prevents other windows from removing it
211+
private versionLock: Lock | null = null;
212+
213+
// Used to release the lock when the current window switches Electron versions
214+
private versionLockController = new AbortController();
215+
216+
private static versionLockNamePrefix = 'version:';
217+
218+
public getVersionLockName(ver: string) {
219+
return [AppState.versionLockNamePrefix, ver].join('');
220+
}
221+
222+
/**
223+
* Retrieves all Electron versions that are currently active in some window.
224+
*/
225+
public async getActiveVersions(): Promise<Set<string>> {
226+
return ((await navigator.locks.query()).held || []).reduce<Set<string>>(
227+
(acc, item) => {
228+
if (item.name?.startsWith(AppState.versionLockNamePrefix)) {
229+
acc.add(item.name);
230+
}
231+
232+
return acc;
233+
},
234+
new Set(),
235+
);
236+
}
237+
210238
constructor(versions: RunnableVersion[]) {
211239
makeObservable<AppState, 'setPageHash'>(this, {
212240
Bisector: observable,
@@ -700,7 +728,9 @@ export class AppState {
700728
public async removeVersion(ver: RunnableVersion): Promise<void> {
701729
const { version, state, source } = ver;
702730

703-
if (ver === this.currentElectronVersion) {
731+
const activeVersions = await this.getActiveVersions();
732+
733+
if (activeVersions.has(this.getVersionLockName(ver.version))) {
704734
console.log(`State: Not removing active version ${version}`);
705735
return;
706736
}
@@ -851,10 +881,36 @@ export class AppState {
851881
return;
852882
}
853883

884+
if (this.versionLock) {
885+
console.log(`Releasing lock on version ${this.version}`);
886+
887+
// release the lock on the previous version
888+
this.versionLockController.abort();
889+
890+
// replace the spent AbortController
891+
this.versionLockController = new AbortController();
892+
}
893+
854894
const { version } = ver;
855895
console.log(`State: Switching to Electron ${version}`);
856896
this.version = version;
857897

898+
navigator.locks.request(
899+
this.getVersionLockName(version),
900+
{ mode: 'shared' },
901+
(lock) => {
902+
this.versionLock = lock;
903+
904+
/**
905+
* The lock is released when this promise resolves, so we keep it in the
906+
* pending state until our AbortController is aborted.
907+
*/
908+
return new Promise<void>((resolve) => {
909+
this.versionLockController.signal.onabort = () => resolve();
910+
});
911+
},
912+
);
913+
858914
// If there's no current fiddle,
859915
// or if the current fiddle is the previous version's template,
860916
// then load the new version's template.

0 commit comments

Comments
 (0)