Skip to content

Commit

Permalink
fix: avoid locking file descriptors of natively opened files
Browse files Browse the repository at this point in the history
  • Loading branch information
connor4312 committed Dec 15, 2021
1 parent 73c57ce commit ade92be
Showing 1 changed file with 90 additions and 24 deletions.
114 changes: 90 additions & 24 deletions src/fileSystemAdaptor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
import type fs from "fs";
import * as vscode from "vscode";
import { FileAccessor, FileWriteOp } from "../shared/fileAccessor";
import { once } from "../shared/util/once";
import { Policy } from "cockatiel";

export const accessFile = async (uri: vscode.Uri, untitledDocumentData?: Uint8Array): Promise<FileAccessor> => {
if (uri.scheme === "untitled") {
Expand All @@ -26,45 +25,121 @@ export const accessFile = async (uri: vscode.Uri, untitledDocumentData?: Uint8Ar
return new SimpleFileAccessor(uri);
};

class FileHandleContainer {
private borrowQueue: ((h: fs.promises.FileHandle | Error) => Promise<void>)[] = [];
private handle?: fs.promises.FileHandle;
private disposeTimeout?: NodeJS.Timeout;
private disposed = false;

constructor(
private readonly path: string,
private readonly _fs: typeof fs,
) {}

/** Borrows the file handle to run the function. */
public borrow<R>(fn: (handle: fs.promises.FileHandle) => R): Promise<R> {
if (this.disposed) {
return Promise.reject(new Error("FileHandle was disposed"));
}

return new Promise<R>((resolve, reject) => {
this.borrowQueue.push(async handle => {
if (handle instanceof Error) {
return reject(handle);
}

try {
resolve(await fn(handle));
} catch (e) {
reject(e);
}
});

if (this.borrowQueue.length === 1) {
this.process();
}
});
}

public dispose() {
this.disposed = true;
this.handle = undefined;
if (this.disposeTimeout) {
clearTimeout(this.disposeTimeout);
}
this.rejectAll(new Error("FileHandle was disposed"));
}

private rejectAll(error: Error) {
while (this.borrowQueue.length) {
this.borrowQueue.pop()!(error);
}
}

private async process() {
if (this.disposeTimeout) {
clearTimeout(this.disposeTimeout);
}

if (!this.handle) {
try {
this.handle = await this._fs.promises.open(this.path, this._fs.constants.O_RDWR | this._fs.constants.O_CREAT);
} catch (e) {
return this.rejectAll(e as Error);
}
}

while (this.borrowQueue.length) {
const fn = this.borrowQueue.pop()!;
await fn(this.handle);
}

// When no one is using the handle, close it after some time. Otherwise the
// filesystem will lock the file which would be frustating to users.
this.disposeTimeout = setTimeout(() => {
this.handle?.close();
this.handle = undefined;
}, 1000);
}

}

/** Native accessor using Node's filesystem. This can be used. */
class NativeFileAccessor implements FileAccessor {
public readonly uri: string;
public readonly supportsIncremetalAccess = true;
private readonly fsPath: string;
private readonly writeGuard = Policy.bulkhead(1, Infinity);
private readonly handle: FileHandleContainer;

constructor(uri: vscode.Uri, private readonly fs: typeof import("fs")) {
this.uri = uri.toString();
this.fsPath = uri.fsPath;
this.handle = new FileHandleContainer(uri.fsPath, fs);
}

async getSize(): Promise<number | undefined> {
const fd = await this.getHandle();
return (await fd.stat()).size;
return this.handle.borrow(async fd => (await fd.stat()).size);
}

async read(offset: number, target: Uint8Array): Promise<number> {
const fd = await this.getHandle();
const { bytesRead } = await fd.read(target, 0, target.byteLength, offset);
return bytesRead;
return this.handle.borrow(async fd => {
const { bytesRead } = await fd.read(target, 0, target.byteLength, offset);
return bytesRead;
});
}

writeBulk(ops: readonly FileWriteOp[]): Promise<void> {
return this.writeGuard.execute(async () => {
const fd = await this.getHandle();
return this.handle.borrow<void>(async fd => {
for (const { data, offset } of ops) {
fd.write(data, 0, data.byteLength, offset);
}
});
}

async writeStream(stream: AsyncIterable<Uint8Array>, cancellation?: vscode.CancellationToken): Promise<void> {
return this.writeGuard.execute(async () => {
return this.handle.borrow(async fd => {
if (cancellation?.isCancellationRequested) {
return;
}

const fd = await this.getHandle();
let offset = 0;
for await (const chunk of stream) {
if (cancellation?.isCancellationRequested) {
Expand All @@ -78,17 +153,8 @@ class NativeFileAccessor implements FileAccessor {
}

public dispose() {
this.getHandle.getValue()?.then(h => h.close()).catch(() => { /* ignore */ });
this.handle.dispose();
}

private readonly getHandle = once(async () => {
try {
return await this.fs.promises.open(this.fsPath, this.fs.constants.O_RDWR | this.fs.constants.O_CREAT);
} catch (e) {
this.getHandle.forget();
throw e;
}
});
}

class SimpleFileAccessor implements FileAccessor {
Expand Down

0 comments on commit ade92be

Please sign in to comment.