Skip to content

Commit 5b79320

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
do not try to restore temp sketches.
Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 1da2dfc commit 5b79320

File tree

8 files changed

+148
-64
lines changed

8 files changed

+148
-64
lines changed

arduino-ide-extension/src/browser/contributions/save-as-sketch.ts

+9-8
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,17 @@ export class SaveAsSketch extends SketchContribution {
104104
await this.saveOntoCopiedSketch(sketch.mainFileUri, sketch.uri, workspaceUri);
105105
}
106106
if (workspaceUri && openAfterMove) {
107+
this.windowService.setSafeToShutDown();
107108
if (wipeOriginal || (openAfterMove && execOnlyIfTemp)) {
108-
try {
109-
await this.fileService.delete(new URI(sketch.uri), {
110-
recursive: true,
111-
});
112-
} catch {
113-
/* NOOP: from time to time, it's not possible to wipe the old resource from the temp dir on Windows */
114-
}
109+
// This window will navigate away.
110+
// Explicitly stop the contribution to dispose the file watcher before deleting the temp sketch.
111+
// Otherwise, users might see irrelevant _Unable to watch for file changes in this large workspace._ notification.
112+
// https://github.com/arduino/arduino-ide/issues/39.
113+
this.sketchServiceClient.onStop();
114+
// TODO: consider implementing the temp sketch deletion the following way:
115+
// Open the other sketch with a `delete the temp sketch` startup-task.
116+
this.sketchService.notifyDeleteSketch(sketch); // This is a notification and will execute on the backend.
115117
}
116-
this.windowService.setSafeToShutDown();
117118
this.workspaceService.open(new URI(workspaceUri), {
118119
preserveWindow: true,
119120
});

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

+5
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ export interface SketchesService {
9595
* Based on https://github.com/arduino/arduino-cli/blob/550179eefd2d2bca299d50a4af9e9bfcfebec649/arduino/builder/builder.go#L30-L38
9696
*/
9797
getIdeTempFolderUri(sketch: Sketch): Promise<string>;
98+
99+
/**
100+
* Notifies the backend to recursively delete the sketch folder with all its content.
101+
*/
102+
notifyDeleteSketch(sketch: Sketch): void;
98103
}
99104

100105
export interface SketchRef {

arduino-ide-extension/src/electron-main/arduino-electron-main-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
ElectronMainWindowServiceExt,
1717
electronMainWindowServiceExtPath,
1818
} from '../electron-common/electron-main-window-service-ext';
19+
import { IsTempSketch } from '../node/is-temp-sketch';
1920
import { ElectronMainWindowServiceExtImpl } from './electron-main-window-service-ext-impl';
2021
import { IDEUpdaterImpl } from './ide-updater/ide-updater-impl';
2122
import { ElectronMainApplication } from './theia/electron-main-application';
@@ -62,4 +63,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
6263
)
6364
)
6465
.inSingletonScope();
66+
67+
bind(IsTempSketch).toSelf().inSingletonScope();
6568
});

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

