64-bit fsnotifier for Linux (fixed)
[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
152   va_start(ap, format);
153   vsyslog(priority, format, ap);
154   va_end(ap);
155
156   if (self_test) {
157     const char* level = "debug";
158     switch (priority) {
159       case LOG_ERR:  level = "error"; break;
160       case LOG_WARNING:  level = " warn"; break;
161       case LOG_INFO:  level = " info"; break;
162     }
163     printf("fsnotifier[%d] %s: ", getpid(), level);
164
165     va_start(ap, format);
166     vprintf(format, ap);
167     va_end(ap);
168
169     printf("\n");
170   }
171 }
172
173
174 static void run_self_test() {
175   array* test_roots = array_create(1);
176   char* cwd = malloc(PATH_MAX);
177   if (getcwd(cwd, PATH_MAX) == NULL) {
178     strncpy(cwd, ".", PATH_MAX);
179   }
180   array_push(test_roots, cwd);
181   update_roots(test_roots);
182 }
183
184
185 static void main_loop() {
186   int input_fd = fileno(stdin), inotify_fd = get_inotify_fd();
187   int nfds = (inotify_fd > input_fd ? inotify_fd : input_fd) + 1;
188   fd_set rfds;
189   bool go_on = true;
190
191   while (go_on) {
192     FD_ZERO(&rfds);
193     FD_SET(input_fd, &rfds);
194     FD_SET(inotify_fd, &rfds);
195     if (select(nfds, &rfds, NULL, NULL, NULL) < 0) {
196       userlog(LOG_ERR, "select: %s", strerror(errno));
197       go_on = false;
198     }
199     else if (FD_ISSET(input_fd, &rfds)) {
200       go_on = read_input();
201     }
202     else if (FD_ISSET(inotify_fd, &rfds)) {
203       go_on = process_inotify_input();
204     }
205   }
206 }
207
208
209 static bool read_input() {
210   char* line = read_line(stdin);
211   userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
212
213   if (line == NULL || strcmp(line, "EXIT") == 0) {
214     return false;
215   }
216
217   if (strcmp(line, "ROOTS") == 0) {
218     array* new_roots = array_create(20);
219     CHECK_NULL(new_roots);
220
221     while (1) {
222       line = read_line(stdin);
223       userlog(LOG_DEBUG, "input: %s", (line ? line : "<null>"));
224       if (line == NULL || strlen(line) == 0) {
225         return false;
226       }
227       else if (strcmp(line, "#") == 0) {
228         break;
229       }
230       else {
231         if (line[0] == '|')  line++;  // flat roots will be differentiated later
232
233         int l = strlen(line);
234         if (l > 1 && line[l-1] == '/')  line[l-1] = '\0';
235
236         CHECK_NULL(array_push(new_roots, strdup(line)));
237       }
238     }
239
240     return update_roots(new_roots);
241   }
242
243   return true;
244 }
245
246
247 static bool update_roots(array* new_roots) {
248   userlog(LOG_INFO, "updating roots (curr:%d, new:%d)", array_size(roots), array_size(new_roots));
249
250   unregister_roots();
251   if (array_size(new_roots) == 0) {
252     return true;
253   }
254   else if (array_size(new_roots) == 1 && strcmp(array_get(new_roots, 0), "/") == 0) {  // refuse to watch entire tree
255     output("UNWATCHEABLE\n/\n#\n");
256     userlog(LOG_INFO, "unwatchable: /");
257     array_delete_vs_data(new_roots);
258     return true;
259   }
260
261   array* unwatchable = array_create(20);
262   CHECK_NULL(unwatchable);
263   if (!unwatchable_mounts(unwatchable)) {
264     return false;
265   }
266
267   if (!register_roots(new_roots, unwatchable)) {
268     return false;
269   }
270
271   // todo: sort/optimize list
272   output("UNWATCHEABLE\n");
273   for (int i=0; i<array_size(unwatchable); i++) {
274     char* s = array_get(unwatchable, i);
275     output("%s\n", s);
276     userlog(LOG_INFO, "unwatchable: %s", s);
277   }
278   output("#\n");
279
280   array_delete_vs_data(unwatchable);
281   array_delete(new_roots);
282
283   return true;
284 }
285
286
287 static void unregister_roots() {
288   watch_root* root;
289   while ((root = array_pop(roots)) != NULL) {
290     userlog(LOG_INFO, "unregistering root: %s", root->name);
291     unwatch(root->id);
292     free(root->name);
293     free(root);
294   };
295 }
296
297
298 static bool register_roots(array* new_roots, array* unwatchable) {
299   for (int i=0; i<array_size(new_roots); i++) {
300     char* new_root = array_get(new_roots, i);
301     userlog(LOG_INFO, "registering root: %s", new_root);
302     int id = watch(new_root, unwatchable);
303     if (id == ERR_ABORT) {
304       return false;
305     }
306     else if (id >= 0) {
307       watch_root* root = malloc(sizeof(watch_root));
308       CHECK_NULL(root);
309       root->id = id;
310       root->name = new_root;
311       CHECK_NULL(array_push(roots, root));
312     }
313     else {
314       if (show_warning && watch_limit_reached()) {
315         int limit = get_watch_count();
316         userlog(LOG_WARNING, "watch limit (%d) reached", limit);
317         output("MESSAGE\n" INOTIFY_LIMIT_MSG, limit);
318         show_warning = false;  // warn only once
319       }
320       CHECK_NULL(array_push(unwatchable, new_root));
321     }
322   }
323
324   return true;
325 }
326
327 static bool is_watchable(const char* dev, const char* mnt, const char* fs) {
328   // don't watch special and network filesystems
329   return !(strncmp(mnt, "/dev", 4) == 0 || strncmp(mnt, "/proc", 5) == 0 || strncmp(mnt, "/sys", 4) == 0 ||
330            strcmp(fs, "fuse.gvfs-fuse-daemon") == 0 || strcmp(fs, "cifs") == 0 || strcmp(fs, "nfs") == 0);
331 }
332
333 #define MTAB_DELIMS " \t"
334
335 static bool unwatchable_mounts(array* mounts) {
336   FILE* mtab = fopen("/etc/mtab", "r");
337   if (mtab == NULL) {
338     mtab = fopen("/proc/mounts", "r");
339   }
340   if (mtab == NULL) {
341     userlog(LOG_ERR, "neither /etc/mtab nor /proc/mounts can be read");
342     return false;
343   }
344
345   char* line;
346   while ((line = read_line(mtab)) != NULL) {
347     userlog(LOG_DEBUG, "mtab: %s", line);
348     char* dev = strtok(line, MTAB_DELIMS);
349     char* point = strtok(NULL, MTAB_DELIMS);
350     char* fs = strtok(NULL, MTAB_DELIMS);
351
352     if (dev == NULL || point == NULL || fs == NULL) {
353       userlog(LOG_ERR, "can't parse mount line");
354       return false;
355     }
356
357     if (!is_watchable(dev, point, fs)) {
358       CHECK_NULL(array_push(mounts, strdup(point)));
359     }
360   }
361
362   fclose(mtab);
363   return true;
364 }
365
366
367 static void inotify_callback(char* path, int event) {
368   if (event & IN_CREATE || event & IN_MOVED_TO) {
369     output("CREATE\n%s\n", path);
370     userlog(LOG_DEBUG, "CREATE: %s", path);
371     return;
372   }
373
374   if (event & IN_MODIFY) {
375     output("CHANGE\n%s\n", path);
376     userlog(LOG_DEBUG, "CHANGE: %s", path);
377     return;
378   }
379
380   if (event & IN_ATTRIB) {
381     output("STATS\n%s\n", path);
382     userlog(LOG_DEBUG, "STATS: %s", path);
383     return;
384   }
385
386   if (event & IN_DELETE || event & IN_MOVED_FROM) {
387     output("DELETE\n%s\n", path);
388     userlog(LOG_DEBUG, "DELETE: %s", path);
389     return;
390   }
391
392   if (event & IN_UNMOUNT) {
393     output("RESET\n");
394     userlog(LOG_DEBUG, "RESET");
395     return;
396   }
397 }
398
399
400 static void output(const char* format, ...) {
401   if (self_test) {
402     return;
403   }
404
405   va_list ap;
406   va_start(ap, format);
407   vprintf(format, ap);
408   va_end(ap);
409 }