1 #include <errno.h>
2 #include <string.h>
3 #ifdef HAS_INOTIFY
4 #include <sys/inotify.h>
5 #elif HAS_KQUEUE
6 // clang-format off
7 #include <sys/types.h>
8 // clang-format on
9 #include <sys/event.h>
10 #undef EV_ERROR              // Avoid clashing with libev's EV_ERROR
11 #include <fcntl.h>           // For O_RDONLY
12 #include <sys/time.h>        // For struct timespec
13 #include <unistd.h>          // For open
14 #endif
15 
16 #include <ev.h>
17 #include <uthash.h>
18 
19 #include "file_watch.h"
20 #include "list.h"
21 #include "log.h"
22 #include "utils.h"
23 
24 struct watched_file {
25 	int wd;
26 	void *ud;
27 	file_watch_cb_t cb;
28 
29 	UT_hash_handle hh;
30 };
31 
32 struct file_watch_registry {
33 	struct ev_io w;
34 
35 	struct watched_file *reg;
36 };
37 
file_watch_ev_cb(EV_P attr_unused,struct ev_io * w,int revent attr_unused)38 static void file_watch_ev_cb(EV_P attr_unused, struct ev_io *w, int revent attr_unused) {
39 	auto fwr = (struct file_watch_registry *)w;
40 
41 	while (true) {
42 		int wd = -1;
43 #ifdef HAS_INOTIFY
44 		struct inotify_event inotify_event;
45 		auto ret = read(w->fd, &inotify_event, sizeof(struct inotify_event));
46 		if (ret < 0) {
47 			if (errno != EAGAIN) {
48 				log_error_errno("Failed to read from inotify fd");
49 			}
50 			break;
51 		}
52 		wd = inotify_event.wd;
53 #elif HAS_KQUEUE
54 		struct kevent ev;
55 		struct timespec timeout = {0};
56 		int ret = kevent(fwr->w.fd, NULL, 0, &ev, 1, &timeout);
57 		if (ret <= 0) {
58 			if (ret < 0) {
59 				log_error_errno("Failed to get kevent");
60 			}
61 			break;
62 		}
63 		wd = (int)ev.ident;
64 #else
65 		assert(false);
66 #endif
67 
68 		struct watched_file *wf = NULL;
69 		HASH_FIND_INT(fwr->reg, &wd, wf);
70 		if (!wf) {
71 			log_warn("Got notification for a file I didn't watch.");
72 			continue;
73 		}
74 		wf->cb(wf->ud);
75 	}
76 }
77 
file_watch_init(EV_P)78 void *file_watch_init(EV_P) {
79 	log_debug("Starting watching for file changes");
80 	int fd = -1;
81 #ifdef HAS_INOTIFY
82 	fd = inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
83 	if (fd < 0) {
84 		log_error_errno("inotify_init1 failed");
85 		return NULL;
86 	}
87 #elif HAS_KQUEUE
88 	fd = kqueue();
89 	if (fd < 0) {
90 		log_error_errno("Failed to create kqueue");
91 		return NULL;
92 	}
93 #else
94 	log_info("No file watching support found on the host system.");
95 	return NULL;
96 #endif
97 	auto fwr = ccalloc(1, struct file_watch_registry);
98 	ev_io_init(&fwr->w, file_watch_ev_cb, fd, EV_READ);
99 	ev_io_start(EV_A_ & fwr->w);
100 
101 	return fwr;
102 }
103 
file_watch_destroy(EV_P_ void * _fwr)104 void file_watch_destroy(EV_P_ void *_fwr) {
105 	log_debug("Stopping watching for file changes");
106 	auto fwr = (struct file_watch_registry *)_fwr;
107 	struct watched_file *i, *tmp;
108 
109 	HASH_ITER(hh, fwr->reg, i, tmp) {
110 		HASH_DEL(fwr->reg, i);
111 #ifdef HAS_KQUEUE
112 		// kqueue watch descriptors are file descriptors of
113 		// the files we are watching, so we need to close
114 		// them
115 		close(i->wd);
116 #endif
117 		free(i);
118 	}
119 
120 	ev_io_stop(EV_A_ & fwr->w);
121 	close(fwr->w.fd);
122 	free(fwr);
123 }
124 
file_watch_add(void * _fwr,const char * filename,file_watch_cb_t cb,void * ud)125 bool file_watch_add(void *_fwr, const char *filename, file_watch_cb_t cb, void *ud) {
126 	log_debug("Adding \"%s\" to watched files", filename);
127 	auto fwr = (struct file_watch_registry *)_fwr;
128 	int wd = -1;
129 
130 	struct stat statbuf;
131 	int ret = stat(filename, &statbuf);
132 	if (ret < 0) {
133 		log_error_errno("Failed to retrieve information about file \"%s\"", filename);
134 		return false;
135 	}
136 	if (!S_ISREG(statbuf.st_mode)) {
137 		log_info("\"%s\" is not a regular file, not watching it.", filename);
138 		return false;
139 	}
140 
141 #ifdef HAS_INOTIFY
142 	wd = inotify_add_watch(fwr->w.fd, filename,
143 	                       IN_CLOSE_WRITE | IN_MOVE_SELF | IN_DELETE_SELF);
144 	if (wd < 0) {
145 		log_error_errno("Failed to watch file \"%s\"", filename);
146 		return false;
147 	}
148 #elif HAS_KQUEUE
149 	wd = open(filename, O_RDONLY);
150 	if (wd < 0) {
151 		log_error_errno("Cannot open file \"%s\" for watching", filename);
152 		return false;
153 	}
154 
155 	uint32_t fflags = NOTE_DELETE | NOTE_RENAME | NOTE_REVOKE | NOTE_ATTRIB;
156 	// NOTE_CLOSE_WRITE is relatively new, so we cannot just use it
157 #ifdef NOTE_CLOSE_WRITE
158 	fflags |= NOTE_CLOSE_WRITE;
159 #else
160 	// NOTE_WRITE will receive notification more frequent than necessary, so is less
161 	// preferrable
162 	fflags |= NOTE_WRITE;
163 #endif
164 	struct kevent ev = {
165 	    .ident = (unsigned int)wd,        // the wd < 0 case is checked above
166 	    .filter = EVFILT_VNODE,
167 	    .flags = EV_ADD | EV_CLEAR,
168 	    .fflags = fflags,
169 	    .data = 0,
170 	    .udata = NULL,
171 	};
172 	if (kevent(fwr->w.fd, &ev, 1, NULL, 0, NULL) < 0) {
173 		log_error_errno("Failed to register kevent");
174 		close(wd);
175 		return false;
176 	}
177 #else
178 	assert(false);
179 #endif        // HAS_KQUEUE
180 
181 	auto w = ccalloc(1, struct watched_file);
182 	w->wd = wd;
183 	w->cb = cb;
184 	w->ud = ud;
185 
186 	HASH_ADD_INT(fwr->reg, wd, w);
187 	return true;
188 }
189