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