Merge pull request #97 from bcmi-labs/robust_workspace_init

More robust workspace initialization
This commit is contained in:
Luca Cipriani 2020-01-23 11:08:28 +00:00 committed by GitHub
commit 41bf1ce6dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 147 additions and 72 deletions

12
.vscode/launch.json vendored
View File

@ -4,6 +4,12 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"type": "node",
"request": "attach",
"name": "Attach by Process ID",
"processId": "${command:PickProcess}"
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
@ -33,7 +39,7 @@
"${workspaceRoot}/electron-app/src-gen/backend/*.js", "${workspaceRoot}/electron-app/src-gen/backend/*.js",
"${workspaceRoot}/electron-app/src-gen/frontend/*.js", "${workspaceRoot}/electron-app/src-gen/frontend/*.js",
"${workspaceRoot}/electron-app/lib/**/*.js", "${workspaceRoot}/electron-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" "${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
], ],
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
@ -63,7 +69,7 @@
"outFiles": [ "outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js", "${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js", "${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" "${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
], ],
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",
@ -88,7 +94,7 @@
"outFiles": [ "outFiles": [
"${workspaceRoot}/browser-app/src-gen/backend/*.js", "${workspaceRoot}/browser-app/src-gen/backend/*.js",
"${workspaceRoot}/browser-app/lib/**/*.js", "${workspaceRoot}/browser-app/lib/**/*.js",
"${workspaceRoot}/arduino-ide-extension/*/lib/**/*.js" "${workspaceRoot}/arduino-ide-extension/lib/**/*.js"
], ],
"smartStep": true, "smartStep": true,
"internalConsoleOptions": "openOnSessionStart", "internalConsoleOptions": "openOnSessionStart",

View File

@ -198,7 +198,7 @@ export default new ContainerModule((bind: interfaces.Bind, unbind: interfaces.Un
}).inSingletonScope(); }).inSingletonScope();
bind(ArduinoWorkspaceService).toSelf().inSingletonScope(); bind(ArduinoWorkspaceService).toSelf().inSingletonScope();
rebind(WorkspaceService).to(ArduinoWorkspaceService).inSingletonScope(); rebind(WorkspaceService).toService(ArduinoWorkspaceService);
const themeService = ThemeService.get(); const themeService = ThemeService.get();
themeService.register(...ArduinoTheme.themes); themeService.register(...ArduinoTheme.themes);

View File

@ -47,7 +47,7 @@ export class ArduinoWorkspaceRootResolver {
} }
protected isValid(uri: string): MaybePromise<boolean> { protected isValid(uri: string): MaybePromise<boolean> {
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. // 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 if (hash
&& hash.length > 1 && hash.length > 1
&& hash.startsWith('#')) { && hash.startsWith('#')) {
const path = hash.slice(1); // Trim the leading `#`. const path = hash.slice(1); // Trim the leading `#`.
return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString(); return new URI(toUnix(path.slice(isWindows && hash.startsWith('/') ? 1 : 0))).withScheme('file').toString();
} }
return undefined; return undefined;
} }
} }

View File

