Skip to content

Commit 743c9bf

Browse files
author
Akos Kitta
committed
fix: backend app shutdown hook is not called
Signed-off-by: Akos Kitta <[email protected]>
1 parent 31cc604 commit 743c9bf

File tree

7 files changed

+118
-104
lines changed

7 files changed

+118
-104
lines changed

Diff for: .vscode/launch.json

-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
".",
1515
"--log-level=debug",
1616
"--hostname=localhost",
17-
"--no-cluster",
1817
"--app-project-path=${workspaceRoot}/electron-app",
1918
"--remote-debugging-port=9222",
2019
"--no-app-auto-install",
@@ -52,7 +51,6 @@
5251
".",
5352
"--log-level=debug",
5453
"--hostname=localhost",
55-
"--no-cluster",
5654
"--app-project-path=${workspaceRoot}/electron-app",
5755
"--remote-debugging-port=9222",
5856
"--no-app-auto-install",

Diff for: arduino-ide-extension/src/browser/contributions/delete-sketch.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as remote from '@theia/core/electron-shared/@electron/remote';
2+
import { ipcRenderer } from '@theia/core/electron-shared/electron';
23
import { Dialog } from '@theia/core/lib/browser/dialogs';
34
import { NavigatableWidget } from '@theia/core/lib/browser/navigatable-types';
45
import { ApplicationShell } from '@theia/core/lib/browser/shell/application-shell';
@@ -9,6 +10,7 @@ import URI from '@theia/core/lib/common/uri';
910
import type { Widget } from '@theia/core/shared/@phosphor/widgets';
1011
import { inject, injectable } from '@theia/core/shared/inversify';
1112
import { SketchesError } from '../../common/protocol';
13+
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
1214
import { Sketch } from '../contributions/contribution';
1315
import { isNotFound } from '../create/typings';
1416
import { Command, CommandRegistry } from './contribution';
@@ -59,7 +61,8 @@ export class DeleteSketch extends CloudSketchContribution {
5961
sketch = toDelete;
6062
}
6163
if (!willNavigateAway) {
62-
return this.sketchesService.deleteSketch(sketch);
64+
this.scheduleDeletion(sketch);
65+
return;
6366
}
6467
const cloudUri = this.createFeatures.cloudUri(sketch);
6568
if (willNavigateAway !== 'force') {
@@ -112,10 +115,14 @@ export class DeleteSketch extends CloudSketchContribution {
112115
),
113116
]);
114117
this.windowService.setSafeToShutDown();
115-
this.sketchesService.deleteSketch(sketch);
118+
this.scheduleDeletion(sketch);
116119
return window.close();
117120
}
118121

