From eb2a1d9663247390c0eaa0bf05a01adfae709353 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Fri, 18 Aug 2023 11:51:37 +0200 Subject: [PATCH] fix: handle `UNKNOWN` code on `syscall: 'stat'` Closes #2166 Signed-off-by: Akos Kitta --- .../src/node/sketches-service-impl.ts | 3 +- .../src/node/utils/errors.ts | 24 ++++- .../node/sketches-service-impl.slow-test.ts | 102 +++++++++++++++++- 3 files changed, 125 insertions(+), 4 deletions(-) diff --git a/arduino-ide-extension/src/node/sketches-service-impl.ts b/arduino-ide-extension/src/node/sketches-service-impl.ts index 95493bb19..0fe8a2158 100644 --- a/arduino-ide-extension/src/node/sketches-service-impl.ts +++ b/arduino-ide-extension/src/node/sketches-service-impl.ts @@ -677,6 +677,7 @@ async function isInvalidSketchNameError( * * 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. + * This method handles `UNKNOWN` errors ([nodejs/node#19965](https://github.com/nodejs/node/issues/19965#issuecomment-380750573)). * * 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`. @@ -689,7 +690,7 @@ export async function isAccessibleSketchPath( try { stats = await fs.stat(path); } catch (err) { - if (ErrnoException.isENOENT(err)) { + if (ErrnoException.isENOENT(err) || ErrnoException.isUNKNOWN(err)) { return undefined; } throw err; diff --git a/arduino-ide-extension/src/node/utils/errors.ts b/arduino-ide-extension/src/node/utils/errors.ts index 952c43763..83ad84f5e 100644 --- a/arduino-ide-extension/src/node/utils/errors.ts +++ b/arduino-ide-extension/src/node/utils/errors.ts @@ -15,7 +15,16 @@ export namespace ErrnoException { } /** - * (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. + * _(Permission denied):_ An attempt was made to access a file in a way forbidden by its file access permissions. + */ + export function isEACCES( + arg: unknown + ): arg is ErrnoException & { code: 'EACCES' } { + return is(arg) && arg.code === 'EACCES'; + } + + /** + * _(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. */ export function isENOENT( arg: unknown @@ -24,11 +33,22 @@ export namespace ErrnoException { } /** - * (Not a directory): A component of the given pathname existed, but was not a directory as expected. Commonly raised by `fs.readdir`. + * _(Not a directory):_ A component of the given pathname existed, but was not a directory as expected. Commonly raised by `fs.readdir`. */ export function isENOTDIR( arg: unknown ): arg is ErrnoException & { code: 'ENOTDIR' } { return is(arg) && arg.code === 'ENOTDIR'; } + + /** + * _"That 4094 error code is a generic network-or-configuration error, Node.js just passes it on from the operating system."_ + * + * See [nodejs/node#19965](https://github.com/nodejs/node/issues/19965#issuecomment-380750573) for more details. + */ + export function isUNKNOWN( + arg: unknown + ): arg is ErrnoException & { code: 'UNKNOWN' } { + return is(arg) && arg.code === 'UNKNOWN'; + } } diff --git a/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts index 082a7474d..3454fb278 100644 --- a/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/sketches-service-impl.slow-test.ts @@ -6,16 +6,116 @@ import { isWindows } from '@theia/core/lib/common/os'; import { FileUri } from '@theia/core/lib/node/file-uri'; import { Container } from '@theia/core/shared/inversify'; import { expect } from 'chai'; +import { rejects } from 'node:assert/strict'; import { promises as fs } from 'node:fs'; import { basename, join } from 'node:path'; import { sync as rimrafSync } from 'rimraf'; +import temp from 'temp'; import { Sketch, SketchesService } from '../../common/protocol'; -import { SketchesServiceImpl } from '../../node/sketches-service-impl'; +import { + isAccessibleSketchPath, + SketchesServiceImpl, +} from '../../node/sketches-service-impl'; import { ErrnoException } from '../../node/utils/errors'; import { createBaseContainer, startDaemon } from './node-test-bindings'; const testTimeout = 10_000; +describe('isAccessibleSketchPath', () => { + let tracked: typeof temp; + let testDirPath: string; + + before(() => (tracked = temp.track())); + beforeEach(() => (testDirPath = tracked.mkdirSync())); + after(() => tracked.cleanupSync()); + + it('should be accessible by the main sketch file', async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' }); + const actual = await isAccessibleSketchPath(mainSketchFilePath); + expect(actual).to.be.equal(mainSketchFilePath); + }); + + it('should be accessible by the sketch folder', async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' }); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.equal(mainSketchFilePath); + }); + + it('should be accessible when the sketch folder and main sketch file basenames are different', async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const mainSketchFilePath = join(sketchFolderPath, 'other_name_sketch.ino'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' }); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.equal(mainSketchFilePath); + }); + + it('should be deterministic (and sort by basename) when multiple sketch files exist', async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const aSketchFilePath = join(sketchFolderPath, 'a.ino'); + const bSketchFilePath = join(sketchFolderPath, 'b.ino'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(aSketchFilePath, '', { encoding: 'utf8' }); + await fs.writeFile(bSketchFilePath, '', { encoding: 'utf8' }); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.equal(aSketchFilePath); + }); + + it('should ignore EACCESS (non-Windows)', async function () { + if (isWindows) { + // `stat` syscall does not result in an EACCESS on Windows after stripping the file permissions. + // an `open` syscall would, but IDE2 on purpose does not check the files. + // the sketch files are provided by the CLI after loading the sketch. + return this.skip(); + } + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.ino'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' }); + await fs.chmod(mainSketchFilePath, 0o000); // remove all permissions + await rejects(fs.readFile(mainSketchFilePath), ErrnoException.isEACCES); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.equal(mainSketchFilePath); + }); + + it("should not be accessible when there are no '.ino' files in the folder", async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.undefined; + }); + + it("should not be accessible when the main sketch file extension is not '.ino'", async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const mainSketchFilePath = join(sketchFolderPath, 'my_sketch.cpp'); + await fs.mkdir(sketchFolderPath, { recursive: true }); + await fs.writeFile(mainSketchFilePath, '', { encoding: 'utf8' }); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.undefined; + }); + + it('should handle ENOENT', async () => { + const sketchFolderPath = join(testDirPath, 'my_sketch'); + const actual = await isAccessibleSketchPath(sketchFolderPath); + expect(actual).to.be.undefined; + }); + + it('should handle UNKNOWN (Windows)', async function () { + if (!isWindows) { + return this.skip(); + } + this.timeout(60_000); + const actual = await isAccessibleSketchPath('\\\\10.0.0.200\\path'); + expect(actual).to.be.undefined; + }); +}); + describe('sketches-service-impl', () => { let container: Container; let toDispose: DisposableCollection;