@ -1,4 +1,5 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { MessageService } from '@theia/core';
import { LabelProvider } from '@theia/core/lib/browser'; import { LabelProvider } from '@theia/core/lib/browser';
import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
@ -21,26 +22,44 @@ export class ArduinoWorkspaceService extends WorkspaceService {
@inject(EditorMode) @inject(EditorMode)
protected readonly editorMode: EditorMode; protected readonly editorMode: EditorMode;
async getDefaultWorkspaceUri(): Promise<string | undefined> { @inject(MessageService)
const [hash, recentWorkspaces, recentSketches] = await Promise.all([ protected readonly messageService: MessageService;
window.location.hash,
this.sketchService.getSketches().then(sketches => sketches.map(({ uri }) => uri)), private workspaceUri?: Promise<string | undefined>;
this.server.getRecentWorkspaces()
]); protected getDefaultWorkspaceUri(): Promise<string | undefined> {
const toOpen = await new ArduinoWorkspaceRootResolver({ if (this.workspaceUri) {
isValid: this.isValid.bind(this) // Avoid creating a new sketch twice
}).resolve({ return this.workspaceUri;
hash,
recentWorkspaces,
recentSketches
});
if (toOpen) {
const { uri } = toOpen;
await this.server.setMostRecentlyUsedWorkspace(uri);
return toOpen.uri;
} }
const { sketchDirUri } = (await this.configService.getConfiguration()); this.workspaceUri = (async () => {
return (await this.sketchService.createNewSketch(sketchDirUri)).uri; 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<boolean> { private async isValid(uri: string): Promise<boolean> {

View File

@ -1,6 +1,7 @@
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import { Diagnostic } from 'vscode-languageserver-types'; import { Diagnostic } from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { ILogger } from '@theia/core';
import { Marker } from '@theia/markers/lib/common/marker'; import { Marker } from '@theia/markers/lib/common/marker';
import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager'; import { ProblemManager } from '@theia/markers/lib/browser/problem/problem-manager';
import { ConfigService } from '../../common/protocol/config-service'; import { ConfigService } from '../../common/protocol/config-service';
@ -10,12 +11,18 @@ export class ArduinoProblemManager extends ProblemManager {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined; protected dataDirUri: URI | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
super.init(); 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<Diagnostic>[] { setMarkers(uri: URI, owner: string, data: Diagnostic[]): Marker<Diagnostic>[] {

View File

@ -1,6 +1,7 @@
import { inject, injectable, postConstruct } from 'inversify'; import { inject, injectable, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { Title, Widget } from '@phosphor/widgets'; import { Title, Widget } from '@phosphor/widgets';
import { ILogger } from '@theia/core';
import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration'; import { WidgetDecoration } from '@theia/core/lib/browser/widget-decoration';
import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator'; import { TabBarDecoratorService } from '@theia/core/lib/browser/shell/tab-bar-decorator';
import { ConfigService } from '../../common/protocol/config-service'; import { ConfigService } from '../../common/protocol/config-service';
@ -11,12 +12,19 @@ export class ArduinoTabBarDecoratorService extends TabBarDecoratorService {
@inject(ConfigService) @inject(ConfigService)
protected readonly configService: ConfigService; protected readonly configService: ConfigService;
@inject(ILogger)
protected readonly logger: ILogger;
protected dataDirUri: URI | undefined; protected dataDirUri: URI | undefined;
@postConstruct() @postConstruct()
protected init(): void { protected init(): void {
super.init(); 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<Widget>): WidgetDecoration.Data[] { getDecorations(title: Title<Widget>): WidgetDecoration.Data[] {

View File

@ -71,7 +71,7 @@ export class ArduinoDaemon implements BackendApplicationContribution {
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
this.isReady.resolve(); this.isReady.resolve();
if (!this.cliContribution.debugCli) { 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 { } else {
this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`); this.logger.info(`Assuming the 'arduino-cli' already runs in debug mode.`);
} }

View File

@ -1,4 +1,4 @@
import { mkdirpSync, existsSync } from 'fs-extra'; import * as fs from './fs-extra';
import { injectable, inject, postConstruct } from 'inversify'; import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri'; import URI from '@theia/core/lib/common/uri';
import { FileUri } from '@theia/core/lib/node/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
@ -14,32 +14,35 @@ export class ConfigServiceImpl implements ConfigService {
protected readonly config: Deferred<Config> = new Deferred(); protected readonly config: Deferred<Config> = new Deferred();
@postConstruct() @postConstruct()
protected init(): void { protected async init(): Promise<void> {
this.cli.getDefaultConfig().then(config => { try {
const config = await this.cli.getDefaultConfig();
const { dataDirUri, sketchDirUri } = config; const { dataDirUri, sketchDirUri } = config;
for (const uri of [dataDirUri, sketchDirUri]) { for (const uri of [dataDirUri, sketchDirUri]) {
const path = FileUri.fsPath(uri); const path = FileUri.fsPath(uri);
if (!existsSync(path)) { if (!fs.existsSync(path)) {
mkdirpSync(path); await fs.mkdirp(path);
} }
} }
this.config.resolve(config); this.config.resolve(config);
}); } catch (err) {
this.config.reject(err);
}
} }
async getConfiguration(): Promise<Config> { getConfiguration(): Promise<Config> {
return this.config.promise; return this.config.promise;
} }
async getVersion(): Promise<string> { getVersion(): Promise<string> {
return this.cli.getVersion(); return this.cli.getVersion();
} }
async isInDataDir(uri: string): Promise<boolean> { isInDataDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri))); return this.getConfiguration().then(({ dataDirUri }) => new URI(dataDirUri).isEqualOrParent(new URI(uri)));
} }
async isInSketchDir(uri: string): Promise<boolean> { isInSketchDir(uri: string): Promise<boolean> {
return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri))); return this.getConfiguration().then(({ sketchDirUri }) => new URI(sketchDirUri).isEqualOrParent(new URI(uri)));
} }

View File

@ -1,15 +1,25 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import { ILogger } from '@theia/core';
import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server'; import { DefaultWorkspaceServer } from '@theia/workspace/lib/node/default-workspace-server';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
@injectable() @injectable()
export class DefaultWorkspaceServerExt extends DefaultWorkspaceServer { 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<string | undefined> { protected async getWorkspaceURIFromCli(): Promise<string | undefined> {
const config = await this.configService.getConfiguration(); try {
return config.sketchDirUri; const config = await this.configService.getConfiguration();
return config.sketchDirUri;
} catch (err) {
this.logger.error(`Failed to determine the sketch directory: ${err}`);
return super.getWorkspaceURIFromCli();
}
} }
} }

View File

@ -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<void> {
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 });
}

View File

@ -1,6 +1,6 @@
import { injectable, inject } from 'inversify'; import { injectable, inject } from 'inversify';
import * as path from 'path'; 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 { FileUri } from '@theia/core/lib/node';
import { ConfigService } from '../common/protocol/config-service'; import { ConfigService } from '../common/protocol/config-service';
import { SketchesService, Sketch } from '../common/protocol/sketches-service'; import { SketchesService, Sketch } from '../common/protocol/sketches-service';
@ -18,7 +18,7 @@ export class SketchesServiceImpl implements SketchesService {
const sketches: Array<Sketch & { mtimeMs: number }> = []; const sketches: Array<Sketch & { mtimeMs: number }> = [];
let fsPath: undefined | string; let fsPath: undefined | string;
if (!uri) { if (!uri) {
const { sketchDirUri } = (await this.configService.getConfiguration()); const { sketchDirUri } = await this.configService.getConfiguration();
fsPath = FileUri.fsPath(sketchDirUri); fsPath = FileUri.fsPath(sketchDirUri);
if (!fs.existsSync(fsPath)) { if (!fs.existsSync(fsPath)) {
await fs.mkdirp(fsPath); await fs.mkdirp(fsPath);
@ -29,11 +29,11 @@ export class SketchesServiceImpl implements SketchesService {
if (!fs.existsSync(fsPath)) { if (!fs.existsSync(fsPath)) {
return []; return [];
} }
const fileNames = fs.readdirSync(fsPath); const fileNames = await fs.readdir(fsPath);
for (const fileName of fileNames) { for (const fileName of fileNames) {
const filePath = path.join(fsPath, fileName); const filePath = path.join(fsPath, fileName);
if (await this.isSketchFolder(FileUri.create(filePath).toString())) { if (await this.isSketchFolder(FileUri.create(filePath).toString())) {
const stat = fs.statSync(filePath); const stat = await fs.stat(filePath);
sketches.push({ sketches.push({
mtimeMs: stat.mtimeMs, mtimeMs: stat.mtimeMs,
name: fileName, name: fileName,
@ -51,10 +51,9 @@ export class SketchesServiceImpl implements SketchesService {
async getSketchFiles(uri: string): Promise<string[]> { async getSketchFiles(uri: string): Promise<string[]> {
const uris: string[] = []; const uris: string[] = [];
const fsPath = FileUri.fsPath(uri); const fsPath = FileUri.fsPath(uri);
const stats = fs.lstatSync(fsPath); if (fs.lstatSync(fsPath).isDirectory()) {
if (stats.isDirectory) {
if (await this.isSketchFolder(uri)) { if (await this.isSketchFolder(uri)) {
const fileNames = fs.readdirSync(fsPath); const fileNames = await fs.readdir(fsPath);
for (const fileName of fileNames) { for (const fileName of fileNames) {
const filePath = path.join(fsPath, fileName); const filePath = path.join(fsPath, fileName);
if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1 if (ALLOWED_FILE_EXTENSIONS.indexOf(path.extname(filePath)) !== -1
@ -116,26 +115,13 @@ void loop() {
async isSketchFolder(uri: string): Promise<boolean> { async isSketchFolder(uri: string): Promise<boolean> {
const fsPath = FileUri.fsPath(uri); const fsPath = FileUri.fsPath(uri);
const exists = await fs.pathExists(fsPath); if (fs.existsSync(fsPath) && fs.lstatSync(fsPath).isDirectory()) {
if (exists) { const basename = path.basename(fsPath);
const stats = await fs.lstat(fsPath); const files = await fs.readdir(fsPath);
if (stats.isDirectory()) { for (let i = 0; i < files.length; i++) {
const basename = path.basename(fsPath); if (files[i] === basename + '.ino') {
return new Promise<boolean>((resolve, reject) => { return true;
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);
});
})
} }
} }
return false; return false;

View File

@ -7,7 +7,7 @@
"arduino-ide-extension": "file:../working-copy/arduino-ide-extension" "arduino-ide-extension": "file:../working-copy/arduino-ide-extension"
}, },
"resolutions": { "resolutions": {
"**/fs-extra": "^8.1.0" "**/fs-extra": "^4.0.3"
}, },
"devDependencies": { "devDependencies": {
"electron-builder": "^21.2.0" "electron-builder": "^21.2.0"