Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit eb2a1d9

Browse filesBrowse files
author
Akos Kitta
committed
fix: handle UNKNOWN code on syscall: 'stat'
Closes #2166 Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
1 parent 69ae38e commit eb2a1d9
Copy full SHA for eb2a1d9

File tree

3 files changed

+125
-4
lines changed
Filter options

3 files changed

+125
-4
lines changed

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

Copy file name to clipboardExpand all lines: arduino-ide-extension/src/node/sketches-service-impl.ts
+2-1Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ async function isInvalidSketchNameError(
677677
*
678678
* The sketch folder name and sketch file name can be different. This method is not sketch folder name compliant.
679679
* The `path` must be an absolute, resolved path. This method does not handle EACCES (Permission denied) errors.
680+
* This method handles `UNKNOWN` errors ([nodejs/node#19965](https://github.com/nodejs/node/issues/19965#issuecomment-380750573)).
680681
*
681682
* When `fallbackToInvalidFolderPath` is `true`, and the `path` is an accessible folder without any sketch files,
682683
* this method returns with the `path` argument instead of `undefined`.
@@ -689,7 +690,7 @@ export async function isAccessibleSketchPath(
689690
try {
690691
stats = await fs.stat(path);
691692
} catch (err) {
692-
if (ErrnoException.isENOENT(err)) {
693+
if (ErrnoException.isENOENT(err) || ErrnoException.isUNKNOWN(err)) {
693694
return undefined;
694695
}
695696
throw err;

‎arduino-ide-extension/src/node/utils/errors.ts

Copy file name to clipboardExpand all lines: arduino-ide-extension/src/node/utils/errors.ts
+22-2Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ export namespace ErrnoException {
1515
}
1616

1717
/**
18-
* (No such file or directory): Commonly raised by `fs` operations to indicate that a component of the specified pathname does not exist — no entity (file or directory) could be found by the given path.
18+
* _(Permission denied):_ An attempt was made to access a file in a way forbidden by its file access permissions.
19+
*/
20+
export function isEACCES(
21+
arg: unknown
22+
): arg is ErrnoException & { code: 'EACCES' } {
23+
return is(arg) && arg.code === 'EACCES';
24+
}
25+
26+
/**
27+
* _(No such file or directory):_ Commonly raised by `fs` operations to indicate that a component of the specified pathname does not exist — no entity (file or directory) could be found by the given path.
1928
*/
2029
export function isENOENT(
2130
arg: unknown
@@ -24,11 +33,22 @@ export namespace ErrnoException {
2433
}
2534

2635
/**
27-
* (Not a directory): A component of the given pathname existed, but was not a directory as expected. Commonly raised by `fs.readdir`.
36+
* _(Not a directory):_ A component of the given pathname existed, but was not a directory as expected. Commonly raised by `fs.readdir`.
2837
*/
2938
export function isENOTDIR(
3039
arg: unknown
3140
): arg is ErrnoException & { code: 'ENOTDIR' } {
3241
return is(arg) && arg.code === 'ENOTDIR';
3342
}
43+
44+
/**
45+
* _"That 4094 error code is a generic network-or-configuration error, Node.js just passes it on from the operating system."_
46+
*
47+
* See [nodejs/node#19965](https://github.com/nodejs/node/issues/19965#issuecomment-380750573) for more details.
48+
*/
49+
export function isUNKNOWN(
50+
arg: unknown
51+
): arg is ErrnoException & { code: 'UNKNOWN' } {
52+
return is(arg) && arg.code === 'UNKNOWN';
53+
}
3454
}

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

Copy file name to clipboardExpand all lines: arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts
+101-1Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,116 @@ import { isWindows } from '@theia/core/lib/common/os';
66
import { FileUri } from '@theia/core/lib/node/file-uri';
77
import { Container } from '@theia/core/shared/inversify';
88
import { expect } from 'chai';
9+
import { rejects } from 'node:assert/strict';
910
import { promises as fs } from 'node:fs';
1011
import { basename, join } from 'node:path';
1112
import { sync as rimrafSync } from 'rimraf';
13+
import temp from 'temp';
1214
import { Sketch, SketchesService } from '../../common/protocol';
13-
import { SketchesServiceImpl } from '../../node/sketches-service-impl';
15+
import {
16+
isAccessibleSketchPath,
17+
SketchesServiceImpl,
18+
} from '../../node/sketches-service-impl';
1419
import { ErrnoException } from '../../node/utils/errors';
1520
import { createBaseContainer, startDaemon } from './node-test-bindings';
1621

1722
const testTimeout = 10_000;
1823

24+
describe('isAccessibleSketchPath', () => {
25+
let tracked: typeof temp;
26+
let testDirPath: string;
27+
28+
before(() => (tracked = temp.track()));
29+
beforeEach(() => (testDirPath = tracked.mkdirSync()));
30+
after(() => tracked.cleanupSync());
31+
32+
it('should be accessible by the main sketch file', async () => {
33+
const sketchFolderPath = join(testDirPath, 'my_sketch');
34+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino');
35+
await fs.mkdir(sketchFolderPath, { recursive: true });
36+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
37+
const actual = await isAccessibleSketchPath(mainSketchFilePath);
38+
expect(actual).to.be.equal(mainSketchFilePath);
39+
});
40+
41+
it('should be accessible by the sketch folder', async () => {
42+
const sketchFolderPath = join(testDirPath, 'my_sketch');
43+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino');
44+
await fs.mkdir(sketchFolderPath, { recursive: true });
45+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
46+
const actual = await isAccessibleSketchPath(sketchFolderPath);
47+
expect(actual).to.be.equal(mainSketchFilePath);
48+
});
49+
50+
it('should be accessible when the sketch folder and main sketch file basenames are different', async () => {
51+
const sketchFolderPath = join(testDirPath, 'my_sketch');
52+
const mainSketchFilePath = join(sketchFolderPath, 'other_name_sketch.ino');
53+
await fs.mkdir(sketchFolderPath, { recursive: true });
54+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
55+
const actual = await isAccessibleSketchPath(sketchFolderPath);
56+
expect(actual).to.be.equal(mainSketchFilePath);
57+
});
58+
59+
it('should be deterministic (and sort by basename) when multiple sketch files exist', async () => {
60+
const sketchFolderPath = join(testDirPath, 'my_sketch');
61+
const aSketchFilePath = join(sketchFolderPath, 'a.ino');
62+
const bSketchFilePath = join(sketchFolderPath, 'b.ino');
63+
await fs.mkdir(sketchFolderPath, { recursive: true });
64+
await fs.writeFile(aSketchFilePath, '', { encoding: 'utf8' });
65+
await fs.writeFile(bSketchFilePath, '', { encoding: 'utf8' });
66+
const actual = await isAccessibleSketchPath(sketchFolderPath);
67+
expect(actual).to.be.equal(aSketchFilePath);
68+
});
69+
70+
it('should ignore EACCESS (non-Windows)', async function () {
71+
if (isWindows) {
72+
// `stat` syscall does not result in an EACCESS on Windows after stripping the file permissions.
73+
// an `open` syscall would, but IDE2 on purpose does not check the files.
74+
// the sketch files are provided by the CLI after loading the sketch.
75+
return this.skip();
76+
}
77+
const sketchFolderPath = join(testDirPath, 'my_sketch');
78+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino');
79+
await fs.mkdir(sketchFolderPath, { recursive: true });
80+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
81+
await fs.chmod(mainSketchFilePath, 0o000); // remove all permissions
82+
await rejects(fs.readFile(mainSketchFilePath), ErrnoException.isEACCES);
83+
const actual = await isAccessibleSketchPath(sketchFolderPath);
84+
expect(actual).to.be.equal(mainSketchFilePath);
85+
});
86+
87+
it("should not be accessible when there are no '.ino' files in the folder", async () => {
88+
const sketchFolderPath = join(testDirPath, 'my_sketch');
89+
await fs.mkdir(sketchFolderPath, { recursive: true });
90+
const actual = await isAccessibleSketchPath(sketchFolderPath);
91+
expect(actual).to.be.undefined;
92+
});
93+
94+
it("should not be accessible when the main sketch file extension is not '.ino'", async () => {
95+
const sketchFolderPath = join(testDirPath, 'my_sketch');
96+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.cpp');
97+
await fs.mkdir(sketchFolderPath, { recursive: true });
98+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
99+
const actual = await isAccessibleSketchPath(sketchFolderPath);
100+
expect(actual).to.be.undefined;
101+
});
102+
103+
it('should handle ENOENT', async () => {
104+
const sketchFolderPath = join(testDirPath, 'my_sketch');
105+
const actual = await isAccessibleSketchPath(sketchFolderPath);
106+
expect(actual).to.be.undefined;
107+
});
108+
109+
it('should handle UNKNOWN (Windows)', async function () {
110+
if (!isWindows) {
111+
return this.skip();
112+
}
113+
this.timeout(60_000);
114+
const actual = await isAccessibleSketchPath('\\\\10.0.0.200\\path');
115+
expect(actual).to.be.undefined;
116+
});
117+
});
118+
19119
describe('sketches-service-impl', () => {
20120
let container: Container;
21121
let toDispose: DisposableCollection;

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.