Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81159 views
1
var assert = require("assert");
2
var path = require("path");
3
var fs = require("graceful-fs");
4
var spawn = require("child_process").spawn;
5
var Q = require("q");
6
var EventEmitter = require("events").EventEmitter;
7
var ReadFileCache = require("./cache").ReadFileCache;
8
var util = require("./util");
9
var hasOwn = Object.prototype.hasOwnProperty;
10
11
function Watcher(readFileCache, persistent) {
12
assert.ok(this instanceof Watcher);
13
assert.ok(this instanceof EventEmitter);
14
assert.ok(readFileCache instanceof ReadFileCache);
15
16
// During tests (and only during tests), persistent === false so that
17
// the test suite can actually finish and exit.
18
if (typeof persistent === "undefined") {
19
persistent = true;
20
}
21
22
EventEmitter.call(this);
23
24
var self = this;
25
var sourceDir = readFileCache.sourceDir;
26
var dirWatcher = new DirWatcher(sourceDir, persistent);
27
28
Object.defineProperties(self, {
29
sourceDir: { value: sourceDir },
30
readFileCache: { value: readFileCache },
31
dirWatcher: { value: dirWatcher }
32
});
33
34
// Watch everything the readFileCache already knows about, and any new
35
// files added in the future.
36
readFileCache.subscribe(function(relativePath) {
37
self.watch(relativePath);
38
});
39
40
readFileCache.on("changed", function(relativePath) {
41
self.emit("changed", relativePath);
42
});
43
44
function handleDirEvent(event, relativePath) {
45
if (self.dirWatcher.ready) {
46
self.getFileHandler(relativePath)(event);
47
}
48
}
49
50
dirWatcher.on("added", function(relativePath) {
51
handleDirEvent("added", relativePath);
52
}).on("deleted", function(relativePath) {
53
handleDirEvent("deleted", relativePath);
54
}).on("changed", function(relativePath) {
55
handleDirEvent("changed", relativePath);
56
});
57
}
58
59
util.inherits(Watcher, EventEmitter);
60
var Wp = Watcher.prototype;
61
62
Wp.watch = function(relativePath) {
63
this.dirWatcher.add(path.dirname(path.join(
64
this.sourceDir, relativePath)));
65
};
66
67
Wp.readFileP = function(relativePath) {
68
return this.readFileCache.readFileP(relativePath);
69
};
70
71
Wp.noCacheReadFileP = function(relativePath) {
72
return this.readFileCache.noCacheReadFileP(relativePath);
73
};
74
75
Wp.getFileHandler = util.cachedMethod(function(relativePath) {
76
var self = this;
77
return function handler(event) {
78
self.readFileCache.reportPossiblyChanged(relativePath);
79
};
80
});
81
82
function orNull(err) {
83
return null;
84
}
85
86
Wp.close = function() {
87
this.dirWatcher.close();
88
};
89
90
/**
91
* DirWatcher code adapted from Jeffrey Lin's original implementation:
92
* https://github.com/jeffreylin/jsx_transformer_fun/blob/master/dirWatcher.js
93
*
94
* Invariant: this only watches the dir inode, not the actual path.
95
* That means the dir can't be renamed and swapped with another dir.
96
*/
97
function DirWatcher(inputPath, persistent) {
98
assert.ok(this instanceof DirWatcher);
99
100
var self = this;
101
var absPath = path.resolve(inputPath);
102
103
if (!fs.statSync(absPath).isDirectory()) {
104
throw new Error(inputPath + "is not a directory!");
105
}
106
107
EventEmitter.call(self);
108
109
self.ready = false;
110
self.on("ready", function(){
111
self.ready = true;
112
});
113
114
Object.defineProperties(self, {
115
// Map of absDirPaths to fs.FSWatcher objects from fs.watch().
116
watchers: { value: {} },
117
dirContents: { value: {} },
118
rootPath: { value: absPath },
119
persistent: { value: !!persistent }
120
});
121
122
process.nextTick(function() {
123
self.add(absPath);
124
self.emit("ready");
125
});
126
}
127
128
util.inherits(DirWatcher, EventEmitter);
129
var DWp = DirWatcher.prototype;
130
131
DWp.add = function(absDirPath) {
132
var self = this;
133
if (hasOwn.call(self.watchers, absDirPath)) {
134
return;
135
}
136
137
self.watchers[absDirPath] = fs.watch(absDirPath, {
138
persistent: this.persistent
139
}).on("change", function(event, filename) {
140
self.updateDirContents(absDirPath, event, filename);
141
});
142
143
// Update internal dir contents.
144
self.updateDirContents(absDirPath);
145
146
// Since we've never seen this path before, recursively add child
147
// directories of this path. TODO: Don't do fs.readdirSync on the
148
// same dir twice in a row. We already do an fs.statSync in
149
// this.updateDirContents() and we're just going to do another one
150
// here...
151
fs.readdirSync(absDirPath).forEach(function(filename) {
152
var filepath = path.join(absDirPath, filename);
153
154
// Look for directories.
155
if (fs.statSync(filepath).isDirectory()) {
156
self.add(filepath);
157
}
158
});
159
};
160
161
DWp.updateDirContents = function(absDirPath, event, fsWatchReportedFilename) {
162
var self = this;
163
164
if (!hasOwn.call(self.dirContents, absDirPath)) {
165
self.dirContents[absDirPath] = [];
166
}
167
168
var oldContents = self.dirContents[absDirPath];
169
var newContents = fs.readdirSync(absDirPath);
170
171
var deleted = {};
172
var added = {};
173
174
oldContents.forEach(function(filename) {
175
deleted[filename] = true;
176
});
177
178
newContents.forEach(function(filename) {
179
if (hasOwn.call(deleted, filename)) {
180
delete deleted[filename];
181
} else {
182
added[filename] = true;
183
}
184
});
185
186
var deletedNames = Object.keys(deleted);
187
deletedNames.forEach(function(filename) {
188
self.emit(
189
"deleted",
190
path.relative(
191
self.rootPath,
192
path.join(absDirPath, filename)
193
)
194
);
195
});
196
197
var addedNames = Object.keys(added);
198
addedNames.forEach(function(filename) {
199
self.emit(
200
"added",
201
path.relative(
202
self.rootPath,
203
path.join(absDirPath, filename)
204
)
205
);
206
});
207
208
// So changed is not deleted or added?
209
if (fsWatchReportedFilename &&
210
!hasOwn.call(deleted, fsWatchReportedFilename) &&
211
!hasOwn.call(added, fsWatchReportedFilename))
212
{
213
self.emit(
214
"changed",
215
path.relative(
216
self.rootPath,
217
path.join(absDirPath, fsWatchReportedFilename)
218
)
219
);
220
}
221
222
// If any of the things removed were directories, remove their watchers.
223
// If a dir was moved, hopefully two changed events fired?
224
// 1) event in dir where it was removed
225
// 2) event in dir where it was moved to (added)
226
deletedNames.forEach(function(filename) {
227
var filepath = path.join(absDirPath, filename);
228
delete self.dirContents[filepath];
229
delete self.watchers[filepath];
230
});
231
232
// if any of the things added were directories, recursively deal with them
233
addedNames.forEach(function(filename) {
234
var filepath = path.join(absDirPath, filename);
235
if (fs.existsSync(filepath) &&
236
fs.statSync(filepath).isDirectory())
237
{
238
self.add(filepath);
239
// mighttttttt need a self.updateDirContents() here in case
240
// we're somehow adding a path that replaces another one...?
241
}
242
});
243
244
// Update state of internal dir contents.
245
self.dirContents[absDirPath] = newContents;
246
};
247
248
DWp.close = function() {
249
var watchers = this.watchers;
250
Object.keys(watchers).forEach(function(filename) {
251
watchers[filename].close();
252
});
253
};
254
255
exports.Watcher = Watcher;
256
257