3bef185211519d8d4b5a0242d75d84821cd57f9a
[idea/community.git] / native / fsNotifier / linux / main.c
1 /*
2  * Copyright 2000-2011 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 <stdarg.h>
22 #include <stdlib.h>
23 #include <string.h>
24 #include <sys/inotify.h>
25 #include <sys/select.h>
26 #include <syslog.h>
27 #include <unistd.h>
28
29 #define LOG_ENV "FSNOTIFIER_LOG_LEVEL"
30 #define LOG_ENV_DEBUG "debug"
31 #define LOG_ENV_INFO "info"
32 #define LOG_ENV_WARNING "warning"
33 #define LOG_ENV_ERROR "error"
34 #define LOG_ENV_OFF "off"
35
36 #define VERSION "1.0"
37 #define VERSION_MSG "fsnotifier " VERSION "\n"
38
39 #define USAGE_MSG \
40     "fsnotifier - IntelliJ IDEA companion program for watching and reporting file and directory structure modifications.\n\n" \
41     "fsnotifier utilizes \"user\" facility of syslog(3) - messages usually can be found in /var/log/user.log.\n" \
42     "Verbosity is regulated via " LOG_ENV " environment variable, possible values are: " \
43     LOG_ENV_DEBUG ", " LOG_ENV_INFO ", " LOG_ENV_WARNING ", " LOG_ENV_ERROR ", " LOG_ENV_OFF "; latter is the default.\n\n" \
44     "Use 'fsnotifier --selftest' to perform some self-diagnostics (output will be logged and printed to console).\n"
45
46 #define HELP_MSG \
47     "Try 'fsnotifier --help' for more information.\n"
48
49 #define INOTIFY_LIMIT_MSG \
50     "The current <b>inotify</b>(7) watch limit of %d is too low. " \
51     "<a href=\"http://confluence.jetbrains.net/display/IDEADEV/Inotify+Watches+Limit\">More details.</a>\n"
52
53 typedef struct {
54   char* name;
55   int id;
56 } watch_root;
57
58 static array* roots = NULL;
59
60 static bool show_warning = true;
61
62 static bool self_test = false;
63
64 #define CHECK_NULL(p) if (p == NULL)  { userlog(LOG_ERR, "out of memory"); return false; }
65
66 static void init_log();
67 static void run_self_test();
68 static void main_loop();
69 static bool read_input();
70 static bool update_roots(array* new_roots);
71 static void unregister_roots();
72 static bool register_roots(array* new_roots, array* unwatchable);
73 static bool unwatchable_mounts(array* mounts);
74 static void inotify_callback(char* path, int event);
75 static void output(const char* format, ...);
76
77
78 int main(int argc, char** argv) {
79   if (argc > 1) {
80     if (strcmp(argv[1], "--help") == 0) {
81       printf(USAGE_MSG);
82       return 0;
83     }
84     else if (strcmp(argv[1], "--version") == 0) {
85       printf(VERSION_MSG);
86       return 0;
87     }
88     else if (strcmp(argv[1], "--selftest") == 0) {
89       self_test = true;
90     }
91     else {
92       printf("unrecognized option: %s\n", argv[1]);
93       printf(HELP_MSG);
94       return 1;
95     }
96   }
97
98   init_log();
99   if (!self_test) {
100     userlog(LOG_INFO, "started (v." VERSION ")");
101   }
102   else {
103     userlog(LOG_INFO, "started (self-test mode) (v." VERSION ")");
104   }
105
106   setvbuf(stdin, NULL, _IONBF, 0);
107   setvbuf(stdout, NULL, _IONBF, 0);
108
109   roots = array_create(20);
110   if (init_inotify() && roots != NULL) {
111     set_inotify_callback(&inotify_callback);
112
113     if (!self_test) {
114       main_loop();
115     }
116     else {
117       run_self_test();
118     }
119
120     unregister_roots();
121   }
122   else {
123     printf("GIVEUP\n");
124   }
125   close_inotify();
126   array_delete(roots);
127
128   userlog(LOG_INFO, "finished");
129   closelog();
130
131   return 0;
132 }
133
134
135 static void init_log() {
136   char* env_level = getenv(LOG_ENV);
137   int level = LOG_EMERG;
138   if (env_level != NULL) {
139     if (strcmp(env_level, LOG_ENV_DEBUG) == 0)  level = LOG_DEBUG;
140     else if (strcmp(env_level, LOG_ENV_INFO) == 0)  level = LOG_INFO;
141     else if (strcmp(env_level, LOG_ENV_WARNING) == 0)  level = LOG_WARNING;
142     else if (strcmp(env_level, LOG_ENV_ERROR) == 0)  level = LOG_ERR;
143   }
144
145   if (self_test) {
146     level = LOG_DEBUG;
147   }
148
149   char ident[32];
150   snprintf(ident, sizeof(ident), "fsnotifier[%d]", getpid());
151   openlog(ident, 0, LOG_USER);
152   setlogmask(LOG_UPTO(level));
153 }
154
155
156 void userlog(int priority, const char* format, ...) {
157   va_list ap;
158
159   va_start(ap, format);
160   vsyslog(priority, format, ap);
161   va_end(ap);
162
163   if (self_test) {
164     const char* level = "debug";
165     switch (priority) {
166       case LOG_ERR:  level = "error"; break;
167       case LOG_WARNING:  level = " warn"; break;
168       case LOG_INFO:  level = " info"; break;
169     }
170     printf("fsnotifier[%d] %s: ", getpid(), level);
171
172     va_start(ap, format);
173     vprintf(format, ap);
174     va_end(ap);
175
176     printf("\n");
177   }
178 }
179
180
181 static void run_self_test() {
182   array* test_roots = array_create(1);
183   char* cwd = malloc(PATH_MAX);
184   if (getcwd(cwd, PATH_MAX) == NULL) {
185     strncpy(cwd, ".", PATH_MAX);
186   }
187   array_push(test_roots, cwd);
188   update_roots(test_roots);
189 }
190
191
192 static void main_loop() {
193   int input_fd = fileno(stdin), inotify_fd = get_inotify_fd();
194   int nfds = (inotify_fd > input_fd ? inotify_fd : input_fd) + 1;
195   fd_set rfds;
196   bool go_on = true;
197
198   while (go_on) {
199     FD_ZERO(&rfds);
200     FD_SET(input_fd, &rfds);
201     FD_SET(inotify_fd, &rfds);
202     if (select(nfds, &rfds, NULL, NULL, NULL) < 0) {
203       userlog(LOG_ERR, "select: %s", strerror(errno));
204       go_on = false;
205     }
206     else if (FD_ISSET(input_fd, &rfds)) {
207       go_on = read_input();
208     }
209     else if (FD_ISSET(inotify_fd, &rfds)) {
210       go_on = process_inotify_input();
211     }
212   }
213 }
214
215
216 static bool read_input() {
217   char* line = read_line(stdin);
218   userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
219
220   if (line == NULL || strcmp(line, "EXIT") == 0) {
221     return false;
222   }
223
224   if (strcmp(line, "ROOTS") == 0) {
225     array* new_roots = array_create(20);
226     CHECK_NULL(new_roots);
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         if (line[0] == '|')  line++;  // flat roots will be differentiated later
239
240         int l = strlen(line);
241         if (l > 1 && line[l-1] == '/')  line[l-1] = '\0';
242
243         CHECK_NULL(array_push(new_roots, strdup(line)));
244       }
245     }
246
247     return update_roots(new_roots);
248   }
249
250   return true;
251 }
252
253
254 static bool update_roots(array* new_roots) {
255   userlog(LOG_INFO, "updating roots (curr:%d, new:%d)", array_size(roots), array_size(new_roots));
256
257   unregister_roots();
258   if (array_size(new_roots) == 0) {
259     return true;
260   }
261   else if (array_size(new_roots) == 1 && strcmp(array_get(new_roots, 0), "/") == 0) {  // refuse to watch entire tree
262     output("UNWATCHEABLE\n/\n#\n");
263     userlog(LOG_INFO, "unwatchable: /");
264     array_delete_vs_data(new_roots);
265     return true;
266   }
267
268   array* unwatchable = array_create(20);
269   CHECK_NULL(unwatchable);
270   if (!unwatchable_mounts(unwatchable)) {
271     return false;
272   }
273
274   if (!register_roots(new_roots, unwatchable)) {
275     return false;
276   }
277
278   // todo: sort/optimize list
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(new_roots);
289
290   return true;
291 }
292
293
294 static void unregister_roots() {
295   watch_root* root;
296   while ((root = array_pop(roots)) != NULL) {
297     userlog(LOG_INFO, "unregistering root: %s", root->name);
298     unwatch(root->id);
299     free(root->name);
300     free(root);
301   };
302 }
303
304
305 static bool register_roots(array* new_roots, array* unwatchable) {
306   for (int i=0; i<array_size(new_roots); i++) {
307     char* new_root = array_get(new_roots, i);
308     userlog(LOG_INFO, "registering root: %s", new_root);
309     int id = watch(new_root, unwatchable);
310     if (id == ERR_ABORT) {
311       return false;
312     }
313     else if (id >= 0) {
314       watch_root* root = malloc(sizeof(watch_root));
315       CHECK_NULL(root);
316       root->id = id;
317       root->name = new_root;
318       CHECK_NULL(array_push(roots, root));
319     }
320     else {
321       if (show_warning && watch_limit_reached()) {
322         int limit = get_watch_count();
323         userlog(LOG_WARNING, "watch limit (%d) reached", limit);
324         output("MESSAGE\n" INOTIFY_LIMIT_MSG, limit);
325         show_warning = false;  // warn only once
326       }
327       CHECK_NULL(array_push(unwatchable, new_root));
328     }
329   }
330
331   return true;
332 }
333
334 static bool is_watchable(const char* dev, const char* mnt, const char* fs) {
335   // don't watch special and network filesystems
336   return !(strncmp(mnt, "/dev", 4) == 0 || strncmp(mnt, "/proc", 5) == 0 || strncmp(mnt, "/sys", 4) == 0 ||
337            strcmp(fs, "fuse.gvfs-fuse-daemon") == 0 || strcmp(fs, "cifs") == 0 || strcmp(fs, "nfs") == 0);
338 }
339
340 #define MTAB_DELIMS " \t"
341
342 static bool unwatchable_mounts(array* mounts) {
343   FILE* mtab = fopen("/etc/mtab", "r");
344   if (mtab == NULL) {
345     mtab = fopen("/proc/mounts", "r");
346   }
347   if (mtab == NULL) {
348     userlog(LOG_ERR, "neither /etc/mtab nor /proc/mounts can be read");
349     return false;
350   }
351
352   char* line;
353   while ((line = read_line(mtab)) != NULL) {
354     userlog(LOG_DEBUG, "mtab: %s", line);
355     char* dev = strtok(line, MTAB_DELIMS);
356     char* point = strtok(NULL, MTAB_DELIMS);
357     char* fs = strtok(NULL, MTAB_DELIMS);
358
359     if (dev == NULL || point == NULL || fs == NULL) {
360       userlog(LOG_ERR, "can't parse mount line");
361       return false;
362     }
363
364     if (!is_watchable(dev, point, fs)) {
365       CHECK_NULL(array_push(mounts, strdup(point)));
366     }
367   }
368
369   fclose(mtab);
370   return true;
371 }
372
373
374 static void inotify_callback(char* path, int event) {
375   if (event & IN_CREATE || event & IN_MOVED_TO) {
376     output("CREATE\n%s\nCHANGE\n%s\n", path, path);
377     userlog(LOG_DEBUG, "CREATE: %s", path);
378     return;
379   }
380
381   if (event & IN_MODIFY) {
382     output("CHANGE\n%s\n", path);
383     userlog(LOG_DEBUG, "CHANGE: %s", path);
384     return;
385   }
386
387   if (event & IN_ATTRIB) {
388     output("STATS\n%s\n", path);
389     userlog(LOG_DEBUG, "STATS: %s", path);
390     return;
391   }
392
393   if (event & IN_DELETE || event & IN_MOVED_FROM) {
394     output("DELETE\n%s\n", path);
395     userlog(LOG_DEBUG, "DELETE: %s", path);
396     return;
397   }
398
399   if (event & IN_UNMOUNT) {
400     output("RESET\n");
401     userlog(LOG_DEBUG, "RESET");
402     return;
403   }
404 }
405
406
407 static void output(const char* format, ...) {
408   if (self_test) {
409     return;
410   }
411
412   va_list ap;
413   va_start(ap, format);
414   vprintf(format, ap);
415   va_end(ap);
416 }