From 1aa944b25ed915367cd9313f00e4b03b0180fcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Sp=C3=B6nemann?= Date: Mon, 20 Jan 2020 16:48:24 +0100 Subject: [PATCH] More robust workspace initialization: guard against errors creating sketch dir --- .vscode/launch.json | 12 +++- .../src/browser/arduino-frontend-module.ts | 2 +- .../src/browser/arduino-workspace-resolver.ts | 8 +-- .../src/browser/arduino-workspace-service.ts | 57 ++++++++++++------- .../markers/arduino-problem-manager.ts | 9 ++- .../shell/arduino-tab-bar-decorator.ts | 10 +++- .../src/node/arduino-daemon.ts | 2 +- .../src/node/config-service-impl.ts | 23 ++++---- .../src/node/default-workspace-server-ext.ts | 18 ++++-- arduino-ide-extension/src/node/fs-extra.ts | 36 ++++++++++++ .../src/node/sketches-service-impl.ts | 40 +++++-------- electron/build/template-package.json | 2 +- 12 files changed, 147 insertions(+), 72 deletions(-) create mode 100644 arduino-ide-extension/src/node/fs-extra.ts diff --git a/.vscode/launch.json b/.vscode/launch.json index cccd368e..2cf29886 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,12 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "node", + "request": "attach", + "name": "Attach by Process ID", + "processId": "${command:PickProcess}" + }, { "type": "node", "request": "launch", @@ -33,7 +39,7 @@ "${workspaceRoot}/electron-app/src-gen/backend/*.js", "${workspaceRoot}/electron-app/src-gen/frontend/*.js", "${workspaceRoot}/electron-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" + "${workspaceRoot}/arduino-ide-extension/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", @@ -63,7 +69,7 @@ "outFiles": [ "${workspaceRoot}/browser-app/src-gen/backend/*.js", "${workspaceRoot}/browser-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" + "${workspaceRoot}/arduino-ide-extension/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", @@ -88,7 +94,7 @@ "outFiles": [ "${workspaceRoot}/browser-app/src-gen/backend/*.js", "${workspaceRoot}/browser-app/lib/**/*.js", - "${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" + "${workspaceRoot}/arduino-ide-extension/lib/**/*.js" ], "smartStep": true, "internalConsoleOptions": "openOnSessionStart", diff --git a/arduino-ide-extension/src/browser/arduino-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-frontend-module.ts index 227ab89c..634165a0 100644 --- a/arduino-ide-extension/src/browser/arduino-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-frontend-module.ts @@ -198,7 +198,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un }).inSingletonScope(); bind(ArduinoWorkspaceService).toSelf().inSingletonScope(); - rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope(); + rebind(WorkspaceService).toService(ArduinoWorkspaceService); const themeService = ThemeService.get(); themeService.register(...ArduinoTheme.themes); diff --git a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts index 3ee087c5..94153af9 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts @@ -47,7 +47,7 @@ export class ArduinoWorkspaceRootResolver { } protected isValid(uri: string): MaybePromise { - return this.options.isValid.bind(this)(uri); + return this.options.isValid(uri); } // 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 { if (hash && hash.length > 1 && hash.startsWith('#')) { - const path = hash.slice(1); // Trim the leading `#`. - return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString(); + const path = hash.slice(1); // Trim the leading `#`. + return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString(); } return undefined; } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/browser/arduino-workspace-service.ts b/arduino-ide-extension/src/browser/arduino-workspace-service.ts index 80b04972..1c2881b4 100644 --- a/arduino-ide-extension/src/browser/arduino-workspace-service.ts +++ b/arduino-ide-extension/src/browser/arduino-workspace-service.ts @@ -1,4 +1,5 @@ import { injectable, inject } from 'inversify'; +import { MessageService } from '@theia/core'; import { LabelProvider } from '@theia/core/lib/browser'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { ConfigService } from '../common/protocol/config-service'; @@ -21,26 +22,44 @@ export class ArduinoWorkspaceService extends WorkspaceService { @inject(EditorMode) protected readonly editorMode: EditorMode; - async getDefaultWorkspaceUri(): Promise { - const [hash, recentWorkspaces, recentSketches] = await Promise.all([ - window.location.hash, - this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)), - this.server.getRecentWorkspaces() - ]); - const toOpen = await new ArduinoWorkspaceRootResolver({ - isValid: this.isValid.bind(this) - }).resolve({ - hash, - recentWorkspaces, - recentSketches - }); - if (toOpen) { - const { uri } = toOpen; - await this.server.setMostRecentlyUsedWorkspace(uri); - return toOpen.uri; + @inject(MessageService) + protected readonly messageService: MessageService; + + private workspaceUri?: Promise; + + protected getDefaultWorkspaceUri(): Promise { + if (this.workspaceUri) { + // Avoid creating a new sketch twice + return this.workspaceUri; } - const { sketchDirUri } = (await this.configService.getConfiguration()); - return (await this.sketchService.createNewSketch(sketchDirUri)).uri; + this.workspaceUri = (async () => { + try { + const hash = window.location.hash; + const [recentWorkspaces, recentSketches] = await Promise.all([ + this.server.getRecentWorkspaces(), + this.sketchService.getSketches().then(sketches => sketches.map(s => s.uri)) + ]); + const toOpen = await new ArduinoWorkspaceRootResolver({ + isValid: this.isValid.bind(this) + }).resolve({ hash, recentWorkspaces, recentSketches }); + if (toOpen) { + const { uri } = toOpen; + await this.server.setMostRecentlyUsedWorkspace(uri); + return toOpen.uri; + } + const { sketchDirUri } = (await this.configService.getConfiguration()); + this.logger.info(`No valid workspace URI found. Creating new sketch in ${sketchDirUri}`) + return (await this.sketchService.createNewSketch(sketchDirUri)).uri; + } catch (err) { + this.logger.fatal(`Failed to determine the sketch directory: ${err}`) + this.messageService.error( + 'There was an error creating the sketch directory. ' + + 'See the log for more details. ' + + 'The application will probably not work as expected.') + return super.getDefaultWorkspaceUri(); + } + })(); + return this.workspaceUri; } private async isValid(uri: string): Promise { diff --git a/arduino-ide-extension/src/browser/markers/arduino-problem-manager.ts b/arduino-ide-extension/src/browser/markers/arduino-problem-manager.ts index 30a11119..a15535b1 100644 --- a/arduino-ide-extension/src/browser/markers/arduino-problem-manager.ts +++ b/arduino-ide-extension/src/browser/markers/arduino-problem-manager.ts @@ -1,6 +1,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import { Diagnostic } from 'vscode-languageserver-types'; import URI from '@theia/core/lib/common/uri'; +import { ILogger } from '@theia/core'; import { Marker } from '@theia/markers/lib/common/marker'; import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager'; import { ConfigService } from '../../common/protocol/config-service'; @@ -10,12 +11,18 @@ export class ArduinoProblemManager extends ProblemManager { @inject(ConfigService) protected readonly configService: ConfigService; + + @inject(ILogger) + protected readonly logger: ILogger; + protected dataDirUri: URI | undefined; @postConstruct() protected init(): void { super.init(); - this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri)); + this.configService.getConfiguration() + .then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri)) + .catch(err => this.logger.error(`Failed to determine the data directory: ${err}`)); } setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker[] { diff --git a/arduino-ide-extension/src/browser/shell/arduino-tab-bar-decorator.ts b/arduino-ide-extension/src/browser/shell/arduino-tab-bar-decorator.ts index c8bcc4d4..ee171cbf 100644 --- a/arduino-ide-extension/src/browser/shell/arduino-tab-bar-decorator.ts +++ b/arduino-ide-extension/src/browser/shell/arduino-tab-bar-decorator.ts @@ -1,6 +1,7 @@ import { inject, injectable, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { Title, Widget } from '@phosphor/widgets'; +import { ILogger } from '@theia/core'; import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { ConfigService } from '../../common/protocol/config-service'; @@ -11,12 +12,19 @@ export class ArduinoTabBarDecoratorService extends TabBarDecoratorService { @inject(ConfigService) protected readonly configService: ConfigService; + + @inject(ILogger) + protected readonly logger: ILogger; + + protected dataDirUri: URI | undefined; @postConstruct() protected init(): void { super.init(); - this.configService.getConfiguration().then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri)); + this.configService.getConfiguration() + .then(({ dataDirUri }) => this.dataDirUri = new URI(dataDirUri)) + .catch(err => this.logger.error(`Failed to determine the data directory: ${err}`)); } getDecorations(title: Title): WidgetDecoration.Data[] { diff --git a/arduino-ide-extension/src/node/arduino-daemon.ts b/arduino-ide-extension/src/node/arduino-daemon.ts index 63047d16..fc843562 100644 --- a/arduino-ide-extension/src/node/arduino-daemon.ts +++ b/arduino-ide-extension/src/node/arduino-daemon.ts @@ -71,7 +71,7 @@ export class ArduinoDaemon implements BackendApplicationContribution { await new Promise(resolve => setTimeout(resolve, 2000)); this.isReady.resolve(); if (!this.cliContribution.debugCli) { - this.logger.info(`<<< The 'arduino-cli' daemon is up an running.`); + this.logger.info(`<<< The 'arduino-cli' daemon is up and running.`); } else { this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`); } diff --git a/arduino-ide-extension/src/node/config-service-impl.ts b/arduino-ide-extension/src/node/config-service-impl.ts index 5f16d264..66a88008 100644 --- a/arduino-ide-extension/src/node/config-service-impl.ts +++ b/arduino-ide-extension/src/node/config-service-impl.ts @@ -1,4 +1,4 @@ -import { mkdirpSync, existsSync } from 'fs-extra'; +import * as fs from './fs-extra'; import { injectable, inject, postConstruct } from 'inversify'; import URI from '@theia/core/lib/common/uri'; import { FileUri } from '@theia/core/lib/node/file-uri'; @@ -14,32 +14,35 @@ export class ConfigServiceImpl implements ConfigService { protected readonly config: Deferred = new Deferred(); @postConstruct() - protected init(): void { - this.cli.getDefaultConfig().then(config => { + protected async init(): Promise { + try { + const config = await this.cli.getDefaultConfig(); const { dataDirUri, sketchDirUri } = config; for (const uri of [dataDirUri, sketchDirUri]) { const path = FileUri.fsPath(uri); - if (!existsSync(path)) { - mkdirpSync(path); + if (!fs.existsSync(path)) { + await fs.mkdirp(path); } } this.config.resolve(config); - }); + } catch (err) { + this.config.reject(err); + } } - async getConfiguration(): Promise { + getConfiguration(): Promise { return this.config.promise; } - async getVersion(): Promise { + getVersion(): Promise { return this.cli.getVersion(); } - async isInDataDir(uri: string): Promise { + isInDataDir(uri: string): Promise { return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri))); } - async isInSketchDir(uri: string): Promise { + isInSketchDir(uri: string): Promise { return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri))); } diff --git a/arduino-ide-extension/src/node/default-workspace-server-ext.ts b/arduino-ide-extension/src/node/default-workspace-server-ext.ts index 70a0a66b..cc232a6f 100644 --- a/arduino-ide-extension/src/node/default-workspace-server-ext.ts +++ b/arduino-ide-extension/src/node/default-workspace-server-ext.ts @@ -1,15 +1,25 @@ import { injectable, inject } from 'inversify'; +import { ILogger } from '@theia/core'; import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; import { ConfigService } from '../common/protocol/config-service'; @injectable() export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer { - @inject(ConfigService) protected readonly configService: ConfigService; + @inject(ConfigService) + protected readonly configService: ConfigService; + + @inject(ILogger) + protected readonly logger: ILogger; protected async getWorkspaceURIFromCli(): Promise { - const config = await this.configService.getConfiguration(); - return config.sketchDirUri; + try { + const config = await this.configService.getConfiguration(); + return config.sketchDirUri; + } catch (err) { + this.logger.error(`Failed to determine the sketch directory: ${err}`); + return super.getWorkspaceURIFromCli(); + } } -} \ No newline at end of file +} diff --git a/arduino-ide-extension/src/node/fs-extra.ts b/arduino-ide-extension/src/node/fs-extra.ts new file mode 100644 index 00000000..690602df --- /dev/null +++ b/arduino-ide-extension/src/node/fs-extra.ts @@ -0,0 +1,36 @@ +import * as fs from 'fs'; +import { promisify } from 'util'; + +export const existsSync = fs.existsSync; +export const lstatSync = fs.lstatSync; +export const readdirSync = fs.readdirSync; +export const statSync = fs.statSync; +export const writeFileSync = fs.writeFileSync; + +export const exists = promisify(fs.exists); +export const lstat = promisify(fs.lstat); +export const readdir = promisify(fs.readdir); +export const stat = promisify(fs.stat); +export const writeFile = promisify(fs.writeFile); + +export function mkdirp(path: string, timeout: number = 3000): Promise { + return new Promise((resolve, reject) => { + let timeoutHandle: NodeJS.Timeout; + if (timeout > 0) { + timeoutHandle = setTimeout(() => { + reject(new Error(`Timeout of ${timeout} ms exceeded while trying to create the directory "${path}"`)); + }, timeout); + } + fs.mkdir(path, { recursive: true }, err => { + clearTimeout(timeoutHandle); + if (err) + reject(err); + else + resolve(); + }); + }); +} + +export function mkdirpSync(path: string): void { + fs.mkdirSync(path, { recursive: true }); +} diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index bcceea97..108ac50d 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -1,6 +1,6 @@ import { injectable, inject } from 'inversify'; import * as path from 'path'; -import * as fs from 'fs-extra'; +import * as fs from './fs-extra'; import { FileUri } from '@theia/core/lib/node'; import { ConfigService } from '../common/protocol/config-service'; import { SketchesService, Sketch } from '../common/protocol/sketches-service'; @@ -18,7 +18,7 @@ export class SketchesServiceImpl implements SketchesService { const sketches: Array = []; let fsPath: undefined | string; if (!uri) { - const { sketchDirUri } = (await this.configService.getConfiguration()); + const { sketchDirUri } = await this.configService.getConfiguration(); fsPath = FileUri.fsPath(sketchDirUri); if (!fs.existsSync(fsPath)) { await fs.mkdirp(fsPath); @@ -29,11 +29,11 @@ export class SketchesServiceImpl implements SketchesService { if (!fs.existsSync(fsPath)) { return []; } - const fileNames = fs.readdirSync(fsPath); + const fileNames = await fs.readdir(fsPath); for (const fileName of fileNames) { const filePath = path.join(fsPath, fileName); if (await this.isSketchFolder(FileUri.create(filePath).toString())) { - const stat = fs.statSync(filePath); + const stat = await fs.stat(filePath); sketches.push({ mtimeMs: stat.mtimeMs, name: fileName, @@ -51,10 +51,9 @@ export class SketchesServiceImpl implements SketchesService { async getSketchFiles(uri: string): Promise { const uris: string[] = []; const fsPath = FileUri.fsPath(uri); - const stats = fs.lstatSync(fsPath); - if (stats.isDirectory) { + if (fs.lstatSync(fsPath).isDirectory()) { if (await this.isSketchFolder(uri)) { - const fileNames = fs.readdirSync(fsPath); + const fileNames = await fs.readdir(fsPath); for (const fileName of fileNames) { const filePath = path.join(fsPath, fileName); if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1 @@ -116,26 +115,13 @@ void loop() { async isSketchFolder(uri: string): Promise { const fsPath = FileUri.fsPath(uri); - const exists = await fs.pathExists(fsPath); - if (exists) { - const stats = await fs.lstat(fsPath); - if (stats.isDirectory()) { - const basename = path.basename(fsPath); - return new Promise((resolve, reject) => { - fs.readdir(fsPath, (error, files) => { - if (error) { - reject(error); - return; - } - for (let i = 0; i < files.length; i++) { - if (files[i] === basename + '.ino') { - resolve(true); - return; - } - } - resolve(false); - }); - }) + if (fs.existsSync(fsPath) && fs.lstatSync(fsPath).isDirectory()) { + const basename = path.basename(fsPath); + const files = await fs.readdir(fsPath); + for (let i = 0; i < files.length; i++) { + if (files[i] === basename + '.ino') { + return true; + } } } return false; diff --git a/electron/build/template-package.json b/electron/build/template-package.json index d433c809..33a037c2 100644 --- a/electron/build/template-package.json +++ b/electron/build/template-package.json @@ -7,7 +7,7 @@ "arduino-ide-extension": "file:../working-copy/arduino-ide-extension" }, "resolutions": { - "**/fs-extra": "^8.1.0" + "**/fs-extra": "^4.0.3" }, "devDependencies": { "electron-builder": "^21.2.0"