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