122+
private scheduleDeletion(sketch: Sketch): void {
123+
ipcRenderer.send(SCHEDULE_DELETION_SIGNAL, sketch);
124+
}
125+
119126
private async loadSketch(uri: string): Promise<Sketch | undefined> {
120127
try {
121128
const sketch = await this.sketchesService.loadSketch(uri);

Diff for: arduino-ide-extension/src/common/protocol/sketches-service.ts

-5
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,6 @@ export interface SketchesService {
107107
*/
108108
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
109109

110-
/**
111-
* Recursively deletes the sketch folder with all its content.
112-
*/
113-
deleteSketch(sketch: Sketch): Promise<void>;
114-
115110
/**
116111
* This is the JS/TS re-implementation of [`GenBuildPath`](https://github.com/arduino/arduino-cli/blob/c0d4e4407d80aabad81142693513b3306759cfa6/arduino/sketch/sketch.go#L296-L306) of the CLI.
117112
* Pass in a sketch and get the build temporary folder filesystem path calculated from the main sketch file location. Can be multiple ones. This method does not check the existence of the sketch.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export const SCHEDULE_DELETION_SIGNAL = 'arduino/scheduleDeletion';

Diff for: arduino-ide-extension/src/electron-main/theia/electron-main-application.ts

+106-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { fork } from 'child_process';
1010
import { AddressInfo } from 'net';
1111
import { join, isAbsolute, resolve } from 'path';
12-
import { promises as fs } from 'fs';
12+
import { promises as fs, rm, rmSync } from 'fs';
1313
import { MaybePromise } from '@theia/core/lib/common/types';
1414
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
1515
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
@@ -29,6 +29,13 @@ import {
2929
} from '../../common/ipc-communication';
3030
import { ErrnoException } from '../../node/utils/errors';
3131
import { isAccessibleSketchPath } from '../../node/sketches-service-impl';
32+
import { SCHEDULE_DELETION_SIGNAL } from '../../electron-common/electron-messages';
33+
import { FileUri } from '@theia/core/lib/node/file-uri';
34+
import {
35+
Disposable,
36+
DisposableCollection,
37+
} from '@theia/core/lib/common/disposable';
38+
import { Sketch } from '../../common/protocol';
3239

3340
app.commandLine.appendSwitch('disable-http-cache');
3441

@@ -66,6 +73,34 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
6673
private startup = false;
6774
private _firstWindowId: number | undefined;
6875
private openFilePromise = new Deferred();
76+
/**
77+
* It contains all things the IDE2 must clean up before a normal stop.
78+
*
79+
* When deleting the sketch, the IDE2 must close the browser window and
80+
* recursively delete the sketch folder from the filesystem. The sketch
81+
* cannot be deleted when the window is open because that is the currently
82+
* opened workspace. IDE2 cannot delete the sketch folder from the
83+
* filesystem after closing the browser window because the window can be
84+
* the last, and when the last window closes, the application quits.
85+
* There is no way to clean up the undesired resources.
86+
*
87+
* This array contains disposable instances wrapping synchronous sketch
88+
* delete operations. When IDE2 closes the browser window, it schedules
89+
* the sketch deletion, and the window closes.
90+
*
91+
* When IDE2 schedules a sketch for deletion, it creates a synchronous
92+
* folder deletion as a disposable instance and pushes it into this
93+
* array. After the push, IDE2 starts the sketch deletion in an
94+
* asynchronous way. When the deletion completes, the disposable is
95+
* removed. If the app quits when the asynchronous deletion is still in
96+
* progress, it disposes the elements of this array. Since it is
97+
* synchronous, it is [ensured by Theia](https://github.com/eclipse-theia/theia/blob/678e335644f1b38cb27522cc27a3b8209293cf31/packages/core/src/node/backend-application.ts#L91-L97)
98+
* that IDE2 won't quit before the cleanup is done. It works only in normal
99+
* quit.
100+
*/
101+
// TODO: Why is it here and not in the Theia backend?
102+
// https://github.com/eclipse-theia/theia/discussions/12135
103+
private readonly scheduledDeletions: Disposable[] = [];
69104

70105
override async start(config: FrontendApplicationConfig): Promise<void> {
71106
// Explicitly set the app name to have better menu items on macOS. ("About", "Hide", and "Quit")
@@ -309,6 +344,13 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
309344
ipcMain.on(Restart, ({ sender }) => {
310345
this.restart(sender.id);
311346
});
347+
ipcMain.on(SCHEDULE_DELETION_SIGNAL, (event, sketch: unknown) => {
348+
if (Sketch.is(sketch)) {
349+
console.log(`Sketch ${sketch.uri} was scheduled for deletion`);
350+
// TODO: remove deleted sketch from closedWorkspaces?
351+
this.delete(sketch);
352+
}
353+
});
312354
}
313355

314356
protected override async onSecondInstance(
@@ -511,6 +553,16 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
511553
`Stored workspaces roots: ${workspaces.map(({ file }) => file)}`
512554
);
513555

556+
if (this.scheduledDeletions.length) {
557+
console.log(
558+
'>>> Finishing scheduled sketch deletions before app quit...'
559+
);
560+
new DisposableCollection(...this.scheduledDeletions).dispose();
561+
console.log('<<< Successfully finishing scheduled sketch deletions.');
562+
} else {
563+
console.log('No sketches were scheduled for deletion.');
564+
}
565+
514566
super.onWillQuit(event);
515567
}
516568

@@ -521,6 +573,59 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
521573
get firstWindowId(): number | undefined {
522574
return this._firstWindowId;
523575
}
576+
577+
private async delete(sketch: Sketch): Promise<void> {
578+
const sketchPath = FileUri.fsPath(sketch.uri);
579+
const disposable = Disposable.create(() => {
580+
try {
581+
this.deleteSync(sketchPath);
582+
} catch (err) {
583+
console.error(
584+
`Could not delete sketch ${sketchPath} on app quit.`,
585+
err
586+
);
587+
}
588+
});
589+
this.scheduledDeletions.push(disposable);
590+
return new Promise<void>((resolve, reject) => {
591+
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
592+
if (error) {
593+
console.error(`Failed to delete sketch ${sketchPath}`, error);
594+
reject(error);
595+
} else {
596+
console.info(`Successfully deleted sketch ${sketchPath}`);
597+
resolve();
598+
const index = this.scheduledDeletions.indexOf(disposable);
599+
if (index >= 0) {
600+
this.scheduledDeletions.splice(index, 1);
601+
console.info(
602+
`Successfully completed the scheduled sketch deletion: ${sketchPath}`
603+
);
604+
} else {
605+
console.warn(
606+
`Could not find the scheduled sketch deletion: ${sketchPath}`
607+
);
608+
}
609+
}
610+
});
611+
});
612+
}
613+
614+
private deleteSync(sketchPath: string): void {
615+
console.info(
616+
`>>> Running sketch deletion ${sketchPath} before app quit...`
617+
);
618+
try {
619+
rmSync(sketchPath, { recursive: true, maxRetries: 5 });
620+
console.info(`<<< Deleted sketch ${sketchPath}`);
621+
} catch (err) {
622+
if (!ErrnoException.isENOENT(err)) {
623+
throw err;
624+
} else {
625+
console.info(`<<< Sketch ${sketchPath} did not exist.`);
626+
}
627+
}
628+
}
524629
}
525630

