Do not try to parse the original `NotFound` error message, but look for
a sketch somewhere in the requested path.

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-11-08 11:40:50 +01:00 committed by Akos Kitta
parent 3735553003
commit d24a3911f8
3 changed files with 67 additions and 97 deletions

View File

@ -102,7 +102,7 @@ export class OpenSketchFiles extends SketchContribution {
): Promise<Sketch | undefined> { ): Promise<Sketch | undefined> {
const { invalidMainSketchUri } = err.data; const { invalidMainSketchUri } = err.data;
requestAnimationFrame(() => this.messageService.error(err.message)); requestAnimationFrame(() => this.messageService.error(err.message));
await wait(10); // let IDE2 toast the error message. await wait(250); // let IDE2 open the editor and toast the error message, then open the modal dialog
const movedSketch = await promptMoveSketch(invalidMainSketchUri, { const movedSketch = await promptMoveSketch(invalidMainSketchUri, {
fileService: this.fileService, fileService: this.fileService,
sketchService: this.sketchService, sketchService: this.sketchService,

View File

@ -9,7 +9,7 @@ import {
import { fork } from 'child_process'; import { fork } from 'child_process';
import { AddressInfo } from 'net'; import { AddressInfo } from 'net';
import { join, isAbsolute, resolve } from 'path'; import { join, isAbsolute, resolve } from 'path';
import { promises as fs, Stats } from 'fs'; import { promises as fs } from 'fs';
import { MaybePromise } from '@theia/core/lib/common/types'; import { MaybePromise } from '@theia/core/lib/common/types';
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token'; import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props'; import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
@ -28,6 +28,7 @@ import {
SHOW_PLOTTER_WINDOW, SHOW_PLOTTER_WINDOW,
} from '../../common/ipc-communication'; } from '../../common/ipc-communication';
import { ErrnoException } from '../../node/utils/errors'; import { ErrnoException } from '../../node/utils/errors';
import { isAccessibleSketchPath } from '../../node/sketches-service-impl';
app.commandLine.appendSwitch('disable-http-cache'); app.commandLine.appendSwitch('disable-http-cache');
@ -145,7 +146,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
event.preventDefault(); event.preventDefault();
const resolvedPath = await this.resolvePath(path, cwd); const resolvedPath = await this.resolvePath(path, cwd);
if (resolvedPath) { if (resolvedPath) {
const sketchFolderPath = await this.isValidSketchPath(resolvedPath); const sketchFolderPath = await isAccessibleSketchPath(
resolvedPath,
true
);
if (sketchFolderPath) { if (sketchFolderPath) {
this.openFilePromise.reject(new InterruptWorkspaceRestoreError()); this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
await this.openSketch(sketchFolderPath); await this.openSketch(sketchFolderPath);
@ -158,49 +162,6 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
} }
} }
/**
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
* or it's a directory, and one of the files in the directory is an `.ino` file.
*
* If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
*
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
* The `path` must be an absolute, resolved path.
*/
private async isValidSketchPath(path: string): Promise<string | undefined> {
let stats: Stats | undefined = undefined;
try {
stats = await fs.stat(path);
} catch (err) {
if (ErrnoException.isENOENT(err)) {
return undefined;
}
throw err;
}
if (!stats) {
return undefined;
}
if (stats.isFile()) {
return path.endsWith('.ino') ? path : undefined;
}
try {
const entries = await fs.readdir(path, { withFileTypes: true });
const sketchFilename = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
.map(({ name }) => name)
.sort((left, right) => left.localeCompare(right))[0];
if (sketchFilename) {
return join(path, sketchFilename);
}
// If no sketches found in the folder, but the folder exists,
// return with the path of the empty folder and let IDE2's frontend
// figure out the workspace root.
return path;
} catch (err) {
throw err;
}
}
private async resolvePath( private async resolvePath(
maybePath: string, maybePath: string,
cwd: string cwd: string
@ -253,7 +214,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
if (!resolvedPath) { if (!resolvedPath) {
continue; continue;
} }
const sketchFolderPath = await this.isValidSketchPath(resolvedPath); const sketchFolderPath = await isAccessibleSketchPath(
resolvedPath,
true
);
if (sketchFolderPath) { if (sketchFolderPath) {
workspace.file = sketchFolderPath; workspace.file = sketchFolderPath;
if (this.isTempSketch.is(workspace.file)) { if (this.isTempSketch.is(workspace.file)) {
@ -284,7 +248,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
if (!resolvedPath) { if (!resolvedPath) {
continue; continue;
} }
const sketchFolderPath = await this.isValidSketchPath(resolvedPath); const sketchFolderPath = await isAccessibleSketchPath(resolvedPath, true);
if (sketchFolderPath) { if (sketchFolderPath) {
path = sketchFolderPath; path = sketchFolderPath;
break; break;

View File

@ -734,62 +734,68 @@ function isNotFoundError(err: unknown): err is ServiceError {
/** /**
* Tries to detect whether the error was caused by an invalid main sketch file name. * Tries to detect whether the error was caused by an invalid main sketch file name.
* IDE2 should handle gracefully when there is an invalid sketch folder name. See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details. * IDE2 should handle gracefully when there is an invalid sketch folder name.
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762), so IDE2 parses the error message and tries to guess it. * See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762),
* IDE2 cannot parse the error message (https://github.com/arduino/arduino-cli/issues/1968#issuecomment-1306936142)
* so it checks if a sketch even if it's invalid can be discovered from the requested path.
* Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move. * Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
*/ */
async function isInvalidSketchNameError( async function isInvalidSketchNameError(
cliErr: unknown, cliErr: unknown,
requestSketchPath: string requestSketchPath: string
): Promise<string | undefined> { ): Promise<string | undefined> {
if (isNotFoundError(cliErr)) { return isNotFoundError(cliErr)
const ino = requestSketchPath.endsWith('.ino'); ? isAccessibleSketchPath(requestSketchPath)
if (ino) { : undefined;
const sketchFolderPath = path.dirname(requestSketchPath); }
const sketchName = path.basename(sketchFolderPath);
const pattern = escapeRegExpCharacters( /**
`${invalidSketchNameErrorRegExpPrefix}${path.join( * The `path` argument is valid, if accessible and either pointing to a `.ino` file,
sketchFolderPath, * or it's a directory, and one of the files in the directory is an `.ino` file.
`${sketchName}.ino` *
)}` * `undefined` if `path` was pointing to neither an accessible sketch file nor a sketch folder.
); *
if (new RegExp(pattern, 'i').test(cliErr.details)) { * The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
try { * The `path` must be an absolute, resolved path. This method does not handle EACCES (Permission denied) errors.
await fs.access(requestSketchPath); *
return requestSketchPath; * When `fallbackToInvalidFolderPath` is `true`, and the `path` is an accessible folder without any sketch files,
} catch { * this method returns with the `path` argument instead of `undefined`.
return undefined; */
} export async function isAccessibleSketchPath(
} path: string,
} else { fallbackToInvalidFolderPath = false
try { ): Promise<string | undefined> {
const resources = await fs.readdir(requestSketchPath, { let stats: Stats | undefined = undefined;
withFileTypes: true, try {
}); stats = await fs.stat(path);
return ( } catch (err) {
resources if (ErrnoException.isENOENT(err)) {
.filter((resource) => resource.isFile()) return undefined;
.filter((resource) => resource.name.endsWith('.ino')) }
// A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much, throw err;
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them. }
.sort(({ name: left }, { name: right }) => if (!stats) {
left.localeCompare(right) return undefined;
) }
.map(({ name }) => name) if (stats.isFile()) {
.map((name) => path.join(requestSketchPath, name))[0] return path.endsWith('.ino') ? path : undefined;
); }
} catch (err) { const entries = await fs.readdir(path, { withFileTypes: true });
if (ErrnoException.isENOENT(err) || ErrnoException.isENOTDIR(err)) { const sketchFilename = entries
return undefined; .filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
} .map(({ name }) => name)
throw err; // A folder might contain multiple sketches. It's OK to pick the first one as IDE2 cannot do much,
} // but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
} .sort((left, right) => left.localeCompare(right))[0];
} if (sketchFilename) {
return undefined; return join(path, sketchFilename);
}
// If no sketches found in the folder, but the folder exists,
// return with the path of the empty folder and let IDE2's frontend
// figure out the workspace root.
return fallbackToInvalidFolderPath ? path : undefined;
} }
const invalidSketchNameErrorRegExpPrefix =
'.*: main file missing from sketch: ';
/* /*
* When a new sketch is created, add a suffix to distinguish it * When a new sketch is created, add a suffix to distinguish it