Skip to content

Commit bed3741

Browse files
author
Akos Kitta
committed
fix: handle UNKNOWN code on syscall: 'stat'
Closes #2166 Signed-off-by: Akos Kitta <[email protected]>
1 parent 9a6a457 commit bed3741

File tree

3 files changed

+119
-4
lines changed

3 files changed

+119
-4
lines changed

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

+2-1
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

+22-2
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

+95-1
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,110 @@ 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 './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', async () => {
71+
const sketchFolderPath = join(testDirPath, 'my_sketch');
72+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino');
73+
await fs.mkdir(sketchFolderPath, { recursive: true });
74+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
75+
await fs.chmod(mainSketchFilePath, 0o000); // remove all permissions
76+
await rejects(fs.readFile(mainSketchFilePath), ErrnoException.isEACCES);
77+
const actual = await isAccessibleSketchPath(sketchFolderPath);
78+
expect(actual).to.be.equal(mainSketchFilePath);
79+
});
80+
81+
it("should not be accessible when there are no '.ino' files in the folder", async () => {
82+
const sketchFolderPath = join(testDirPath, 'my_sketch');
83+
await fs.mkdir(sketchFolderPath, { recursive: true });
84+
const actual = await isAccessibleSketchPath(sketchFolderPath);
85+
expect(actual).to.be.undefined;
86+
});
87+
88+
it("should not be accessible when the main sketch file extension is not '.ino'", async () => {
89+
const sketchFolderPath = join(testDirPath, 'my_sketch');
90+
const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.cpp');
91+
await fs.mkdir(sketchFolderPath, { recursive: true });
92+
await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' });
93+
const actual = await isAccessibleSketchPath(sketchFolderPath);
94+
expect(actual).to.be.undefined;
95+
});
96+
97+
it('should handle ENOENT', async () => {
98+
const sketchFolderPath = join(testDirPath, 'my_sketch');
99+
const actual = await isAccessibleSketchPath(sketchFolderPath);
100+
expect(actual).to.be.undefined;
101+
});
102+
103+
it('should handle UNKNOWN (Windows)', async function () {
104+
if (!isWindows) {
105+
return this.skip();
106+
}
107+
this.timeout(60_000);
108+
const actual = isAccessibleSketchPath('\\\\10.0.0.200\\path');
109+
expect(actual).to.be.undefined;
110+
});
111+
});
112+
19113
describe('sketches-service-impl', () => {
20114
let container: Container;
21115
let toDispose: DisposableCollection;

0 commit comments

Comments
 (0)