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