UNPKG

@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
"use strict"; /*--------------------------------------------------------------------------------------------- * 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