Skip to content

Commit 1aa944b

Browse files
committed
More robust workspace initialization: guard against errors creating sketch dir
1 parent b220ce4 commit 1aa944b

12 files changed

+147
-72
lines changed

.vscode/launch.json

+9-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
55
"version": "0.2.0",
66
"configurations": [
7+
{
8+
"type": "node",
9+
"request": "attach",
10+
"name": "Attach by Process ID",
11+
"processId": "${command:PickProcess}"
12+
},
713
{
814
"type": "node",
915
"request": "launch",
@@ -33,7 +39,7 @@
3339
"${workspaceRoot}/electron-app/src-gen/backend/*.js",
3440
"${workspaceRoot}/electron-app/src-gen/frontend/*.js",
3541
"${workspaceRoot}/electron-app/lib/**/*.js",
36-
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
42+
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
3743
],
3844
"smartStep": true,
3945
"internalConsoleOptions": "openOnSessionStart",
@@ -63,7 +69,7 @@
6369
"outFiles": [
6470
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
6571
"${workspaceRoot}/browser-app/lib/**/*.js",
66-
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
72+
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
6773
],
6874
"smartStep": true,
6975
"internalConsoleOptions": "openOnSessionStart",
@@ -88,7 +94,7 @@
8894
"outFiles": [
8995
"${workspaceRoot}/browser-app/src-gen/backend/*.js",
9096
"${workspaceRoot}/browser-app/lib/**/*.js",
91-
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js"
97+
"${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
9298
],
9399
"smartStep": true,
94100
"internalConsoleOptions": "openOnSessionStart",

arduino-ide-extension/src/browser/arduino-frontend-module.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
198198
}).inSingletonScope();
199199

200200
bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
201-
rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope();
201+
rebind(WorkspaceService).toService(ArduinoWorkspaceService);
202202

203203
const themeService = ThemeService.get();
204204
themeService.register(...ArduinoTheme.themes);

arduino-ide-extension/src/browser/arduino-workspace-resolver.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export class ArduinoWorkspaceRootResolver {
4747
}
4848

4949
protected isValid(uri: string): MaybePromise<boolean> {
50-
return this.options.isValid.bind(this)(uri);
50+
return this.options.isValid(uri);
5151
}
5252

5353
// Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first.
@@ -59,10 +59,10 @@ export class ArduinoWorkspaceRootResolver {
5959
if (hash
6060
&& hash.length > 1
6161
&& hash.startsWith('#')) {
62-
const path = hash.slice(1); // Trim the leading `#`.
63-
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
62+
const path = hash.slice(1); // Trim the leading `#`.
63+
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
6464
}
6565
return undefined;
6666
}
6767

68-
}
68+
}

arduino-ide-extension/src/browser/arduino-workspace-service.ts