526631
class InterruptWorkspaceRestoreError extends Error {

Diff for: arduino-ide-extension/src/node/arduino-ide-backend-module.ts

-1
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
196196
// Shared sketches service
197197
bind(SketchesServiceImpl).toSelf().inSingletonScope();
198198
bind(SketchesService).toService(SketchesServiceImpl);
199-
bind(BackendApplicationContribution).toService(SketchesServiceImpl);
200199
bind(ConnectionHandler)
201200
.toDynamicValue(
202201
(context) =>

Diff for: arduino-ide-extension/src/node/sketches-service-impl.ts

+2-93
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
import { injectable, inject, named } from '@theia/core/shared/inversify';
2-
import {
3-
promises as fs,
4-
realpath,
5-
lstat,
6-
Stats,
7-
constants,
8-
rm,
9-
rmSync,
10-
} from 'fs';
2+
import { promises as fs, realpath, lstat, Stats, constants } from 'fs';
113
import * as os from 'os';
124
import * as temp from 'temp';
135
import * as path from 'path';
@@ -51,11 +43,6 @@ import {
5143
firstToUpperCase,
5244
startsWithUpperCase,
5345
} from '../common/utils';
54-
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
55-
import {
56-
Disposable,
57-
DisposableCollection,
58-
} from '@theia/core/lib/common/disposable';
5946

6047
const RecentSketches = 'recent-sketches.json';
6148
const DefaultIno = `void setup() {
@@ -72,7 +59,7 @@ void loop() {
7259
@injectable()
7360
export class SketchesServiceImpl
7461
extends CoreClientAware
75-
implements SketchesService, BackendApplicationContribution
62+
implements SketchesService
7663
{
7764
private sketchSuffixIndex = 1;
7865
private lastSketchBaseName: string;
@@ -82,32 +69,6 @@ export class SketchesServiceImpl
8269
concurrency: 1,
8370
});
8471
private inoContent: Deferred<string> | undefined;
85-
/**
86-
* It contains all things the IDE2 must clean up before a normal stop.
87-
*
88-
* When deleting the sketch, the IDE2 must close the browser window and
89-
* recursively delete the sketch folder from the filesystem. The sketch
90-
* cannot be deleted when the window is open because that is the currently
91-
* opened workspace. IDE2 cannot delete the sketch folder from the
92-
* filesystem after closing the browser window because the window can be
93-
* the last, and when the last window closes, the application quits.
94-
* There is no way to clean up the undesired resources.
95-
*
96-
* This array contains disposable instances wrapping synchronous sketch
97-
* delete operations. When IDE2 closes the browser window, it schedules
98-
* the sketch deletion, and the window closes.
99-
*
100-
* When IDE2 schedules a sketch for deletion, it creates a synchronous
101-
* folder deletion as a disposable instance and pushes it into this
102-
* array. After the push, IDE2 starts the sketch deletion in an
103-
* asynchronous way. When the deletion completes, the disposable is
104-
* removed. If the app quits when the asynchronous deletion is still in
105-
* progress, it disposes the elements of this array. Since it is
106-
* synchronous, it is [ensured by Theia](https://github.com/eclipse-theia/theia/blob/678e335644f1b38cb27522cc27a3b8209293cf31/packages/core/src/node/backend-application.ts#L91-L97)
107-
* that IDE2 won't quit before the cleanup is done. It works only in normal
108-
* quit.
109-
*/
110-
private readonly scheduledDeletions: Disposable[] = [];
11172

11273
@inject(ILogger)
11374
@named('sketches-service')
@@ -125,14 +86,6 @@ export class SketchesServiceImpl
12586
@inject(IsTempSketch)
12687
private readonly isTempSketch: IsTempSketch;
12788

128-
onStop(): void {
129-
if (this.scheduledDeletions.length) {
130-
this.logger.info(`>>> Disposing sketches service...`);
131-
new DisposableCollection(...this.scheduledDeletions).dispose();
132-
this.logger.info(`<<< Disposed sketches service.`);
133-
}
134-
}
135-
13689
async getSketches({ uri }: { uri?: string }): Promise<SketchContainer> {
13790
const root = await this.root(uri);
13891
if (!root) {
@@ -679,50 +632,6 @@ export class SketchesServiceImpl
679632
return folderName;
680633
}
681634

682-
async deleteSketch(sketch: Sketch): Promise<void> {
683-
const sketchPath = FileUri.fsPath(sketch.uri);
684-
const disposable = Disposable.create(() =>
685-
this.deleteSketchSync(sketchPath)
686-
);
687-
this.scheduledDeletions.push(disposable);
688-
return new Promise<void>((resolve, reject) => {
689-
rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
690-
if (error) {
691-
this.logger.error(`Failed to delete sketch at ${sketchPath}.`, error);
692-
reject(error);
693-
} else {
694-
this.logger.info(`Successfully deleted sketch at ${sketchPath}.`);
695-
resolve();
696-
const index = this.scheduledDeletions.indexOf(disposable);
697-
if (index >= 0) {
698-
this.scheduledDeletions.splice(index, 1);
699-
this.logger.info(
700-
`Removed the successfully completed scheduled sketch deletion: ${sketchPath}`
701-
);
702-
} else {
703-
this.logger.warn(
704-
`Could not find the scheduled sketch deletion: ${sketchPath}`
705-
);
706-
}
707-
}
708-
});
709-
});
710-
}
711-
712-
private deleteSketchSync(sketchPath: string): void {
713-
this.logger.info(
714-
`>>> Running sketch deletion ${sketchPath} before app quit...`
715-
);
716-
try {
717-
rmSync(sketchPath, { recursive: true, maxRetries: 5 });
718-
this.logger.info(`<<< Deleted sketch ${sketchPath}.`);
719-
} catch (err) {
720-
if (!ErrnoException.isENOENT(err)) {
721-
throw err;
722-
}
723-
}
724-
}
725-
726635
// Returns the default.ino from the settings or from default folder.
727636
private async readSettings(): Promise<Record<string, unknown> | undefined> {
728637
const configDirUri = await this.envVariableServer.getConfigDirUri();

0 commit comments

Comments
 (0)