Skip to content

Commit 6b26d3c

Browse files
author
Akos Kitta
committed
fix: workaround for arduino/arduino-cli#1968
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 <[email protected]>
1 parent c87d3bc commit 6b26d3c

File tree

3 files changed

+69
-93
lines changed

3 files changed

+69
-93
lines changed

arduino-ide-extension/src/browser/contributions/open-sketch-files.ts

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

arduino-ide-extension/src/electron-main/theia/electron-main-application.ts

+11-47
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import { fork } from 'child_process';
1010
import { AddressInfo } from 'net';
1111
import { join, isAbsolute, resolve } from 'path';
12-
import { promises as fs, Stats } from 'fs';
12+
import { promises as fs } from 'fs';
1313
import { MaybePromise } from '@theia/core/lib/common/types';
1414
import { ElectronSecurityToken } from '@theia/core/lib/electron-common/electron-token';
1515
import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
@@ -28,6 +28,7 @@ import {
2828
SHOW_PLOTTER_WINDOW,
2929
} from '../../common/ipc-communication';
3030
import isValidPath = require('is-valid-path');
31+
import { isAccessibleSketchPath } from '../../node/sketches-service-impl';
3132

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

@@ -145,7 +146,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
145146
event.preventDefault();
146147
const resolvedPath = await this.resolvePath(path, cwd);
147148
if (resolvedPath) {
148-
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
149+
const sketchFolderPath = await isAccessibleSketchPath(
150+
resolvedPath,
151+
true
152+
);
149153
if (sketchFolderPath) {
150154
this.openFilePromise.reject(new InterruptWorkspaceRestoreError());
151155
await this.openSketch(sketchFolderPath);
@@ -158,49 +162,6 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
158162
}
159163
}
160164