+38-19
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { injectable, inject } from 'inversify';
2+
import { MessageService } from '@theia/core';
23
import { LabelProvider } from '@theia/core/lib/browser';
34
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
45
import { ConfigService } from '../common/protocol/config-service';
@@ -21,26 +22,44 @@ export class ArduinoWorkspaceService extends WorkspaceService {
2122
@inject(EditorMode)
2223
protected readonly editorMode: EditorMode;
2324

24-
async getDefaultWorkspaceUri(): Promise<string | undefined> {
25-
const [hash, recentWorkspaces, recentSketches] = await Promise.all([
26-
window.location.hash,
27-
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)),
28-
this.server.getRecentWorkspaces()
29-
]);
30-
const toOpen = await new ArduinoWorkspaceRootResolver({
31-
isValid: this.isValid.bind(this)
32-
}).resolve({
33-
hash,
34-
recentWorkspaces,
35-
recentSketches
36-
});
37-
if (toOpen) {
38-
const { uri } = toOpen;
39-
await this.server.setMostRecentlyUsedWorkspace(uri);
40-
return toOpen.uri;
25+
@inject(MessageService)
26+
protected readonly messageService: MessageService;
27+
28+
private workspaceUri?: Promise<string | undefined>;
29+
30+
protected getDefaultWorkspaceUri(): Promise<string | undefined> {
31+
if (this.workspaceUri) {
32+
// Avoid creating a new sketch twice
33+
return this.workspaceUri;
4134
}
42-
const { sketchDirUri } = (await this.configService.getConfiguration());
43-
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
35+
this.workspaceUri = (async () => {
36+
try {
37+
const hash = window.location.hash;
38+
const [recentWorkspaces, recentSketches] = await Promise.all([
39+
this.server.getRecentWorkspaces(),
40+
this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri))
41+
]);
42+
const toOpen = await new ArduinoWorkspaceRootResolver({
43+
isValid: this.isValid.bind(this)
44+
}).resolve({ hash, recentWorkspaces, recentSketches });
45+
if (toOpen) {
46+
const { uri } = toOpen;
47+
await this.server.setMostRecentlyUsedWorkspace(uri);
48+
return toOpen.uri;
49+
}
50+
const { sketchDirUri } = (await this.configService.getConfiguration());
51+
this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`)
52+
return (await this.sketchService.createNewSketch(sketchDirUri)).uri;
53+
} catch (err) {
54+
this.logger.fatal(`Failed to determine the sketch directory: ${err}`)
55+
this.messageService.error(
56+
'There was an error creating the sketch directory. ' +
57+
'See the log for more details. ' +
58+
'The application will probably not work as expected.')
59+
return super.getDefaultWorkspaceUri();
60+
}
61+
})();
62+
return this.workspaceUri;
4463
}
4564

4665
private async isValid(uri: string): Promise<boolean> {

arduino-ide-extension/src/browser/markers/arduino-problem-manager.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inject, injectable, postConstruct } from 'inversify';
22
import { Diagnostic } from 'vscode-languageserver-types';
33
import URI from '@theia/core/lib/common/uri';
4+
import { ILogger } from '@theia/core';
45
import { Marker } from '@theia/markers/lib/common/marker';
56
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
67
import { ConfigService } from '../../common/protocol/config-service';
@@ -10,12 +11,18 @@ export class ArduinoProblemManager extends ProblemManager {
1011

1112
@inject(ConfigService)
1213
protected readonly configService: ConfigService;
14+
15+
@inject(ILogger)
16+
protected readonly logger: ILogger;
17+
1318
protected dataDirUri: URI | undefined;
1419

1520
@postConstruct()
1621
protected init(): void {
1722
super.init();
18-
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
23+
this.configService.getConfiguration()
24+
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
25+
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
1926
}
2027

2128
setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {

arduino-ide-extension/src/browser/shell/arduino-tab-bar-decorator.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inject, injectable, postConstruct } from 'inversify';
22
import URI from '@theia/core/lib/common/uri';
33
import { Title, Widget } from '@phosphor/widgets';
4+
import { ILogger } from '@theia/core';
45
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
56
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
67
import { ConfigService } from '../../common/protocol/config-service';
@@ -11,12 +12,19 @@ export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
1112

1213
@inject(ConfigService)
1314
protected readonly configService: ConfigService;
15+
16+
@inject(ILogger)
17+
protected readonly logger: ILogger;
18+
19+
1420
protected dataDirUri: URI | undefined;
1521

1622
@postConstruct()
1723
protected init(): void {
1824
super.init();
19-
this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri));
25+
this.configService.getConfiguration()
26+
.then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri))
27+
.catch(err => this.logger.error(`Failed to determine the data directory: ${err}`));
2028
}
2129

2230
getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {

arduino-ide-extension/src/node/arduino-daemon.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class ArduinoDaemon implements BackendApplicationContribution {
7171
await new Promise(resolve => setTimeout(resolve, 2000));
7272
this.isReady.resolve();
7373
if (!this.cliContribution.debugCli) {
74-
this.logger.info(`<<< The 'arduino-cli' daemon is up an running.`);
74+
this.logger.info(`<<< The 'arduino-cli' daemon is up and running.`);
7575
} else {
7676
this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`);
7777
}

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

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdirpSync, existsSync } from 'fs-extra';
1+
import * as fs from './fs-extra';
22
import { injectable, inject, postConstruct } from 'inversify';
33
import URI from '@theia/core/lib/common/uri';
44
import { FileUri } from '@theia/core/lib/node/file-uri';
@@ -14,32 +14,35 @@ export class ConfigServiceImpl implements ConfigService {
1414
protected readonly config: Deferred<Config> = new Deferred();
1515

1616
@postConstruct()
17-
protected init(): void {
18-
this.cli.getDefaultConfig().then(config => {
17+
protected async init(): Promise<void> {
18+
try {
19+
const config = await this.cli.getDefaultConfig();
1920
const { dataDirUri, sketchDirUri } = config;
2021
for (const uri of [dataDirUri, sketchDirUri]) {
2122
const path = FileUri.fsPath(uri);
22-
if (!existsSync(path)) {
23-
mkdirpSync(path);
23+
if (!fs.existsSync(path)) {
24+
await fs.mkdirp(path);
2425
}
2526
}
2627
this.config.resolve(config);
27-
});
28+
} catch (err) {
29+
this.config.reject(err);
30+
}
2831
}
2932

30-
async getConfiguration(): Promise<Config> {
33+
getConfiguration(): Promise<Config> {
3134
return this.config.promise;
3235
}
3336

34-
async getVersion(): Promise<string> {
37+
getVersion(): Promise<string> {
3538
return this.cli.getVersion();
3639
}
3740

38-
async isInDataDir(uri: string): Promise<boolean> {
41+
isInDataDir(uri: string): Promise<boolean> {
3942
return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri)));
4043
}
4144

42-
async isInSketchDir(uri: string): Promise<boolean> {
45+
isInSketchDir(uri: string): Promise<boolean> {
4346
return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri)));
4447
}
4548

Original file line numberDiff line numberDiff line change
@@ -1,15 +1,25 @@
11
import { injectable, inject } from 'inversify';
2+
import { ILogger } from '@theia/core';
23
import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server';
34
import { ConfigService } from '../common/protocol/config-service';
45

56
@injectable()
67
export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer {
78

8-
@inject(ConfigService) protected readonly configService: ConfigService;
9+
@inject(ConfigService)
10+
protected readonly configService: ConfigService;
11+
12+
@inject(ILogger)
13+
protected readonly logger: ILogger;
914

1015
protected async getWorkspaceURIFromCli(): Promise<string | undefined> {
11-
const config = await this.configService.getConfiguration();
12-
return config.sketchDirUri;
16+
try {
17+
const config = await this.configService.getConfiguration();
18+
return config.sketchDirUri;
19+
} catch (err) {
20+
this.logger.error(`Failed to determine the sketch directory: ${err}`);
21+
return super.getWorkspaceURIFromCli();
22+
}
1323
}
1424

15-
}
25+
}
+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as fs from 'fs';
2+
import { promisify } from 'util';
3+
4+
export const existsSync = fs.existsSync;
5+
export const lstatSync = fs.lstatSync;
6+
export const readdirSync = fs.readdirSync;
7+
export const statSync = fs.statSync;
8+
export const writeFileSync = fs.writeFileSync;
9+
10+
export const exists = promisify(fs.exists);
11+
export const lstat = promisify(fs.lstat);
12+
export const readdir = promisify(fs.readdir);
13+
export const stat = promisify(fs.stat);
14+
export const writeFile = promisify(fs.writeFile);
15+
16+
export function mkdirp(path: string, timeout: number = 3000): Promise<void> {
17+
return new Promise((resolve, reject) => {
18+
let timeoutHandle: NodeJS.Timeout;
19+
if (timeout > 0) {
20+
timeoutHandle = setTimeout(() => {
21+
reject(new Error(`Timeout of ${timeout} ms exceeded while trying to create the directory "${path}"`));
22+
}, timeout);
23+
}
24+
fs.mkdir(path, { recursive: true }, err => {
25+
clearTimeout(timeoutHandle);
26+
if (err)
27+
reject(err);
28+
else
29+
resolve();
30+
});
31+
});
32+
}
33+
34+
export function mkdirpSync(path: string): void {
35+
fs.mkdirSync(path, { recursive: true });
36+
}

0 commit comments

Comments
 (0)