1 |
|
2 | var debug = require('debug')('dev-server:watch');
|
3 | var fs = require('fs');
|
4 | var path = require('path');
|
5 |
|
6 | var notify = Function.prototype;
|
7 |
|
8 |
|
9 |
|
10 | Watcher.ignoreNames = ['.git', '.svn', '_svn', '_git'];
|
11 | Watcher.DEFER_TIMEOUT = 100;
|
12 |
|
13 |
|
14 |
|
15 | function Watcher(root) {
|
16 | this.root = root;
|
17 | this._pendingDiscovery = 0;
|
18 | this._watch(root);
|
19 | this._seenStat = {};
|
20 | this._targets = [];
|
21 | debug('Watcher started in ' + root);
|
22 | }
|
23 |
|
24 |
|
25 | Watcher.prototype._error = function(err) {
|
26 | console.error('Watcher error: ', err);
|
27 | };
|
28 |
|
29 |
|
30 |
|
31 | Watcher.prototype._decPending = function() {
|
32 | this._pendingDiscovery--;
|
33 | if (this._pendingDiscovery === 0) {
|
34 | debug('All discovered');
|
35 | notify('Watching for changes', 'info');
|
36 | }
|
37 | };
|
38 |
|
39 |
|
40 |
|
41 | Watcher.prototype._watch = function(name) {
|
42 | var self = this;
|
43 | self._pendingDiscovery++;
|
44 |
|
45 | fs.lstat(name, function(err, stat) {
|
46 | if (err) {
|
47 | self._decPending();
|
48 | return self._error(err);
|
49 | }
|
50 | var relative = path.relative(self.root, name);
|
51 | self._seenStat[relative] = stat || null;
|
52 | if (stat.isDirectory()) {
|
53 | self._watchDir(name);
|
54 | } else {
|
55 | self._decPending();
|
56 | }
|
57 | });
|
58 | };
|
59 |
|
60 |
|
61 |
|
62 | Watcher.prototype._watchDir = function(root) {
|
63 | var self = this;
|
64 |
|
65 | fs.watch(root, function(ev, filename) {
|
66 | debug('fs.watch', arguments);
|
67 | self._change(root, filename, ev);
|
68 | });
|
69 |
|
70 | fs.readdir(root, function(err, files) {
|
71 | if (err) {
|
72 | self._decPending();
|
73 | return self._error(err);
|
74 | }
|
75 |
|
76 | files.forEach(function(name) {
|
77 | if (Watcher.ignoreNames.indexOf(name) !== -1)
|
78 | return;
|
79 |
|
80 | var fullname = path.join(root, name);
|
81 | self._watch(fullname);
|
82 | });
|
83 | self._decPending();
|
84 | });
|
85 | };
|
86 |
|
87 |
|
88 | Watcher._clearDeferTimeout = function() {
|
89 | Watcher._defered = {
|
90 | timeout: null,
|
91 | all: []
|
92 | };
|
93 | };
|
94 | Watcher._clearDeferTimeout();
|
95 |
|
96 |
|
97 |
|
98 | Watcher.prototype._defer = function(method, relative, stat, oldStat) {
|
99 | var data = Watcher._defered;
|
100 | if (data.timeout) clearTimeout(data.timeout);
|
101 | data.all.push({
|
102 | method: method,
|
103 | path: relative,
|
104 | stat: stat,
|
105 | oldStat: oldStat
|
106 | });
|
107 | data.timeout = setTimeout(
|
108 | this._deferTimeout.bind(this), Watcher.DEFER_TIMEOUT);
|
109 | };
|
110 |
|
111 |
|
112 |
|
113 | Watcher.prototype._deferTimeout = function() {
|
114 | var data = Watcher._defered;
|
115 | var self = this;
|
116 | var byInode = {};
|
117 |
|
118 | data.all.forEach(function(item) {
|
119 | if (item.stat && item.stat.ino) {
|
120 | var key = item.stat.dev + ':' + item.stat.ino;
|
121 | if (byInode[key]) {
|
122 | var item2 = byInode[key];
|
123 |
|
124 | if (item.method === item2.method) return;
|
125 |
|
126 | if (item2.method === 'deleted') {
|
127 | var tmp = item; item = item2; item2 = tmp;
|
128 | }
|
129 |
|
130 | if (item.method === 'deleted' && item2.method === 'created') {
|
131 | item2.method = 'moved';
|
132 | item2.from = item.path;
|
133 | byInode[key] = item2;
|
134 | } else {
|
135 | debug('Multiple events?', [item, item2]);
|
136 | }
|
137 | } else {
|
138 | byInode[key] = item;
|
139 | }
|
140 | } else {
|
141 | debug('Item has no stat or inode info', item);
|
142 | }
|
143 | });
|
144 |
|
145 | Object.getOwnPropertyNames(byInode).forEach(function(key) {
|
146 | var ev = byInode[key];
|
147 | self._targets.forEach(function(target) {
|
148 | if (target.target instanceof RegExp) {
|
149 | if (target.target.test(ev.path)) {
|
150 | debug('Invoking custom hook for ' + ev.path);
|
151 | target.callback(ev);
|
152 | }
|
153 | }
|
154 | });
|
155 | });
|
156 |
|
157 | Watcher._clearDeferTimeout();
|
158 | };
|
159 |
|
160 |
|
161 |
|
162 | Watcher.prototype._updateStat = function(absolute, relative, err, stat) {
|
163 | var oldStat = this._seenStat[relative];
|
164 | var newStat = this._seenStat[relative] = stat || null;
|
165 | if (newStat && !oldStat) {
|
166 | this._defer('created', relative, newStat);
|
167 |
|
168 | if (newStat.isDirectory())
|
169 | this._watchDir(absolute);
|
170 |
|
171 | } else if (!newStat && oldStat) {
|
172 | this._defer('deleted', relative, oldStat);
|
173 | } else if (!newStat && !oldStat) {
|
174 |
|
175 | if (err) {
|
176 | if (err.code === 'ENOENT') {
|
177 | this._defer('deleted', relative);
|
178 | } else {
|
179 | notify('Error stating ' + relative + ': ' + err.toString());
|
180 | }
|
181 | } else {
|
182 | notify('No old, no new and no error, WTF?? ' + relative);
|
183 | }
|
184 | } else {
|
185 | if ((newStat.mtime.getTime() !== oldStat.mtime.getTime()) ||
|
186 | (newStat.size !== oldStat.size))
|
187 | {
|
188 | this._defer('updated', relative, newStat, oldStat);
|
189 | }
|
190 | }
|
191 | };
|
192 |
|
193 |
|
194 |
|
195 | Watcher.prototype._change = function(root, filename, ev) {
|
196 | var absolute = path.join(root, filename);
|
197 | var relative = path.relative(this.root, absolute);
|
198 | var self = this;
|
199 | fs.lstat(absolute, function(err, stat) {
|
200 | self._updateStat(absolute, relative, err, stat);
|
201 | });
|
202 | };
|
203 |
|
204 |
|
205 |
|
206 | Watcher.prototype.add = function(target, callback) {
|
207 | this._targets.push({target: target, callback: callback});
|
208 | };
|
209 |
|
210 |
|
211 |
|
212 | module.exports = function(app) {
|
213 | app.watch = function(target, callback) {
|
214 | if (!app._watcher)
|
215 | app._watcher = new Watcher(app.root);
|
216 | return app._watcher.add.apply(app._watcher, arguments);
|
217 | };
|
218 | notify = function() {
|
219 | debug(arguments);
|
220 | return app.notify.apply(null, arguments);
|
221 | }
|
222 | };
|