161-
/**
162-
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
163-
* or it's a directory, and one of the files in the directory is an `.ino` file.
164-
*
165-
* If `undefined`, `path` was pointing to neither an accessible sketch file nor a sketch folder.
166-
*
167-
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
168-
* The `path` must be an absolute, resolved path.
169-
*/
170-
private async isValidSketchPath(path: string): Promise<string | undefined> {
171-
let stats: Stats | undefined = undefined;
172-
try {
173-
stats = await fs.stat(path);
174-
} catch (err) {
175-
if ('code' in err && err.code === 'ENOENT') {
176-
return undefined;
177-
}
178-
throw err;
179-
}
180-
if (!stats) {
181-
return undefined;
182-
}
183-
if (stats.isFile() && path.endsWith('.ino')) {
184-
return path;
185-
}
186-
try {
187-
const entries = await fs.readdir(path, { withFileTypes: true });
188-
const sketchFilename = entries
189-
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
190-
.map(({ name }) => name)
191-
.sort((left, right) => left.localeCompare(right))[0];
192-
if (sketchFilename) {
193-
return join(path, sketchFilename);
194-
}
195-
// If no sketches found in the folder, but the folder exists,
196-
// return with the path of the empty folder and let IDE2's frontend
197-
// figure out the workspace root.
198-
return path;
199-
} catch (err) {
200-
throw err;
201-
}
202-
}
203-
204165
private async resolvePath(
205166
maybePath: string,
206167
cwd: string
@@ -256,7 +217,10 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
256217
if (!resolvedPath) {
257218
continue;
258219
}
259-
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
220+
const sketchFolderPath = await isAccessibleSketchPath(
221+
resolvedPath,
222+
true
223+
);
260224
if (sketchFolderPath) {
261225
workspace.file = sketchFolderPath;
262226
if (this.isTempSketch.is(workspace.file)) {
@@ -287,7 +251,7 @@ export class ElectronMainApplication extends TheiaElectronMainApplication {
287251
if (!resolvedPath) {
288252
continue;
289253
}
290-
const sketchFolderPath = await this.isValidSketchPath(resolvedPath);
254+
const sketchFolderPath = await isAccessibleSketchPath(resolvedPath, true);
291255
if (sketchFolderPath) {
292256
path = sketchFolderPath;
293257
break;

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

+57-45
Original file line numberDiff line numberDiff line change
@@ -720,60 +720,72 @@ function isNotFoundError(err: unknown): err is ServiceError {
720720

721721
/**
722722
* Tries to detect whether the error was caused by an invalid main sketch file name.
723-
* 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.
724-
* 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.
723+
* IDE2 should handle gracefully when there is an invalid sketch folder name.
724+
* See the [spec](https://arduino.github.io/arduino-cli/latest/sketch-specification/#sketch-root-folder) for details.
725+
* The CLI does not have error codes (https://github.com/arduino/arduino-cli/issues/1762),
726+
* IDE2 cannot parse the error message (https://github.com/arduino/arduino-cli/issues/1968#issuecomment-1306936142)
727+
* so it checks if a sketch even if it's invalid can be discovered from the requested path.
725728
* Nothing guarantees that the invalid existing main sketch file still exits by the time client performs the sketch move.
726729
*/
727730
async function isInvalidSketchNameError(
728731
cliErr: unknown,
729732
requestSketchPath: string
730733
): Promise<string | undefined> {
731-
if (isNotFoundError(cliErr)) {
732-
const ino = requestSketchPath.endsWith('.ino');
733-
if (ino) {
734-
const sketchFolderPath = path.dirname(requestSketchPath);
735-
const sketchName = path.basename(sketchFolderPath);
736-
const pattern = `${invalidSketchNameErrorRegExpPrefix}${path.join(
737-
sketchFolderPath,
738-
`${sketchName}.ino`
739-
)}`.replace(/\\/g, '\\\\'); // make windows path separator with \\ to have a valid regexp.
740-
if (new RegExp(pattern, 'i').test(cliErr.details)) {
741-
try {
742-
await fs.access(requestSketchPath);
743-
return requestSketchPath;
744-
} catch {
745-
return undefined;
746-
}
747-
}
748-
} else {
749-
try {
750-
const resources = await fs.readdir(requestSketchPath, {
751-
withFileTypes: true,
752-
});
753-
return (
754-
resources
755-
.filter((resource) => resource.isFile())
756-
.filter((resource) => resource.name.endsWith('.ino'))
757-
// A folder might contain multiple sketches. It's OK to ick the first one as IDE2 cannot do much,
758-
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
759-
.sort(({ name: left }, { name: right }) =>
760-
left.localeCompare(right)
761-
)
762-
.map(({ name }) => name)
763-
.map((name) => path.join(requestSketchPath, name))[0]
764-
);
765-
} catch (err) {
766-
if ('code' in err && err.code === 'ENOTDIR') {
767-
return undefined;
768-
}
769-
throw err;
770-
}
734+
return isNotFoundError(cliErr)
735+
? isAccessibleSketchPath(requestSketchPath)
736+
: undefined;
737+
}
738+
739+
/**
740+
* The `path` argument is valid, if accessible and either pointing to a `.ino` file,
741+
* or it's a directory, and one of the files in the directory is an `.ino` file.
742+
*
743+
* `undefined` if `path` was pointing to neither an accessible sketch file nor a sketch folder.
744+
*
745+
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
746+
* The `path` must be an absolute, resolved path. This method does not handle EACCES (Permission denied) errors.
747+
*
748+
* When `fallbackToInvalidFolderPath` is `true`, and the `path` is an accessible folder without any sketch files,
749+
* this method returns with the `path` argument instead of `undefined`.
750+
*/
751+
export async function isAccessibleSketchPath(
752+
path: string,
753+
fallbackToInvalidFolderPath = false
754+
): Promise<string | undefined> {
755+
let stats: Stats | undefined = undefined;
756+
try {
757+
stats = await fs.stat(path);
758+
} catch (err) {
759+
if ('code' in err && err.code === 'ENOENT') {
760+
return undefined;
761+
}
762+
throw err;
763+
}
764+
if (!stats) {
765+
return undefined;
766+
}
767+
if (stats.isFile()) {
768+
return path.endsWith('.ino') ? path : undefined;
769+
}
770+
try {
771+
const entries = await fs.readdir(path, { withFileTypes: true });
772+
const sketchFilename = entries
773+
.filter((entry) => entry.isFile() && entry.name.endsWith('.ino'))
774+
.map(({ name }) => name)
775+
// A folder might contain multiple sketches. It's OK to pick the first one as IDE2 cannot do much,
776+
// but ensure a deterministic behavior as `readdir(3)` does not guarantee an order. Sort them.
777+
.sort((left, right) => left.localeCompare(right))[0];
778+
if (sketchFilename) {
779+
return join(path, sketchFilename);
771780
}
781+
// If no sketches found in the folder, but the folder exists,
782+
// return with the path of the empty folder and let IDE2's frontend
783+
// figure out the workspace root.
784+
return fallbackToInvalidFolderPath ? path : undefined;
785+
} catch (err) {
786+
throw err;
772787
}
773-
return undefined;
774788
}
775-
const invalidSketchNameErrorRegExpPrefix =
776-
'.*: main file missing from sketch: ';
777789

778790
/*
779791
* When a new sketch is created, add a suffix to distinguish it

0 commit comments

Comments
 (0)