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