+19-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { injectable } from '@theia/core/shared/inversify';
1+
import { inject, injectable } from '@theia/core/shared/inversify';
22
import {
33
app,
44
BrowserWindow,
@@ -22,6 +22,7 @@ import { Deferred } from '@theia/core/lib/common/promise-util';
2222
import * as os from '@theia/core/lib/common/os';
2323
import { Restart } from '@theia/core/lib/electron-common/messaging/electron-messages';
2424
import { TheiaBrowserWindowOptions } from '@theia/core/lib/electron-main/theia-electron-window';
25+
import { IsTempSketch } from '../../node/is-temp-sketch';
2526

2627
app.commandLine.appendSwitch('disable-http-cache');
2728

@@ -54,6 +55,8 @@ const APP_STARTED_WITH_CONTENT_TRACE =
5455

5556
@injectable()
5657
export class ElectronMainApplication extends TheiaElectronMainApplication {
58+
@inject(IsTempSketch)
59+
private readonly isTempSketch: IsTempSketch;
5760
private startup = false;
5861
private _firstWindowId: number | undefined;
5962
private openFilePromise = new Deferred();
@@ -176,6 +179,12 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
176179
);
177180
for (const workspace of workspaces) {
178181
if (await this.isValidSketchPath(workspace.file)) {
182+
if (this.isTempSketch.is(workspace.file)) {
183+
console.info(
184+
`Skipped opening sketch. The sketch was detected as temporary. Workspace path: ${workspace.file}.`
185+
);
186+
continue;
187+
}
179188
useDefault = false;
180189
await this.openSketch(workspace);
181190
}
@@ -405,6 +414,15 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
405414
const workspaceUri = URI.file(workspace);
406415
const bounds = window.getNormalBounds();
407416
const now = Date.now();
417+
// Do not try to reopen the sketch if it was temp.
418+
// Unfortunately, IDE2 has two different logic of restoring recent sketches: the Theia default `recentworkspace.json` and there is the `recent-sketches.json`.
419+
const file = workspaceUri.fsPath;
420+
if (this.isTempSketch.is(file)) {
421+
console.info(
422+
`Ignored marking workspace as a closed sketch. The sketch was detected as temporary. Workspace URI: ${workspaceUri.toString()}.`
423+
);
424+
return;
425+
}
408426
console.log(
409427
`Marking workspace as a closed sketch. Workspace URI: ${workspaceUri.toString()}. Date: ${now}.`
410428
);

arduino-ide-extension/src/node/arduino-ide-backend-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ import {
109109
SurveyNotificationService,
110110
SurveyNotificationServicePath,
111111
} from '../common/protocol/survey-service';
112+
import { IsTempSketch } from './is-temp-sketch';
112113

113114
export default new ContainerModule((bind, unbind, isBound, rebind) => {
114115
bind(BackendApplication).toSelf().inSingletonScope();
@@ -419,4 +420,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => {
419420
)
420421
)
421422
.inSingletonScope();
423+
424+
bind(IsTempSketch).toSelf().inSingletonScope();
422425
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as fs from 'fs';
2+
import * as tempDir from 'temp-dir';
3+
import { isWindows, isOSX } from '@theia/core/lib/common/os';
4+
import { injectable } from '@theia/core/shared/inversify';
5+
import { firstToLowerCase } from '../common/utils';
6+
7+
const Win32DriveRegex = /^[a-zA-Z]:\\/;
8+
export const TempSketchPrefix = '.arduinoIDE-unsaved';
9+
10+
@injectable()
11+
export class IsTempSketch {
12+
// If on macOS, the `temp-dir` lib will make sure there is resolved realpath.
13+
// If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `C:\Users\kittaakos\AppData\Local\Temp`.
14+
// Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`.
15+
// https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992
16+
private readonly tempDirRealpath = isOSX
17+
? tempDir
18+
: maybeNormalizeDrive(fs.realpathSync.native(tempDir));
19+
20+
is(sketchPath: string): boolean {
21+
// Consider the following paths:
22+
// macOS:
23+
// - Temp folder: /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T
24+
// - Sketch folder: /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925
25+
// Windows:
26+
// - Temp folder: C:\Users\KITTAA~1\AppData\Local\Temp
27+
// - Sketch folder: c:\Users\kittaakos\AppData\Local\Temp\.arduinoIDE-unsaved2022431-21824-116kfaz.9ljl\sketch_may31a
28+
// Both sketches are valid and temp, but this function will give a false-negative result if we use the default `os.tmpdir()` logic.
29+
const normalizedSketchPath = maybeNormalizeDrive(sketchPath);
30+
const result =
31+
normalizedSketchPath.startsWith(this.tempDirRealpath) &&
32+
normalizedSketchPath.includes(TempSketchPrefix);
33+
console.debug(`isTempSketch: ${result}. Input was ${normalizedSketchPath}`);
34+
return result;
35+
}
36+
}
37+
38+
/**
39+
* If on Windows, will change the input `C:\\path\\to\\somewhere` to `c:\\path\\to\\somewhere`.
40+
* Otherwise, returns with the argument.
41+
*/
42+
export function maybeNormalizeDrive(fsPath: string): string {
43+
if (isWindows && Win32DriveRegex.test(fsPath)) {
44+
return firstToLowerCase(fsPath);
45+
}
46+
return fsPath;
47+
}

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

+25-42
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@ import { injectable, inject } from '@theia/core/shared/inversify';
22
import * as fs from 'fs';
33
import * as os from 'os';
44
import * as temp from 'temp';
5-
import * as tempDir from 'temp-dir';
5+
66
import * as path from 'path';
77
import * as crypto from 'crypto';
88
import { ncp } from 'ncp';
99
import { promisify } from 'util';
1010
import URI from '@theia/core/lib/common/uri';
1111
import { FileUri } from '@theia/core/lib/node';
12-
import { isWindows, isOSX } from '@theia/core/lib/common/os';
1312
import { ConfigServiceImpl } from './config-service-impl';
1413
import {
1514
SketchesService,
@@ -18,7 +17,6 @@ import {
1817
SketchContainer,
1918
SketchesError,
2019
} from '../common/protocol/sketches-service';
21-
import { firstToLowerCase } from '../common/utils';
2220
import { NotificationServiceServerImpl } from './notification-service-server';
2321
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
2422
import { CoreClientAware } from './core-client-provider';
@@ -30,10 +28,11 @@ import { duration } from '../common/decorators';
3028
import * as glob from 'glob';
3129
import { Deferred } from '@theia/core/lib/common/promise-util';
3230
import { ServiceError } from './service-error';
33-
34-
const WIN32_DRIVE_REGEXP = /^[a-zA-Z]:\\/;
35-
36-
const prefix = '.arduinoIDE-unsaved';
31+
import {
32+
IsTempSketch,
33+
maybeNormalizeDrive,
34+
TempSketchPrefix,
35+
} from './is-temp-sketch';
3736

3837
@injectable()
3938
export class SketchesServiceImpl
@@ -42,22 +41,18 @@ export class SketchesServiceImpl
4241
{
4342
private sketchSuffixIndex = 1;
4443
private lastSketchBaseName: string;
45-
// If on macOS, the `temp-dir` lib will make sure there is resolved realpath.
46-
// If on Windows, the `C:\Users\KITTAA~1\AppData\Local\Temp` path will be resolved and normalized to `C:\Users\kittaakos\AppData\Local\Temp`.
47-
// Note: VS Code URI normalizes the drive letter. `C:` will be converted into `c:`.
48-
// https://github.com/Microsoft/vscode/issues/68325#issuecomment-462239992
49-
private tempDirRealpath = isOSX
50-
? tempDir
51-
: maybeNormalizeDrive(fs.realpathSync.native(tempDir));
5244

5345
@inject(ConfigServiceImpl)
54-
protected readonly configService: ConfigServiceImpl;
46+
private readonly configService: ConfigServiceImpl;
5547

5648
@inject(NotificationServiceServerImpl)
57-
protected readonly notificationService: NotificationServiceServerImpl;
49+
private readonly notificationService: NotificationServiceServerImpl;
5850

5951
@inject(EnvVariablesServer)
60-
protected readonly envVariableServer: EnvVariablesServer;
52+
private readonly envVariableServer: EnvVariablesServer;
53+
54+
@inject(IsTempSketch)
55+
private readonly isTempSketch: IsTempSketch;
6156

6257
async getSketches({
6358
uri,
@@ -424,7 +419,7 @@ void loop() {
424419
*/
425420
private createTempFolder(): Promise<string> {
426421
return new Promise<string>((resolve, reject) => {
427-
temp.mkdir({ prefix }, (createError, dirPath) => {
422+
temp.mkdir({ prefix: TempSketchPrefix }, (createError, dirPath) => {
428423
if (createError) {
429424
reject(createError);
430425
return;
@@ -475,20 +470,7 @@ void loop() {
475470
}
476471

477472
async isTemp(sketch: SketchRef): Promise<boolean> {
478-
// Consider the following paths:
479-
// macOS:
480-
// - Temp folder: /var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T
481-
// - Sketch folder: /private/var/folders/k3/d2fkvv1j16v3_rz93k7f74180000gn/T/arduino-ide2-A0337D47F86B24A51DF3DBCF2CC17925
482-
// Windows:
483-
// - Temp folder: C:\Users\KITTAA~1\AppData\Local\Temp
484-
// - Sketch folder: c:\Users\kittaakos\AppData\Local\Temp\.arduinoIDE-unsaved2022431-21824-116kfaz.9ljl\sketch_may31a
485-
// Both sketches are valid and temp, but this function will give a false-negative result if we use the default `os.tmpdir()` logic.
486-
const sketchPath = maybeNormalizeDrive(FileUri.fsPath(sketch.uri));
487-
const tempPath = this.tempDirRealpath; // https://github.com/sindresorhus/temp-dir
488-
const result =
489-
sketchPath.indexOf(prefix) !== -1 && sketchPath.startsWith(tempPath);
490-
console.log('isTemp?', result, sketch.uri);
491-
return result;
473+
return this.isTempSketch.is(FileUri.fsPath(sketch.uri));
492474
}
493475

494476
async copy(
@@ -578,6 +560,17 @@ void loop() {
578560
const suffix = crypto.createHash('md5').update(sketchPath).digest('hex');
579561
return path.join(os.tmpdir(), `arduino-ide2-${suffix}`);
580562
}
563+
564+
notifyDeleteSketch(sketch: Sketch): void {
565+
const sketchPath = FileUri.fsPath(sketch.uri);
566+
fs.rm(sketchPath, { recursive: true, maxRetries: 5 }, (error) => {
567+
if (error) {
568+
console.error(`Failed to delete sketch at ${sketchPath}.`, error);
569+
} else {
570+
console.error(`Successfully delete sketch at ${sketchPath}.`);
571+
}
572+
});
573+
}
581574
}
582575

583576
interface SketchWithDetails extends Sketch {
@@ -618,16 +611,6 @@ function isNotFoundError(err: unknown): err is ServiceError {
618611
return ServiceError.is(err) && err.code === 5; // `NOT_FOUND` https://grpc.github.io/grpc/core/md_doc_statuscodes.html
619612
}
620613

621-
/**
622-
* If on Windows, will change the input `C:\\path\\to\\somewhere` to `c:\\path\\to\\somewhere`.
623-
*/
624-
function maybeNormalizeDrive(input: string): string {
625-
if (isWindows && WIN32_DRIVE_REGEXP.test(input)) {
626-
return firstToLowerCase(input);
627-
}
628-
return input;
629-
}
630-
631614
/*
632615
* When a new sketch is created, add a suffix to distinguish it
633616
* from other new sketches I created today.

arduino-ide-extension/src/node/theia/workspace/default-workspace-server.ts

+37-13
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,16 @@
11
import { promises as fs, constants } from 'fs';
22
import { injectable, inject } from '@theia/core/shared/inversify';
3-
import { ILogger } from '@theia/core/lib/common/logger';
43
import { DefaultWorkspaceServer as TheiaDefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server';
5-
import { ConfigService } from '../../../common/protocol/config-service';
64
import { SketchesService } from '../../../common/protocol';
75
import { FileUri } from '@theia/core/lib/node';
6+
import { IsTempSketch } from '../../is-temp-sketch';
87

98
@injectable()
109
export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
11-
@inject(ConfigService)
12-
protected readonly configService: ConfigService;
13-
14-
@inject(ILogger)
15-
protected readonly logger: ILogger;
16-
1710
@inject(SketchesService)
1811
private readonly sketchesService: SketchesService;
19-
20-
override async onStart(): Promise<void> {
21-
// NOOP
22-
// No need to remove untitled workspaces. IDE2 does not use workspaces.
23-
}
12+
@inject(IsTempSketch)
13+
private readonly isTempSketch: IsTempSketch;
2414

2515
override async getMostRecentlyUsedWorkspace(): Promise<string | undefined> {
2616
const uri = await super.getMostRecentlyUsedWorkspace();
@@ -51,6 +41,35 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
5141
return listUri;
5242
}
5343

44+
protected override async writeToUserHome(
45+
data: RecentWorkspacePathsData
46+
): Promise<void> {
47+
return super.writeToUserHome(this.filterTempSketches(data));
48+
}
49+
50+
protected override async readRecentWorkspacePathsFromUserHome(): Promise<
51+
RecentWorkspacePathsData | undefined
52+
> {
53+
const data = await super.readRecentWorkspacePathsFromUserHome();
54+
return data ? this.filterTempSketches(data) : undefined;
55+
}
56+
57+
protected override async removeOldUntitledWorkspaces(): Promise<void> {
58+
// NOOP
59+
// No need to remove untitled workspaces. IDE2 does not use workspaces.
60+
}
61+
62+
private filterTempSketches(
63+
data: RecentWorkspacePathsData
64+
): RecentWorkspacePathsData {
65+
const recentRoots = data.recentRoots.filter(
66+
(uri) => !this.isTempSketch.is(FileUri.fsPath(uri))
67+
);
68+
return {
69+
recentRoots,
70+
};
71+
}
72+
5473
private async exists(uri: string): Promise<boolean> {
5574
try {
5675
await fs.access(FileUri.fsPath(uri), constants.R_OK | constants.W_OK);
@@ -60,3 +79,8 @@ export class DefaultWorkspaceServer extends TheiaDefaultWorkspaceServer {
6079
}
6180
}
6281
}
82+
83+
// Remove after https://github.com/eclipse-theia/theia/pull/11603
84+
interface RecentWorkspacePathsData {
85+
recentRoots: string[];
86+
}

0 commit comments

Comments
 (0)