Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/backend/path-watcher.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Watch A DIRECTORY for changes of the files in *that* directory only (not recursive).7Use ./watcher.ts for a single file.89Slightly generalized fs.watch that works even when the directory doesn't exist,10but also doesn't provide any information about what changed.1112NOTE: We could maintain the directory listing and just try to update info about the filename,13taking into account the type. That's probably really hard to get right, and just14debouncing and computing the whole listing is going to be vastly easier and good15enough at least for first round of this.1617We assume path is relative to HOME and contained inside of HOME.1819The code below deals with two very different cases:20- when that path doesn't exist: use fs.watch on the parent directory.21NOTE: this case can't happen when path='', which exists, so we can assume to have read perms on parent.22- when the path does exist: use fs.watch (hence inotify) on the path itself to report when it changes2324NOTE: if you are running on a file system like NFS, inotify won't work well or not at all.25In that case, set the env variable COCALC_FS_WATCHER=poll to use polling instead.26You can configure the poll interval by setting COCALC_FS_WATCHER_POLL_INTERVAL_MS.2728UPDATE: We are using polling in ALL cases. We have subtle bugs29with adding and removing directories otherwise, and also30we are only ever watching a relatively small number of directories31with a long interval, so polling is not so bad.32*/3334import { watch, WatchOptions } from "chokidar";35import { FSWatcher } from "fs";36import { join } from "path";37import { EventEmitter } from "events";38import { debounce } from "lodash";39import { exists } from "@cocalc/backend/misc/async-utils-node";40import { close, path_split } from "@cocalc/util/misc";41import { getLogger } from "./logger";4243const logger = getLogger("backend:path-watcher");4445// const COCALC_FS_WATCHER = process.env.COCALC_FS_WATCHER ?? "inotify";46// if (!["inotify", "poll"].includes(COCALC_FS_WATCHER)) {47// throw new Error(48// `$COCALC_FS_WATCHER=${COCALC_FS_WATCHER} -- must be "inotify" or "poll"`,49// );50// }51// const POLLING = COCALC_FS_WATCHER === "poll";5253const POLLING = true;5455const DEFAULT_POLL_MS = parseInt(56process.env.COCALC_FS_WATCHER_POLL_INTERVAL_MS ?? "3000",57);5859const ChokidarOpts: WatchOptions = {60persistent: true, // otherwise won't work61followSymlinks: false, // don't wander about62disableGlobbing: true, // watch the path as it is, that's it63usePolling: POLLING,64interval: DEFAULT_POLL_MS,65binaryInterval: DEFAULT_POLL_MS,66depth: 0, // we only care about the explicitly mentioned path – there could be a lot of files and sub-dirs!67// maybe some day we want this:68// awaitWriteFinish: {69// stabilityThreshold: 100,70// pollInterval: 50,71// },72ignorePermissionErrors: true,73alwaysStat: false,74} as const;7576export class Watcher extends EventEmitter {77private path: string;78private exists: boolean;79private watchContents?: FSWatcher;80private watchExistence?: FSWatcher;81private debounce_ms: number;82private debouncedChange: any;83private log: Function;8485constructor(86path: string,87{ debounce: debounce_ms = DEFAULT_POLL_MS }: { debounce?: number } = {},88) {89super();90this.log = logger.extend(path).debug;91this.log(`initializing: poll=${POLLING}`);92if (process.env.HOME == null) {93throw Error("bug -- HOME must be defined");94}95this.path = path.startsWith("/") ? path : join(process.env.HOME, path);96this.debounce_ms = debounce_ms;97this.debouncedChange = this.debounce_ms98? debounce(this.change, this.debounce_ms, {99leading: true,100trailing: true,101}).bind(this)102: this.change;103this.init();104}105106private async init(): Promise<void> {107this.log("init watching", this.path);108this.exists = await exists(this.path);109if (this.path != "") {110this.log("init watching", this.path, " for existence");111this.initWatchExistence();112}113if (this.exists) {114this.log("init watching", this.path, " contents");115this.initWatchContents();116}117}118119private initWatchContents(): void {120this.watchContents = watch(this.path, ChokidarOpts);121this.watchContents.on("all", this.debouncedChange);122this.watchContents.on("error", (err) => {123this.log(`error watching listings -- ${err}`);124});125}126127private async initWatchExistence(): Promise<void> {128const containing_path = path_split(this.path).head;129this.watchExistence = watch(containing_path, ChokidarOpts);130this.watchExistence.on("all", this.watchExistenceChange(containing_path));131this.watchExistence.on("error", (err) => {132this.log(`error watching for existence of ${this.path} -- ${err}`);133});134}135136private watchExistenceChange = (containing_path) => async (_, filename) => {137const path = join(containing_path, filename);138if (path != this.path) return;139const e = await exists(this.path);140if (!this.exists && e) {141// it sprung into existence142this.exists = e;143this.initWatchContents();144this.change();145} else if (this.exists && !e) {146// it got deleted147this.exists = e;148if (this.watchContents != null) {149this.watchContents.close();150delete this.watchContents;151}152153this.change();154}155};156157private change = (): void => {158this.emit("change");159};160161public close(): void {162this.watchExistence?.close();163this.watchContents?.close();164close(this);165}166}167168169