Skip to content

fix: handle UNKNOWN code on syscall: 'stat' #2174

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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