extract method: logging for possible NPE later
[idea/community.git] / native / fsNotifier / linux / main.c
1 /*
2  * Copyright 2000-2016 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 <sys/stat.h>
29 #include <syslog.h>
30 #include <unistd.h>
31
32 #define LOG_ENV "FSNOTIFIER_LOG_LEVEL"
33 #define LOG_ENV_DEBUG "debug"
34 #define LOG_ENV_INFO "info"
35 #define LOG_ENV_WARNING "warning"
36 #define LOG_ENV_ERROR "error"
37 #define LOG_ENV_OFF "off"
38
39
40 #define USAGE_MSG \
41     "fsnotifier - IntelliJ IDEA companion program for watching and reporting file and directory structure modifications.\n\n" \
42     "fsnotifier utilizes \"user\" facility of syslog(3) - messages usually can be found in /var/log/user.log.\n" \
43     "Verbosity is regulated via " LOG_ENV " environment variable, possible values are: " \
44     LOG_ENV_DEBUG ", " LOG_ENV_INFO ", " LOG_ENV_WARNING ", " LOG_ENV_ERROR ", " LOG_ENV_OFF "; default is " LOG_ENV_WARNING ".\n\n" \
45     "Use 'fsnotifier --selftest' to perform some self-diagnostics (output will be logged and printed to console).\n"
46
47 #define HELP_MSG \
48     "Try 'fsnotifier --help' for more information.\n"
49
50 #define INSTANCE_LIMIT_TEXT \
51     "The <b>inotify</b>(7) instances limit reached. " \
52     "<a href=\"https://confluence.jetbrains.com/display/IDEADEV/Inotify+Instances+Limit\">More details.</a>\n"
53
54 #define WATCH_LIMIT_TEXT \
55     "The current <b>inotify</b>(7) watch limit is too low. " \
56     "<a href=\"https://confluence.jetbrains.com/display/IDEADEV/Inotify+Watches+Limit\">More details.</a>\n"
57
58 #define MISSING_ROOT_TIMEOUT 1
59
60 #define UNFLATTEN(root) (root[0] == '|' ? root + 1 : root)
61
62 typedef struct {
63   char* path;
64   int id;  // negative value means missing root
65 } watch_root;
66
67 static array* roots = NULL;
68
69 static int log_level = 0;
70 static bool self_test = false;
71
72 static void init_log();
73 static void run_self_test();
74 static bool main_loop();
75 static int read_input();
76 static bool update_roots(array* new_roots);
77 static void unregister_roots();
78 static bool register_roots(array* new_roots, array* unwatchable, array* mounts);
79 static array* unwatchable_mounts();
80 static void inotify_callback(const char* path, int event);
81 static void report_event(const char* event, const char* path);
82 static void output(const char* format, ...);
83 static void check_missing_roots();
84 static void check_root_removal(const char*);
85
86
87 int main(int argc, char** argv) {
88   if (argc > 1) {
89     if (strcmp(argv[1], "--help") == 0) {
90       printf(USAGE_MSG);
91       return 0;
92     }
93     else if (strcmp(argv[1], "--version") == 0) {
94       printf("fsnotifier " VERSION "\n");
95       return 0;
96     }
97     else if (strcmp(argv[1], "--selftest") == 0) {
98       self_test = true;
99     }
100     else {
101       printf("unrecognized option: %s\n", argv[1]);
102       printf(HELP_MSG);
103       return 1;
104     }
105   }
106
107   init_log();
108   if (!self_test) {
109     userlog(LOG_INFO, "started (v." VERSION ")");
110   }
111   else {
112     userlog(LOG_INFO, "started (self-test mode) (v." VERSION ")");
113   }
114
115   setvbuf(stdin, NULL, _IONBF, 0);
116
117   int rv = 0;
118   roots = array_create(20);
119   if (roots != NULL && init_inotify()) {
120     set_inotify_callback(&inotify_callback);
121
122     if (!self_test) {
123       if (!main_loop()) {
124         rv = 3;
125       }
126     }
127     else {
128       run_self_test();
129     }
130
131     unregister_roots();
132   }
133   else {
134     output("GIVEUP\n");
135     rv = 2;
136   }
137   close_inotify();
138   array_delete(roots);
139
140   userlog(LOG_INFO, "finished (%d)", rv);
141   closelog();
142
143   return rv;
144 }
145
146
147 static void init_log() {
148   int level = LOG_WARNING;
149
150   char* env_level = getenv(LOG_ENV);
151   if (env_level != NULL) {
152     if (strcmp(env_level, LOG_ENV_DEBUG) == 0)  level = LOG_DEBUG;
153     else if (strcmp(env_level, LOG_ENV_INFO) == 0)  level = LOG_INFO;
154     else if (strcmp(env_level, LOG_ENV_WARNING) == 0)  level = LOG_WARNING;
155     else if (strcmp(env_level, LOG_ENV_ERROR) == 0)  level = LOG_ERR;
156   }
157
158   if (self_test) {
159     level = LOG_DEBUG;
160   }
161
162   char ident[32];
163   snprintf(ident, sizeof(ident), "fsnotifier[%d]", getpid());
164   openlog(ident, 0, LOG_USER);
165   log_level = level;
166 }
167
168
169 void message(MSG id) {
170   if (id == MSG_INSTANCE_LIMIT) {
171     output("MESSAGE\n" INSTANCE_LIMIT_TEXT);
172   }
173   else if (id == MSG_WATCH_LIMIT) {
174     output("MESSAGE\n" WATCH_LIMIT_TEXT);
175   }
176   else {
177     userlog(LOG_ERR, "unknown message: %d", id);
178   }
179 }
180
181
182 void userlog(int priority, const char* format, ...) {
183   if (priority > log_level) {
184     return;
185   }
186
187   va_list ap;
188   va_start(ap, format);
189   vsyslog(priority, format, ap);
190   va_end(ap);
191
192   if (self_test) {
193     const char* level = "debug";
194     switch (priority) {
195       case LOG_ERR:  level = "error"; break;
196       case LOG_WARNING:  level = " warn"; break;
197       case LOG_INFO:  level = " info"; break;
198     }
199     printf("fsnotifier[%d] %s: ", getpid(), level);
200
201     va_start(ap, format);
202     vprintf(format, ap);
203     va_end(ap);
204
205     printf("\n");
206   }
207 }
208
209
210 static void run_self_test() {
211   array* test_roots = array_create(1);
212   char* cwd = malloc(PATH_MAX);
213   if (getcwd(cwd, PATH_MAX) == NULL) {
214     strncpy(cwd, ".", PATH_MAX);
215   }
216   array_push(test_roots, cwd);
217   update_roots(test_roots);
218 }
219
220
221 static bool main_loop() {
222   int input_fd = fileno(stdin), inotify_fd = get_inotify_fd();
223   int nfds = (inotify_fd > input_fd ? inotify_fd : input_fd) + 1;
224   fd_set rfds;
225   struct timeval timeout;
226
227   while (true) {
228     usleep(50000);
229
230     FD_ZERO(&rfds);
231     FD_SET(input_fd, &rfds);
232     FD_SET(inotify_fd, &rfds);
233     timeout = (struct timeval){MISSING_ROOT_TIMEOUT, 0};
234
235     if (select(nfds, &rfds, NULL, NULL, &timeout) < 0) {
236       if (errno != EINTR) {
237         userlog(LOG_ERR, "select: %s", strerror(errno));
238         return false;
239       }
240     }
241     else if (FD_ISSET(input_fd, &rfds)) {
242       int result = read_input();
243       if (result == 0) return true;
244       else if (result != ERR_CONTINUE) return false;
245     }
246     else if (FD_ISSET(inotify_fd, &rfds)) {
247       if (!process_inotify_input()) return false;
248     }
249     else {
250       check_missing_roots();
251     }
252   }
253 }
254
255
256 static int read_input() {
257   char* line = read_line(stdin);
258   userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
259
260   if (line == NULL || strcmp(line, "EXIT") == 0) {
261     userlog(LOG_INFO, "exiting: %s", line);
262     return 0;
263   }
264
265   if (strcmp(line, "ROOTS") == 0) {
266     array* new_roots = array_create(20);
267     CHECK_NULL(new_roots, ERR_ABORT);
268
269     while (1) {
270       line = read_line(stdin);
271       userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
272       if (line == NULL || strlen(line) == 0) {
273         return 0;
274       }
275       else if (strcmp(line, "#") == 0) {
276         break;
277       }
278       else {
279         int l = strlen(line);
280         if (l > 1 && line[l-1] == '/')  line[l-1] = '\0';
281         CHECK_NULL(array_push(new_roots, strdup(line)), ERR_ABORT);
282       }
283     }
284
285     return update_roots(new_roots) ? ERR_CONTINUE : ERR_ABORT;
286   }
287
288   userlog(LOG_WARNING, "unrecognised command: %s", line);
289   return ERR_CONTINUE;
290 }
291
292
293 static bool update_roots(array* new_roots) {
294   userlog(LOG_INFO, "updating roots (curr:%d, new:%d)", array_size(roots), array_size(new_roots));
295
296   unregister_roots();
297
298   if (array_size(new_roots) == 0) {
299     output("UNWATCHEABLE\n#\n");
300     array_delete(new_roots);
301     return true;
302   }
303   else if (array_size(new_roots) == 1 && strcmp(array_get(new_roots, 0), "/") == 0) {  // refuse to watch entire tree
304     output("UNWATCHEABLE\n/\n#\n");
305     userlog(LOG_INFO, "unwatchable: /");
306     array_delete_vs_data(new_roots);
307     return true;
308   }
309
310   array* mounts = unwatchable_mounts();
311   if (mounts == NULL) {
312     return false;
313   }
314
315   array* unwatchable = array_create(20);
316   if (!register_roots(new_roots, unwatchable, mounts)) {
317     return false;
318   }
319
320   output("UNWATCHEABLE\n");
321   for (int i=0; i<array_size(unwatchable); i++) {
322     char* s = array_get(unwatchable, i);
323     output("%s\n", s);
324     userlog(LOG_INFO, "unwatchable: %s", s);
325   }
326   output("#\n");
327
328   array_delete_vs_data(unwatchable);
329   array_delete_vs_data(mounts);
330   array_delete_vs_data(new_roots);
331
332   return true;
333 }
334
335
336 static void unregister_roots() {
337   watch_root* root;
338   while ((root = array_pop(roots)) != NULL) {
339     userlog(LOG_INFO, "unregistering root: %s", root->path);
340     unwatch(root->id);
341     free(root->path);
342     free(root);
343   };
344 }
345
346
347 static bool register_roots(array* new_roots, array* unwatchable, array* mounts) {
348   for (int i=0; i<array_size(new_roots); i++) {
349     char* new_root = array_get(new_roots, i);
350     char* unflattened = UNFLATTEN(new_root);
351     userlog(LOG_INFO, "registering root: %s", new_root);
352
353     if (unflattened[0] != '/') {
354       userlog(LOG_WARNING, "invalid root: %s", new_root);
355       continue;
356     }
357
358     array* inner_mounts = array_create(5);
359     CHECK_NULL(inner_mounts, false);
360
361     bool skip = false;
362     for (int j=0; j<array_size(mounts); j++) {
363       char* mount = array_get(mounts, j);
364       if (is_parent_path(mount, unflattened)) {
365         userlog(LOG_INFO, "watch root '%s' is under mount point '%s' - skipping", unflattened, mount);
366         CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
367         skip = true;
368         break;
369       }
370       else if (is_parent_path(unflattened, mount)) {
371         userlog(LOG_INFO, "watch root '%s' contains mount point '%s' - partial watch", unflattened, mount);
372         char* copy = strdup(mount);
373         CHECK_NULL(array_push(unwatchable, copy), false);
374         CHECK_NULL(array_push(inner_mounts, copy), false);
375       }
376     }
377     if (skip) {
378       continue;
379     }
380
381     int id = watch(new_root, inner_mounts);
382     array_delete(inner_mounts);
383
384     if (id >= 0 || id == ERR_MISSING) {
385       watch_root* root = malloc(sizeof(watch_root));
386       CHECK_NULL(root, false);
387       root->id = id;
388       root->path = strdup(new_root);
389       CHECK_NULL(root->path, false);
390       CHECK_NULL(array_push(roots, root), false);
391     }
392     else if (id == ERR_ABORT) {
393       return false;
394     }
395     else if (id != ERR_IGNORE) {
396       userlog(LOG_WARNING, "watch root '%s' cannot be watched: %d", unflattened, id);
397       CHECK_NULL(array_push(unwatchable, strdup(unflattened)), false);
398     }
399   }
400
401   return true;
402 }
403
404
405 static bool is_watchable(const char* fs) {
406   // don't watch special and network filesystems
407   return !(strncmp(fs, "dev", 3) == 0 || strcmp(fs, "proc") == 0 || strcmp(fs, "sysfs") == 0 || strcmp(fs, MNTTYPE_SWAP) == 0 ||
408            (strncmp(fs, "fuse", 4) == 0 && strcmp(fs, "fuseblk") != 0) ||
409            strcmp(fs, "cifs") == 0 || strcmp(fs, MNTTYPE_NFS) == 0);
410 }
411
412 static array* unwatchable_mounts() {
413   FILE* mtab = setmntent(_PATH_MOUNTED, "r");
414   if (mtab == NULL) {
415     userlog(LOG_ERR, "cannot open " _PATH_MOUNTED);
416     return NULL;
417   }
418
419   array* mounts = array_create(20);
420   CHECK_NULL(mounts, NULL);
421
422   struct mntent* ent;
423   while ((ent = getmntent(mtab)) != NULL) {
424     userlog(LOG_DEBUG, "mtab: %s : %s", ent->mnt_dir, ent->mnt_type);
425     if (strcmp(ent->mnt_type, MNTTYPE_IGNORE) != 0 && !is_watchable(ent->mnt_type)) {
426       CHECK_NULL(array_push(mounts, strdup(ent->mnt_dir)), NULL);
427     }
428   }
429
430   endmntent(mtab);
431   return mounts;
432 }
433
434
435 static void inotify_callback(const char* path, int event) {
436   if (event & (IN_CREATE | IN_MOVED_TO)) {
437     report_event("CREATE", path);
438     report_event("CHANGE", path);
439   }
440   else if (event & IN_MODIFY) {
441     report_event("CHANGE", path);
442   }
443   else if (event & IN_ATTRIB) {
444     report_event("STATS", path);
445   }
446   else if (event & (IN_DELETE | IN_MOVED_FROM)) {
447     report_event("DELETE", path);
448   }
449   if (event & (IN_DELETE_SELF | IN_MOVE_SELF)) {
450     check_root_removal(path);
451   }
452   else if (event & IN_UNMOUNT) {
453     output("RESET\n");
454     userlog(LOG_DEBUG, "RESET");
455   }
456 }
457
458 static void report_event(const char* event, const char* path) {
459   userlog(LOG_DEBUG, "%s: %s", event, path);
460
461 #pragma clang diagnostic push
462 #pragma clang diagnostic ignored "-Wincompatible-pointer-types"
463   char* copy = path, *p;
464   for (p = copy; *p != '\0'; ++p) {
465     if (*p == '\n') {
466       if (copy == path) {
467         copy = strdup(path);
468         p = copy + (p - path);
469       }
470       *p = '\0';
471     }
472   }
473 #pragma clang diagnostic pop
474
475   fputs(event, stdout);
476   fputc('\n', stdout);
477   fwrite(copy, (p - copy), 1, stdout);
478   fputc('\n', stdout);
479
480   if (copy != path) {
481     free(copy);
482   }
483
484   fflush(stdout);
485 }
486
487
488 static void output(const char* format, ...) {
489   if (self_test) {
490     return;
491   }
492
493   va_list ap;
494   va_start(ap, format);
495   vprintf(format, ap);
496   va_end(ap);
497
498   fflush(stdout);
499 }
500
501
502 static void check_missing_roots() {
503   struct stat st;
504   for (int i=0; i<array_size(roots); i++) {
505     watch_root* root = array_get(roots, i);
506     if (root->id < 0) {
507       char* unflattened = UNFLATTEN(root->path);
508       if (stat(unflattened, &st) == 0) {
509         root->id = watch(root->path, NULL);
510         userlog(LOG_INFO, "root restored: %s\n", root->path);
511         report_event("CREATE", unflattened);
512         report_event("CHANGE", unflattened);
513       }
514     }
515   }
516 }
517
518 static void check_root_removal(const char* path) {
519   for (int i=0; i<array_size(roots); i++) {
520     watch_root* root = array_get(roots, i);
521     if (root->id >= 0 && strcmp(path, UNFLATTEN(root->path)) == 0) {
522       unwatch(root->id);
523       root->id = -1;
524       userlog(LOG_INFO, "root deleted: %s\n", root->path);
525       report_event("DELETE", path);
526     }
527   }
528 }