@pkerschbaum/code-oss-file-service
Version:
VS Code ([microsoft/vscode](https://github.com/microsoft/vscode)) includes a rich "`FileService`" and "`DiskFileSystemProvider`" abstraction built on top of Node.js core modules (`fs`, `path`) and Electron's `shell` module. This package allows to use that
243 lines • 12.5 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.watchFileContents = exports.CHANGE_BUFFER_DELAY = exports.watchFolder = exports.watchFile = void 0;
const fs_1 = require("fs");
const cancellation_1 = require("../../base/common/cancellation");
const extpath_1 = require("../../base/common/extpath");
const lifecycle_1 = require("../../base/common/lifecycle");
const normalization_1 = require("../../base/common/normalization");
const path_1 = require("../../base/common/path");
const platform_1 = require("../../base/common/platform");
const pfs_1 = require("../../base/node/pfs");
function watchFile(path, onChange, onError) {
return doWatchNonRecursive({ path, isDirectory: false }, onChange, onError);
}
exports.watchFile = watchFile;
function watchFolder(path, onChange, onError) {
return doWatchNonRecursive({ path, isDirectory: true }, onChange, onError);
}
exports.watchFolder = watchFolder;
exports.CHANGE_BUFFER_DELAY = 100;
function doWatchNonRecursive(file, onChange, onError) {
// macOS: watching samba shares can crash VSCode so we do
// a simple check for the file path pointing to /Volumes
// (https://github.com/microsoft/vscode/issues/106879)
// TODO@electron this needs a revisit when the crash is
// fixed or mitigated upstream.
if (platform_1.isMacintosh && (0, extpath_1.isEqualOrParent)(file.path, '/Volumes/')) {
onError(`Refusing to watch ${file.path} for changes using fs.watch() for possibly being a network share where watching is unreliable and unstable.`);
return lifecycle_1.Disposable.None;
}
const originalFileName = (0, path_1.basename)(file.path);
const mapPathToStatDisposable = new Map();
let disposed = false;
let watcherDisposables = [(0, lifecycle_1.toDisposable)(() => {
mapPathToStatDisposable.forEach(disposable => (0, lifecycle_1.dispose)(disposable));
mapPathToStatDisposable.clear();
})];
try {
// Creating watcher can fail with an exception
const watcher = (0, fs_1.watch)(file.path);
watcherDisposables.push((0, lifecycle_1.toDisposable)(() => {
watcher.removeAllListeners();
watcher.close();
}));
// Folder: resolve children to emit proper events
const folderChildren = new Set();
if (file.isDirectory) {
pfs_1.Promises.readdir(file.path).then(children => children.forEach(child => folderChildren.add(child)));
}
watcher.on('error', (code, signal) => {
if (!disposed) {
onError(`Failed to watch ${file.path} for changes using fs.watch() (${code}, ${signal})`);
}
});
watcher.on('change', (type, raw) => {
if (disposed) {
return; // ignore if already disposed
}
// Normalize file name
let changedFileName = '';
if (raw) { // https://github.com/microsoft/vscode/issues/38191
changedFileName = raw.toString();
if (platform_1.isMacintosh) {
// Mac: uses NFD unicode form on disk, but we want NFC
// See also https://github.com/nodejs/node/issues/2165
changedFileName = (0, normalization_1.normalizeNFC)(changedFileName);
}
}
if (!changedFileName || (type !== 'change' && type !== 'rename')) {
return; // ignore unexpected events
}
// File path: use path directly for files and join with changed file name otherwise
const changedFilePath = file.isDirectory ? (0, path_1.join)(file.path, changedFileName) : file.path;
// File
if (!file.isDirectory) {
if (type === 'rename' || changedFileName !== originalFileName) {
// The file was either deleted or renamed. Many tools apply changes to files in an
// atomic way ("Atomic Save") by first renaming the file to a temporary name and then
// renaming it back to the original name. Our watcher will detect this as a rename
// and then stops to work on Mac and Linux because the watcher is applied to the
// inode and not the name. The fix is to detect this case and trying to watch the file
// again after a certain delay.
// In addition, we send out a delete event if after a timeout we detect that the file
// does indeed not exist anymore.
const timeoutHandle = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
const fileExists = yield pfs_1.Promises.exists(changedFilePath);
if (disposed) {
return; // ignore if disposed by now
}
// File still exists, so emit as change event and reapply the watcher
if (fileExists) {
onChange('changed', changedFilePath);
watcherDisposables = [doWatchNonRecursive(file, onChange, onError)];
}
// File seems to be really gone, so emit a deleted event
else {
onChange('deleted', changedFilePath);
}
}), exports.CHANGE_BUFFER_DELAY);
// Very important to dispose the watcher which now points to a stale inode
// and wire in a new disposable that tracks our timeout that is installed
(0, lifecycle_1.dispose)(watcherDisposables);
watcherDisposables = [(0, lifecycle_1.toDisposable)(() => clearTimeout(timeoutHandle))];
}
else {
onChange('changed', changedFilePath);
}
}
// Folder
else {
// Children add/delete
if (type === 'rename') {
// Cancel any previous stats for this file path if existing
const statDisposable = mapPathToStatDisposable.get(changedFilePath);
if (statDisposable) {
(0, lifecycle_1.dispose)(statDisposable);
}
// Wait a bit and try see if the file still exists on disk to decide on the resulting event
const timeoutHandle = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
mapPathToStatDisposable.delete(changedFilePath);
const fileExists = yield pfs_1.Promises.exists(changedFilePath);
if (disposed) {
return; // ignore if disposed by now
}
// Figure out the correct event type:
// File Exists: either 'added' or 'changed' if known before
// File Does not Exist: always 'deleted'
let type;
if (fileExists) {
if (folderChildren.has(changedFileName)) {
type = 'changed';
}
else {
type = 'added';
folderChildren.add(changedFileName);
}
}
else {
folderChildren.delete(changedFileName);
type = 'deleted';
}
onChange(type, changedFilePath);
}), exports.CHANGE_BUFFER_DELAY);
mapPathToStatDisposable.set(changedFilePath, (0, lifecycle_1.toDisposable)(() => clearTimeout(timeoutHandle)));
}
// Other events
else {
// Figure out the correct event type: if this is the
// first time we see this child, it can only be added
let type;
if (folderChildren.has(changedFileName)) {
type = 'changed';
}
else {
type = 'added';
folderChildren.add(changedFileName);
}
onChange(type, changedFilePath);
}
}
});
}
catch (error) {
pfs_1.Promises.exists(file.path).then(exists => {
if (exists && !disposed) {
onError(`Failed to watch ${file.path} for changes using fs.watch() (${error.toString()})`);
}
});
}
return (0, lifecycle_1.toDisposable)(() => {
disposed = true;
watcherDisposables = (0, lifecycle_1.dispose)(watcherDisposables);
});
}
/**
* Watch the provided `path` for changes and return
* the data in chunks of `Uint8Array` for further use.
*/
function watchFileContents(path, onData, token, bufferSize = 512) {
return __awaiter(this, void 0, void 0, function* () {
const handle = yield pfs_1.Promises.open(path, 'r');
const buffer = Buffer.allocUnsafe(bufferSize);
const cts = new cancellation_1.CancellationTokenSource(token);
let error = undefined;
let isReading = false;
const watcher = watchFile(path, (type) => __awaiter(this, void 0, void 0, function* () {
if (type === 'changed') {
if (isReading) {
return; // return early if we are already reading the output
}
isReading = true;
try {
// Consume the new contents of the file until finished
// everytime there is a change event signalling a change
while (!cts.token.isCancellationRequested) {
const { bytesRead } = yield pfs_1.Promises.read(handle, buffer, 0, bufferSize, null);
if (!bytesRead || cts.token.isCancellationRequested) {
break;
}
onData(buffer.slice(0, bytesRead));
}
}
catch (err) {
error = new Error(err);
cts.dispose(true);
}
finally {
isReading = false;
}
}
}), err => {
error = new Error(err);
cts.dispose(true);
});
return new Promise((resolve, reject) => {
cts.token.onCancellationRequested(() => __awaiter(this, void 0, void 0, function* () {
watcher.dispose();
yield pfs_1.Promises.close(handle);
if (error) {
reject(error);
}
else {
resolve();
}
}));
});
});
}
exports.watchFileContents = watchFileContents;
//# sourceMappingURL=watcher.js.map
;