Skip to content

Commit

Permalink
fix: workaround for arduino/arduino-cli#1968
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Akos Kitta committed Nov 10, 2022
1 parent b6c4777 commit bc78f4e
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export class OpenSketchFiles extends SketchContribution {
): Promise<Sketch | undefined> {
const { invalidMainSketchUri } = err.data;
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, {
fileService: this.fileService,
sketchService: this.sketchService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { fork } from 'child_process';
import { AddressInfo } from 'net';
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 { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
Expand All @@ -29,6 +29,7 @@ import {
} from '../../common/ipc-communication';
import isValidPath = require('is-valid-path');
import { ErrnoException } from '../../node/utils/errors';
import { isAccessibleSketchPath } from '../../node/sketches-service-impl';

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

Expand Down Expand Up @@ -146,7 +147,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
event.preventDefault();
const resolvedPath = await this.resolvePath(path, cwd);
if (resolvedPath) {
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
const sketchFolderPath = await isAccessibleSketchPath(
resolvedPath,
true
);
if (sketchFolderPath) {
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
await this.openSketch(sketchFolderPath);
Expand All @@ -159,49 +163,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(
maybePath: string,
cwd: string
Expand Down Expand Up @@ -257,7 +218,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
if (!resolvedPath) {
continue;
}
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
const sketchFolderPath = await isAccessibleSketchPath(
resolvedPath,
true
);
if (sketchFolderPath) {
workspace.file = sketchFolderPath;
if (this.isTempSketch.is(workspace.file)) {
Expand Down Expand Up @@ -288,7 +252,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
if (!resolvedPath) {
continue;
}
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
const sketchFolderPath = await isAccessibleSketchPath(resolvedPath, true);
if (sketchFolderPath) {
path = sketchFolderPath;
break;
Expand Down
98 changes: 53 additions & 45 deletions arduino-ide-extension/src/node/sketches-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,60 +733,68 @@ function isNotFoundError(err: unknown): err is ServiceError {

/**
* 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.
* 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.
* 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.
* 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.
*/
async function isInvalidSketchNameError(
cliErr: unknown,
requestSketchPath: string
): Promise<string | undefined> {
if (isNotFoundError(cliErr)) {
const ino = requestSketchPath.endsWith('.ino');
if (ino) {
const sketchFolderPath = path.dirname(requestSketchPath);
const sketchName = path.basename(sketchFolderPath);
const pattern = `${invalidSketchNameErrorRegExpPrefix}${path.join(
sketchFolderPath,
`${sketchName}.ino`
)}`.replace(/\\/g, '\\\\'); // make windows path separator with \\ to have a valid regexp.
if (new RegExp(pattern, 'i').test(cliErr.details)) {
try {
await fs.access(requestSketchPath);
return requestSketchPath;
} catch {
return undefined;
}
}
} else {
try {
const resources = await fs.readdir(requestSketchPath, {
withFileTypes: true,
});
return (
resources
.filter((resource) => resource.isFile())
.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,
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
.sort(({ name: left }, { name: right }) =>
left.localeCompare(right)
)
.map(({ name }) => name)
.map((name) => path.join(requestSketchPath, name))[0]
);
} catch (err) {
if (ErrnoException.isENOENT(err) || ErrnoException.isENOTDIR(err)) {
return undefined;
}
throw err;
}
return isNotFoundError(cliErr)
? isAccessibleSketchPath(requestSketchPath)
: undefined;
}

/**
* 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.
*
* `undefined` if `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. This method does not handle EACCES (Permission denied) errors.
*
* When `fallbackToInvalidFolderPath` is `true`, and the `path` is an accessible folder without any sketch files,
* this method returns with the `path` argument instead of `undefined`.
*/
export async function isAccessibleSketchPath(
path: string,
fallbackToInvalidFolderPath = false
): 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;
}
const entries = await fs.readdir(path, { withFileTypes: true });
const sketchFilename = entries
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
.map(({ name }) => name)
// 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 join(path, sketchFilename);
}
return undefined;
// 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
Expand Down

0 comments on commit bc78f4e

Please sign in to comment.