blob: 606e914dbc2ee0e18a08d65afe1438c5f8aac676 [file] [log] [blame]
Jean-Baptiste Querub56ea2a2013-01-08 11:11:20 -08001/*
2 * Copyright 2000-2012 JetBrains s.r.o.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include "fsnotifier.h"
18
19#include <errno.h>
20#include <limits.h>
21#include <mntent.h>
22#include <paths.h>
23#include <stdarg.h>
24#include <stdlib.h>
25#include <string.h>
26#include <sys/inotify.h>
27#include <sys/select.h>
28#include <syslog.h>
29#include <unistd.h>
30
31#define LOG_ENV "FSNOTIFIER_LOG_LEVEL"
32#define LOG_ENV_DEBUG "debug"
33#define LOG_ENV_INFO "info"
34#define LOG_ENV_WARNING "warning"
35#define LOG_ENV_ERROR "error"
36#define LOG_ENV_OFF "off"
37
38#define VERSION "1.2"
39#define VERSION_MSG "fsnotifier " VERSION "\n"
40
41#define USAGE_MSG \
42 "fsnotifier - IntelliJ IDEA companion program for watching and reporting file and directory structure modifications.\n\n" \
43 "fsnotifier utilizes \"user\" facility of syslog(3) - messages usually can be found in /var/log/user.log.\n" \
44 "Verbosity is regulated via " LOG_ENV " environment variable, possible values are: " \
45 LOG_ENV_DEBUG ", " LOG_ENV_INFO ", " LOG_ENV_WARNING ", " LOG_ENV_ERROR ", " LOG_ENV_OFF "; latter is the default.\n\n" \
46 "Use 'fsnotifier --selftest' to perform some self-diagnostics (output will be logged and printed to console).\n"
47
48#define HELP_MSG \
49 "Try 'fsnotifier --help' for more information.\n"
50
51#define INOTIFY_LIMIT_MSG \
52 "The current <b>inotify</b>(7) watch limit of %d is too low. " \
53 "<a href=\"http://confluence.jetbrains.net/display/IDEADEV/Inotify+Watches+Limit\">More details.</a>\n"
54
55typedef struct {
56 char* name;
57 int id;
58} watch_root;
59
60static array* roots = NULL;
61
62static bool show_warning = true;
63static bool self_test = false;
64
65static void init_log();
66static void run_self_test();
67static void main_loop();
68static bool read_input();
69static bool update_roots(array* new_roots);
70static void unregister_roots();
71static bool register_roots(array* new_roots, array* unwatchable, array* mounts);
72static array* unwatchable_mounts();
73static void inotify_callback(char* path, int event);
74static void output(const char* format, ...);
75
76
77int main(int argc, char** argv) {
78 if (argc > 1) {
79 if (strcmp(argv[1], "--help") == 0) {
80 printf(USAGE_MSG);
81 return 0;
82 }
83 else if (strcmp(argv[1], "--version") == 0) {
84 printf(VERSION_MSG);
85 return 0;
86 }
87 else if (strcmp(argv[1], "--selftest") == 0) {
88 self_test = true;
89 }
90 else {
91 printf("unrecognized option: %s\n", argv[1]);
92 printf(HELP_MSG);
93 return 1;
94 }
95 }
96
97 init_log();
98 if (!self_test) {
99 userlog(LOG_INFO, "started (v." VERSION ")");
100 }
101 else {
102 userlog(LOG_INFO, "started (self-test mode) (v." VERSION ")");
103 }
104
105 setvbuf(stdin, NULL, _IONBF, 0);
106 setvbuf(stdout, NULL, _IONBF, 0);
107
108 roots = array_create(20);
109 if (init_inotify() && roots != NULL) {
110 set_inotify_callback(&inotify_callback);
111
112 if (!self_test) {
113 main_loop();
114 }
115 else {
116 run_self_test();
117 }
118
119 unregister_roots();
120 }
121 else {
122 printf("GIVEUP\n");
123 }
124 close_inotify();
125 array_delete(roots);
126
127 userlog(LOG_INFO, "finished");
128 closelog();
129
130 return 0;
131}
132
133
134static void init_log() {
135 char* env_level = getenv(LOG_ENV);
136 int level = LOG_EMERG;
137 if (env_level != NULL) {
138 if (strcmp(env_level, LOG_ENV_DEBUG) == 0) level = LOG_DEBUG;
139 else if (strcmp(env_level, LOG_ENV_INFO) == 0) level = LOG_INFO;
140 else if (strcmp(env_level, LOG_ENV_WARNING) == 0) level = LOG_WARNING;
141 else if (strcmp(env_level, LOG_ENV_ERROR) == 0) level = LOG_ERR;
142 }
143
144 if (self_test) {
145 level = LOG_DEBUG;
146 }
147
148 char ident[32];
149 snprintf(ident, sizeof(ident), "fsnotifier[%d]", getpid());
150 openlog(ident, 0, LOG_USER);
151 setlogmask(LOG_UPTO(level));
152}
153
154
155void userlog(int priority, const char* format, ...) {
156 va_list ap;
157
158 va_start(ap, format);
159 vsyslog(priority, format, ap);
160 va_end(ap);
161
162 if (self_test) {
163 const char* level = "debug";
164 switch (priority) {
165 case LOG_ERR: level = "error"; break;
166 case LOG_WARNING: level = " warn"; break;
167 case LOG_INFO: level = " info"; break;
168 }
169 printf("fsnotifier[%d] %s: ", getpid(), level);
170
171 va_start(ap, format);
172 vprintf(format, ap);
173 va_end(ap);
174
175 printf("\n");
176 }
177}
178
179
180static void run_self_test() {
181 array* test_roots = array_create(1);
182 char* cwd = malloc(PATH_MAX);
183 if (getcwd(cwd, PATH_MAX) == NULL) {
184 strncpy(cwd, ".", PATH_MAX);
185 }
186 array_push(test_roots, cwd);
187 update_roots(test_roots);
188}
189
190
191static void main_loop() {
192 int input_fd = fileno(stdin), inotify_fd = get_inotify_fd();
193 int nfds = (inotify_fd > input_fd ? inotify_fd : input_fd) + 1;
194 fd_set rfds;
195 bool go_on = true;
196
197 while (go_on) {
198 FD_ZERO(&rfds);
199 FD_SET(input_fd, &rfds);
200 FD_SET(inotify_fd, &rfds);
201 if (select(nfds, &rfds, NULL, NULL, NULL) < 0) {
202 userlog(LOG_ERR, "select: %s", strerror(errno));
203 go_on = false;
204 }
205 else if (FD_ISSET(input_fd, &rfds)) {
206 go_on = read_input();
207 }
208 else if (FD_ISSET(inotify_fd, &rfds)) {
209 go_on = process_inotify_input();
210 }
211 }
212}
213
214
215static bool read_input() {
216 char* line = read_line(stdin);
217 userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
218
219 if (line == NULL || strcmp(line, "EXIT") == 0) {
220 userlog(LOG_INFO, "exiting: %s", line);
221 return false;
222 }
223
224 if (strcmp(line, "ROOTS") == 0) {
225 array* new_roots = array_create(20);
226 CHECK_NULL(new_roots, false);
227
228 while (1) {
229 line = read_line(stdin);
230 userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
231 if (line == NULL || strlen(line) == 0) {
232 return false;
233 }
234 else if (strcmp(line, "#") == 0) {
235 break;
236 }
237 else {
238 int l = strlen(line);
239 if (l > 1 && line[l-1] == '/') line[l-1] = '\0';
240 CHECK_NULL(array_push(new_roots, strdup(line)), false);
241 }
242 }
243
244 return update_roots(new_roots);
245 }
246
247 userlog(LOG_INFO, "unrecognised command: %s", line);
248 return true;
249}
250
251
252static bool update_roots(array* new_roots) {
253 userlog(LOG_INFO, "updating roots (curr:%d, new:%d)", array_size(roots), array_size(new_roots));
254
255 unregister_roots();
256
257 if (array_size(new_roots) == 0) {
258 output("UNWATCHEABLE\n#\n");
259 array_delete(new_roots);
260 return true;
261 }
262 else if (array_size(new_roots) == 1 && strcmp(array_get(new_roots, 0), "/") == 0) { // refuse to watch entire tree
263 output("UNWATCHEABLE\n/\n#\n");
264 userlog(LOG_INFO, "unwatchable: /");
265 array_delete_vs_data(new_roots);
266 return true;
267 }
268
269 array* mounts = unwatchable_mounts();
270 if (mounts == NULL) {
271 return false;
272 }
273
274 array* unwatchable = array_create(20);
275 if (!register_roots(new_roots, unwatchable, mounts)) {
276 return false;
277 }
278
279 output("UNWATCHEABLE\n");
280 for (int i=0; i<array_size(unwatchable); i++) {
281 char* s = array_get(unwatchable, i);
282 output("%s\n", s);
283 userlog(LOG_INFO, "unwatchable: %s", s);
284 }
285 output("#\n");
286
287 array_delete_vs_data(unwatchable);
288 array_delete_vs_data(mounts);
289 array_delete_vs_data(new_roots);
290
291 return true;
292}
293
294
295static void unregister_roots() {
296 watch_root* root;
297 while ((root = array_pop(roots)) != NULL) {
298 userlog(LOG_INFO, "unregistering root: %s", root->name);
299 unwatch(root->id);
300 free(root->name);
301 free(root);
302 };
303}
304
305
306static bool register_roots(array* new_roots, array* unwatchable, array* mounts) {
307 for (int i=0; i<array_size(new_roots); i++) {
308 char* new_root = array_get(new_roots, i);
309 char* unflattened = new_root;
310 if (unflattened[0] == '|') ++unflattened;
311 userlog(LOG_INFO, "registering root: %s", new_root);
312
313 if (unflattened[0] != '/') {
314 userlog(LOG_WARNING, " ... not valid, skipped");
315 continue;
316 }
317
318 array* inner_mounts = array_create(5);
319 CHECK_NULL(inner_mounts, false);
320
321 bool skip = false;
322 for (int j=0; j<array_size(mounts); j++) {
323 char* mount = array_get(mounts, j);
324 if (is_parent_path(mount, unflattened)) {
325 userlog(LOG_DEBUG, "watch root '%s' is under mount point '%s' - skipping", unflattened, mount);
326 CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
327 skip = true;
328 break;
329 }
330 else if (is_parent_path(unflattened, mount)) {
331 userlog(LOG_DEBUG, "watch root '%s' contains mount point '%s' - partial watch", unflattened, mount);
332 char* copy = strdup(mount);
333 CHECK_NULL(array_push(unwatchable, copy), false);
334 CHECK_NULL(array_push(inner_mounts, copy), false);
335 }
336 }
337 if (skip) {
338 continue;
339 }
340
341 int id = watch(new_root, inner_mounts);
342 array_delete(inner_mounts);
343
344 if (id >= 0) {
345 watch_root* root = malloc(sizeof(watch_root));
346 CHECK_NULL(root, false);
347 root->id = id;
348 root->name = strdup(new_root);
349 CHECK_NULL(root->name, false);
350 CHECK_NULL(array_push(roots, root), false);
351 }
352 else if (id == ERR_ABORT) {
353 return false;
354 }
355 else if (id != ERR_IGNORE) {
356 if (show_warning && watch_limit_reached()) {
357 int limit = get_watch_count();
358 userlog(LOG_WARNING, "watch limit (%d) reached", limit);
359 output("MESSAGE\n" INOTIFY_LIMIT_MSG, limit);
360 show_warning = false; // warn only once
361 }
362 CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
363 }
364 }
365
366 return true;
367}
368
369
370static bool is_watchable(const char* fs) {
371 // don't watch special and network filesystems
372 return !(strncmp(fs, "dev", 3) == 0 || strcmp(fs, "proc") == 0 || strcmp(fs, "sysfs") == 0 || strcmp(fs, MNTTYPE_SWAP) == 0 ||
373 strncmp(fs, "fuse", 4) == 0 || strcmp(fs, "cifs") == 0 || strcmp(fs, MNTTYPE_NFS) == 0);
374}
375
376static array* unwatchable_mounts() {
377 FILE* mtab = setmntent(_PATH_MOUNTED, "r");
378 if (mtab == NULL) {
379 userlog(LOG_ERR, "cannot open " _PATH_MOUNTED);
380 return NULL;
381 }
382
383 array* mounts = array_create(20);
384 CHECK_NULL(mounts, NULL);
385
386 struct mntent* ent;
387 while ((ent = getmntent(mtab)) != NULL) {
388 userlog(LOG_DEBUG, "mtab: %s : %s", ent->mnt_dir, ent->mnt_type);
389 if (strcmp(ent->mnt_type, MNTTYPE_IGNORE) != 0 && !is_watchable(ent->mnt_type)) {
390 CHECK_NULL(array_push(mounts, strdup(ent->mnt_dir)), NULL);
391 }
392 }
393
394 endmntent(mtab);
395 return mounts;
396}
397
398
399static void inotify_callback(char* path, int event) {
400 if (event & IN_CREATE || event & IN_MOVED_TO) {
401 output("CREATE\n%s\nCHANGE\n%s\n", path, path);
402 userlog(LOG_DEBUG, "CREATE: %s", path);
403 return;
404 }
405
406 if (event & IN_MODIFY) {
407 output("CHANGE\n%s\n", path);
408 userlog(LOG_DEBUG, "CHANGE: %s", path);
409 return;
410 }
411
412 if (event & IN_ATTRIB) {
413 output("STATS\n%s\n", path);
414 userlog(LOG_DEBUG, "STATS: %s", path);
415 return;
416 }
417
418 if (event & IN_DELETE || event & IN_MOVED_FROM) {
419 output("DELETE\n%s\n", path);
420 userlog(LOG_DEBUG, "DELETE: %s", path);
421 return;
422 }
423
424 if (event & IN_UNMOUNT) {
425 output("RESET\n");
426 userlog(LOG_DEBUG, "RESET");
427 return;
428 }
429}
430
431
432static void output(const char* format, ...) {
433 if (self_test) {
434 return;
435 }
436
437 va_list ap;
438 va_start(ap, format);
439 vprintf(format, ap);
440 va_end(ap);
441}