@cocalc/project
Version:
CoCalc: project daemon
121 lines (117 loc) • 4.8 kB
JavaScript
"use strict";
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Watcher = void 0;
/*
Slightly generalized fs.watch that works even when the directory doesn't exist,
but also doesn't provide any information about what changed.
NOTE: We could maintain the directory listing and just try to update info about the filename,
taking into account the type. That's probably really hard to get right, and just
debouncing and computing the whole listing is going to be vastly easier and good
enough at least for first round of this.
We assume path is relative to HOME and contained inside of HOME.
The code below deals with two very different cases:
- when that path doesn't exist: use fs.watch on the parent directory.
NOTE: this case can't happen when path='', which exists, so we can assume to have read perms on parent.
- when the path does exist: use fs.watch (hence inotify) on the path itself to report when it changes
NOTE: if you are running on a filesystem like NFS, inotify won't work well or not at all.
In that case, set the env variable COCALC_FS_WATCHER=poll to use polling instead.
You can configure the poll interval by setting COCALC_FS_WATCHER_POLL_INTERVAL_MS.
*/
const chokidar_1 = require("chokidar");
const path_1 = require("path");
const events_1 = require("events");
const lodash_1 = require("lodash");
const async_utils_node_1 = require("../jupyter/async-utils-node");
const misc_1 = require("@cocalc/util/misc");
const logger_1 = require("@cocalc/project/logger");
const L = (0, logger_1.getLogger)("fs-watcher");
const COCALC_FS_WATCHER = process.env.COCALC_FS_WATCHER ?? "inotify";
if (!["inotify", "poll"].includes(COCALC_FS_WATCHER)) {
throw new Error(`$COCALC_FS_WATCHER=${COCALC_FS_WATCHER} -- must be "inotify" or "poll"`);
}
const POLLING = COCALC_FS_WATCHER === "poll";
const DEFAULT_POLL_MS = parseInt(process.env.COCALC_FS_WATCHER_POLL_INTERVAL_MS ?? "2000");
const ChokidarOpts = {
persistent: true,
followSymlinks: false,
disableGlobbing: true,
usePolling: POLLING,
interval: DEFAULT_POLL_MS,
binaryInterval: DEFAULT_POLL_MS,
depth: 0,
// maybe some day we want this:
// awaitWriteFinish: {
// stabilityThreshold: 100,
// pollInterval: 50,
// },
ignorePermissionErrors: true,
};
class Watcher extends events_1.EventEmitter {
constructor(path, debounce_ms) {
super();
this.watchExistenceChange = (containing_path) => async (_, filename) => {
const path = (0, path_1.join)(containing_path, filename);
if (path != this.path)
return;
const e = await (0, async_utils_node_1.exists)(this.path);
if (!this.exists && e) {
// it sprung into existence
this.exists = e;
this.initWatchContents();
this.change();
}
else if (this.exists && !e) {
// it got deleted
this.exists = e;
if (this.watchContents != null) {
this.watchContents.close();
delete this.watchContents;
}
this.change();
}
};
this.log = L.extend(path).debug;
this.log(`initalizing: poll=${POLLING}`);
if (process.env.HOME == null)
throw Error("bug -- HOME must be defined");
this.path = (0, path_1.join)(process.env.HOME, path);
this.debounce_ms = debounce_ms;
this.debouncedChange = (0, lodash_1.debounce)(this.change.bind(this), this.debounce_ms, {
leading: true,
trailing: true,
}).bind(this);
this.init();
}
async init() {
this.exists = await (0, async_utils_node_1.exists)(this.path);
if (this.path != "") {
this.initWatchExistence();
}
if (this.exists) {
this.initWatchContents();
}
}
initWatchContents() {
this.watchContents = (0, chokidar_1.watch)(this.path, ChokidarOpts);
this.watchContents.on("all", this.debouncedChange);
}
async initWatchExistence() {
const containing_path = (0, misc_1.path_split)(this.path).head;
this.watchExistence = (0, chokidar_1.watch)(containing_path, ChokidarOpts);
this.watchExistence.on("all", this.watchExistenceChange(containing_path));
}
change() {
this.emit("change");
}
close() {
this.watchExistence?.close();
this.watchContents?.close();
(0, misc_1.close)(this);
}
}
exports.Watcher = Watcher;
//# sourceMappingURL=path-watcher.js.map