Skip to content

Commit

Permalink
fix: handle UNKNOWN code on syscall: 'stat'
Browse files Browse the repository at this point in the history
Closes #2166

Signed-off-by: Akos Kitta <[email protected]>
  • Loading branch information
Akos Kitta authored and kittaakos committed Aug 20, 2023
1 parent 420d31f commit b256655
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 4 deletions.
3 changes: 2 additions & 1 deletion arduino-ide-extension/src/node/sketches-service-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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;
Expand Down
24 changes: 22 additions & 2 deletions arduino-ide-extension/src/node/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit b256655

Please sign in to comment.