-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmanager.js
More file actions
executable file
·277 lines (220 loc) · 6.95 KB
/
Copy pathmanager.js
File metadata and controls
executable file
·277 lines (220 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
var util = require('util')
, fs = require('fs')
, spawn = require('child_process').spawn
, Inotify = require('inotify').Inotify
, inotify = new Inotify()
module.exports = function(options) {
return new NodeManager(options)
}
/**
* The manager which watches files and restarts
* Main options:
* run {string!}: the js-file to run
* e.g "./app.js", no default, required
*
* watchDir {string} the folder to watch, default: '.'
*
* ignoredPaths {array}: array of ignored paths, which shouldn't not be watched
* members can be
* strings (matched as ===),
* regexps (like extension check) or
* function(p) { returns true if path p should be ignored }
* default: []
*
*
* Logging options
* debug {bool}: adds additional output about watches and changes, default: false
* logger {object}: custom logger, must have error and debug methods
*
*/
function NodeManager(options) {
// public interface
this.watchFile = watchFile
this.watchFolder = watchFolder
this.run = run
this.start = start
if (!options.run) {
throw new Error("Please provide what to run. E.g. new NodeManager({ run: './app.js' })")
}
var ignoredPaths = options.ignoredPaths || []
var logger = options.logger || {
debug: function() {
options.debug && console.log.apply(console, arguments)
},
error: function() {
console.log.apply(console, arguments)
}
}
var onRunOutput = options.onRunOutput || function(data) {
util.print(data);
}
var onRunError = options.onRunError || function(data) {
util.print(data);
}
/**
* Run with default watch config
*/
function start() {
var dir = options.watchDir || '.'
this.run();
this.watchFile(dir); // watch current folder
this.watchFolder(dir); // watch all files under current folder
process.stdin.resume();
process.stdin.setEncoding('utf8');
}
var child // references the child process, after spawned
// Kill child process before ending
process.on('SIGTERM', function () {
if (child) {
child.kill();
}
process.exit();
});
/**
* executes the command given by the argument
*/
function run() {
// run the server
var node = options.node || options.run.match(/\.coffee$/) ? 'coffee' : 'node'
child = spawn(node, [options.run].concat(process.argv.slice(3)));
// let the child's output escape.
child.stdout.on('data', onRunOutput);
child.stderr.on('data', onRunError);
// let the user's typing get to the child
process.stdin.pipe(child.stdin);
console.log('\nStarting: ' + options.run);
}
var restartIsScheduled
, restartTimer
, lastRestartTime
/**
* restarts the server
* doesn't restart more often than once per 10 ms
* suspends restart a little bit in case of massive changes to save CPU
*/
function restart() {
if (restartIsScheduled) return
if (lastRestartTime && lastRestartTime + 500 > new Date) {
// if last restart was recent, we postpone new restart a bit to save resources,
// because it often means that many files are changed at once
schedule(150)
} else {
// in any way we schedule restart in next 10 ms, so many change events on one file/dir lead to single restart
schedule(10)
}
function schedule(ms) {
restartIsScheduled = true
setTimeout(function() {
restartIsScheduled = false
doRestart()
}, ms)
}
function doRestart() {
lastRestartTime = +new Date
// kill if running
if (child) child.kill();
// run it again
run();
}
}
/**
* watches all files and subdirectories in given folder (recursively),
* excluding the folder itself
* @param root
*/
function watchFolder(root) {
var files = fs.readdirSync(root);
files.forEach(function(file) {
var path = root + '/' + file
, stat = fs.statSync(path);
// watch file/folder
if (isIgnoredPath(path)) {
logger.debug("ignored path " + path)
return
}
// ignore absent files
if (!stat) {
logger.error("ERROR: couldn't stat " + path)
return
}
watchFile(path);
// recur if directory
if (stat.isDirectory()) {
watchFolder(path);
}
});
}
function isIgnoredPath(path) {
for (var i = 0; i < ignoredPaths.length; i++) {
var test = ignoredPaths[i];
logger.debug("isIgnoredPath: path " + path + " tested against " + test);
if (test === path) return true;
if (test instanceof RegExp && test.test(path)) return true;
if (test instanceof Function && test(path)) return true;
}
logger.debug("path is ok: " + path)
return false;
}
// arrays for debug events output
var flag2event = {}
var eventsAll = [
"IN_ACCESS", "IN_ATTRIB", "IN_CLOSE", "IN_CLOSE_NOWRITE", "IN_CLOSE_WRITE", "IN_CREATE",
"IN_DELETE", "IN_DELETE_SELF", "IN_DONT_FOLLOW", "IN_IGNORED", "IN_ISDIR",
"IN_MASK_ADD", "IN_MODIFY", "IN_MOVE", "IN_MOVED_FROM", "IN_MOVED_TO", "IN_MOVE_SELF",
"IN_ONESHOT", "IN_ONLYDIR", "IN_OPEN", "IN_Q_OVERFLOW", "IN_UNMOUNT"
]
eventsAll.forEach(function(event) {
flag2event[Inotify[event]] = event
})
function watchFile(file) {
logger.debug("Watch: " + file);
function callback(event) {
try {
event.toString = function() {
// translate event flags into string
// e.g IN_CLOSE | IN_CLOSE_WRITE
var txt = []
eventsAll.forEach(function(e) {
if (Inotify[e] & this.mask) txt.push(e)
}.bind(this))
return txt.join(' | ')
}
if (event.name) { // name of updated file inside watched folder
var path = file + '/' + event.name
if (isIgnoredPath(path)) {
return
}
if (event.mask & Inotify.IN_CREATE) { // IN_CREATE always comes here
// when a new file is created we wait for it's close_write
watchFile(path)
if (fs.statSync(path).isDirectory()) {
watchFolder(path)
}
return
}
logger.debug(path + ' ' + event);
restart()
} else {
logger.debug(file + ' ' + event);
restart()
}
}
catch (exception) {
// Ignore exceptions caused by missing files (temp files)
if (exception.code !== 'ENOENT') {
throw exception;
}
}
}
// we watch for these events for files and folders
// there may be many IN_MODIFY, so we don't watch it, but we await for IN_CLOSE_WRITE
var watchEvents = [ 'IN_CLOSE_WRITE' , 'IN_CREATE' , 'IN_DELETE' , 'IN_DELETE_SELF' , 'IN_MOVE_SELF', 'IN_MOVE' ];
inotify.addWatch({
path: file,
watch_for: watchEvents.reduce(function(prev, cur) {
return prev | Inotify[cur]
}, 0),
callback: callback
})
}
}