Line data Source code
1 : /*
2 : Unix SMB/CIFS implementation.
3 :
4 : Copyright (C) Andrew Tridgell 2006
5 :
6 : This program is free software; you can redistribute it and/or modify
7 : it under the terms of the GNU General Public License as published by
8 : the Free Software Foundation; either version 3 of the License, or
9 : (at your option) any later version.
10 :
11 : This program is distributed in the hope that it will be useful,
12 : but WITHOUT ANY WARRANTY; without even the implied warranty of
13 : MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 : GNU General Public License for more details.
15 :
16 : You should have received a copy of the GNU General Public License
17 : along with this program. If not, see <http://www.gnu.org/licenses/>.
18 : */
19 :
20 : /*
21 : this is the change notify database. It implements mechanisms for
22 : storing current change notify waiters in a tdb, and checking if a
23 : given event matches any of the stored notify waiiters.
24 : */
25 :
26 : #include "includes.h"
27 : #include "system/filesys.h"
28 : #include "messaging/messaging.h"
29 : #include "lib/messaging/irpc.h"
30 : #include "librpc/gen_ndr/ndr_notify.h"
31 : #include "../lib/util/dlinklist.h"
32 : #include "ntvfs/common/ntvfs_common.h"
33 : #include "ntvfs/sysdep/sys_notify.h"
34 : #include "cluster/cluster.h"
35 : #include "param/param.h"
36 : #include "lib/util/tsort.h"
37 : #include "lib/dbwrap/dbwrap.h"
38 : #include "../lib/util/util_tdb.h"
39 :
40 : struct notify_context {
41 : struct db_context *db;
42 : struct server_id server;
43 : struct imessaging_context *imessaging_ctx;
44 : struct notify_list *list;
45 : struct notify_array *array;
46 : int64_t seqnum;
47 : struct sys_notify_context *sys_notify_ctx;
48 : };
49 :
50 :
51 : struct notify_list {
52 : struct notify_list *next, *prev;
53 : void *private_data;
54 : void (*callback)(void *, const struct notify_event *);
55 : void *sys_notify_handle;
56 : int depth;
57 : };
58 :
59 : #define NOTIFY_KEY "notify array"
60 :
61 : #define NOTIFY_ENABLE "notify:enable"
62 : #define NOTIFY_ENABLE_DEFAULT true
63 :
64 : static NTSTATUS notify_remove_all(struct notify_context *notify);
65 : static void notify_handler(struct imessaging_context *msg_ctx,
66 : void *private_data,
67 : uint32_t msg_type,
68 : struct server_id server_id,
69 : size_t num_fds,
70 : int *fds,
71 : DATA_BLOB *data);
72 :
73 : /*
74 : destroy the notify context
75 : */
76 1328 : static int notify_destructor(struct notify_context *notify)
77 : {
78 1328 : imessaging_deregister(notify->imessaging_ctx, MSG_PVFS_NOTIFY, notify);
79 1328 : notify_remove_all(notify);
80 1328 : return 0;
81 : }
82 :
83 : /*
84 : Open up the notify.tdb database. You should close it down using
85 : talloc_free(). We need the imessaging_ctx to allow for notifications
86 : via internal messages
87 : */
88 1328 : struct notify_context *notify_init(TALLOC_CTX *mem_ctx, struct server_id server,
89 : struct imessaging_context *imessaging_ctx,
90 : struct loadparm_context *lp_ctx,
91 : struct tevent_context *ev,
92 : struct share_config *scfg)
93 : {
94 0 : struct notify_context *notify;
95 :
96 1328 : if (share_bool_option(scfg, NOTIFY_ENABLE, NOTIFY_ENABLE_DEFAULT) != true) {
97 0 : return NULL;
98 : }
99 :
100 1328 : if (ev == NULL) {
101 0 : return NULL;
102 : }
103 :
104 1328 : notify = talloc(mem_ctx, struct notify_context);
105 1328 : if (notify == NULL) {
106 0 : return NULL;
107 : }
108 :
109 1328 : notify->db = cluster_db_tmp_open(notify, lp_ctx, "notify", TDB_SEQNUM);
110 1328 : if (notify->db == NULL) {
111 0 : talloc_free(notify);
112 0 : return NULL;
113 : }
114 :
115 1328 : notify->server = server;
116 1328 : notify->imessaging_ctx = imessaging_ctx;
117 1328 : notify->list = NULL;
118 1328 : notify->array = NULL;
119 1328 : notify->seqnum = dbwrap_get_seqnum(notify->db);
120 :
121 1328 : talloc_set_destructor(notify, notify_destructor);
122 :
123 : /* register with the messaging subsystem for the notify
124 : message type */
125 1328 : imessaging_register(notify->imessaging_ctx, notify,
126 : MSG_PVFS_NOTIFY, notify_handler);
127 :
128 1328 : notify->sys_notify_ctx = sys_notify_context_create(scfg, notify, ev);
129 :
130 1328 : return notify;
131 : }
132 :
133 :
134 : /*
135 : lock the notify db
136 : */
137 1026 : static struct db_record *notify_lock(struct notify_context *notify)
138 : {
139 1026 : TDB_DATA key = string_term_tdb_data(NOTIFY_KEY);
140 :
141 1026 : return dbwrap_fetch_locked(notify->db, notify, key);
142 : }
143 :
144 1026 : static void notify_unlock(struct db_record *lock)
145 : {
146 1026 : talloc_free(lock);
147 1026 : }
148 :
149 : /*
150 : load the notify array
151 : */
152 211386 : static NTSTATUS notify_load(struct notify_context *notify)
153 : {
154 0 : TDB_DATA dbuf;
155 0 : DATA_BLOB blob;
156 0 : enum ndr_err_code ndr_err;
157 0 : int seqnum;
158 0 : NTSTATUS status;
159 :
160 211386 : seqnum = dbwrap_get_seqnum(notify->db);
161 :
162 211386 : if (seqnum == notify->seqnum && notify->array != NULL) {
163 209165 : return NT_STATUS_OK;
164 : }
165 :
166 2221 : notify->seqnum = seqnum;
167 :
168 2221 : talloc_free(notify->array);
169 2221 : notify->array = talloc_zero(notify, struct notify_array);
170 2221 : NT_STATUS_HAVE_NO_MEMORY(notify->array);
171 :
172 2221 : status = dbwrap_fetch_bystring(notify->db, notify, NOTIFY_KEY, &dbuf);
173 2221 : if (!NT_STATUS_IS_OK(status)) {
174 1364 : return NT_STATUS_OK;
175 : }
176 :
177 857 : blob.data = dbuf.dptr;
178 857 : blob.length = dbuf.dsize;
179 :
180 857 : ndr_err = ndr_pull_struct_blob(&blob, notify->array, notify->array,
181 : (ndr_pull_flags_fn_t)ndr_pull_notify_array);
182 857 : talloc_free(dbuf.dptr);
183 857 : if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
184 0 : return ndr_map_error2ntstatus(ndr_err);
185 : }
186 :
187 857 : return NT_STATUS_OK;
188 : }
189 :
190 : /*
191 : compare notify entries for sorting
192 : */
193 89 : static int notify_compare(const void *p1, const void *p2)
194 : {
195 89 : const struct notify_entry *e1 = p1, *e2 = p2;
196 89 : return strcmp(e1->path, e2->path);
197 : }
198 :
199 : /*
200 : save the notify array
201 : */
202 1026 : static NTSTATUS notify_save(struct notify_context *notify)
203 : {
204 0 : TDB_DATA dbuf;
205 0 : DATA_BLOB blob;
206 0 : enum ndr_err_code ndr_err;
207 0 : TALLOC_CTX *tmp_ctx;
208 0 : NTSTATUS status;
209 :
210 : /* if possible, remove some depth arrays */
211 5466 : while (notify->array->num_depths > 0 &&
212 4973 : notify->array->depth[notify->array->num_depths-1].num_entries == 0) {
213 4440 : notify->array->num_depths--;
214 : }
215 :
216 : /* we might just be able to delete the record */
217 1026 : if (notify->array->num_depths == 0) {
218 493 : return dbwrap_delete_bystring(notify->db, NOTIFY_KEY);
219 : }
220 :
221 533 : tmp_ctx = talloc_new(notify);
222 533 : NT_STATUS_HAVE_NO_MEMORY(tmp_ctx);
223 :
224 533 : ndr_err = ndr_push_struct_blob(&blob, tmp_ctx, notify->array,
225 : (ndr_push_flags_fn_t)ndr_push_notify_array);
226 533 : if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
227 0 : talloc_free(tmp_ctx);
228 0 : return ndr_map_error2ntstatus(ndr_err);
229 : }
230 :
231 533 : dbuf.dptr = blob.data;
232 533 : dbuf.dsize = blob.length;
233 :
234 533 : status = dbwrap_store_bystring(notify->db, NOTIFY_KEY, dbuf,
235 : TDB_REPLACE);
236 533 : talloc_free(tmp_ctx);
237 533 : return status;
238 : }
239 :
240 :
241 : /*
242 : handle incoming notify messages
243 : */
244 431 : static void notify_handler(struct imessaging_context *msg_ctx,
245 : void *private_data,
246 : uint32_t msg_type,
247 : struct server_id server_id,
248 : size_t num_fds,
249 : int *fds,
250 : DATA_BLOB *data)
251 : {
252 431 : struct notify_context *notify = talloc_get_type(private_data, struct notify_context);
253 0 : enum ndr_err_code ndr_err;
254 0 : struct notify_event ev;
255 431 : TALLOC_CTX *tmp_ctx = talloc_new(notify);
256 0 : struct notify_list *listel;
257 :
258 431 : if (num_fds != 0) {
259 0 : DBG_WARNING("Received %zu fds, ignoring message\n", num_fds);
260 0 : return;
261 : }
262 :
263 431 : if (tmp_ctx == NULL) {
264 0 : return;
265 : }
266 :
267 431 : ndr_err = ndr_pull_struct_blob(data, tmp_ctx, &ev,
268 : (ndr_pull_flags_fn_t)ndr_pull_notify_event);
269 431 : if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
270 0 : talloc_free(tmp_ctx);
271 0 : return;
272 : }
273 :
274 2193 : for (listel=notify->list;listel;listel=listel->next) {
275 2191 : if (listel->private_data == ev.private_data) {
276 429 : listel->callback(listel->private_data, &ev);
277 429 : break;
278 : }
279 : }
280 :
281 431 : talloc_free(tmp_ctx);
282 : }
283 :
284 : /*
285 : callback from sys_notify telling us about changes from the OS
286 : */
287 0 : static void sys_notify_callback(struct sys_notify_context *ctx,
288 : void *ptr, struct notify_event *ev)
289 : {
290 0 : struct notify_list *listel = talloc_get_type(ptr, struct notify_list);
291 0 : ev->private_data = listel;
292 0 : listel->callback(listel->private_data, ev);
293 0 : }
294 :
295 : /*
296 : add an entry to the notify array
297 : */
298 513 : static NTSTATUS notify_add_array(struct notify_context *notify, struct notify_entry *e,
299 : void *private_data, int depth)
300 : {
301 0 : int i;
302 0 : struct notify_depth *d;
303 0 : struct notify_entry *ee;
304 :
305 : /* possibly expand the depths array */
306 513 : if (depth >= notify->array->num_depths) {
307 495 : d = talloc_realloc(notify->array, notify->array->depth,
308 : struct notify_depth, depth+1);
309 495 : NT_STATUS_HAVE_NO_MEMORY(d);
310 4935 : for (i=notify->array->num_depths;i<=depth;i++) {
311 4440 : ZERO_STRUCT(d[i]);
312 : }
313 495 : notify->array->depth = d;
314 495 : notify->array->num_depths = depth+1;
315 : }
316 513 : d = ¬ify->array->depth[depth];
317 :
318 : /* expand the entries array */
319 513 : ee = talloc_realloc(notify->array->depth, d->entries, struct notify_entry,
320 : d->num_entries+1);
321 513 : NT_STATUS_HAVE_NO_MEMORY(ee);
322 513 : d->entries = ee;
323 :
324 513 : d->entries[d->num_entries] = *e;
325 513 : d->entries[d->num_entries].private_data = private_data;
326 513 : d->entries[d->num_entries].server = notify->server;
327 513 : d->entries[d->num_entries].path_len = strlen(e->path);
328 513 : d->num_entries++;
329 :
330 513 : d->max_mask |= e->filter;
331 513 : d->max_mask_subdir |= e->subdir_filter;
332 :
333 513 : TYPESAFE_QSORT(d->entries, d->num_entries, notify_compare);
334 :
335 : /* recalculate the maximum masks */
336 513 : d->max_mask = 0;
337 513 : d->max_mask_subdir = 0;
338 :
339 1076 : for (i=0;i<d->num_entries;i++) {
340 563 : d->max_mask |= d->entries[i].filter;
341 563 : d->max_mask_subdir |= d->entries[i].subdir_filter;
342 : }
343 :
344 513 : return notify_save(notify);
345 : }
346 :
347 : /*
348 : add a notify watch. This is called when a notify is first setup on a open
349 : directory handle.
350 : */
351 513 : NTSTATUS notify_add(struct notify_context *notify, struct notify_entry *e0,
352 : void (*callback)(void *, const struct notify_event *),
353 : void *private_data)
354 : {
355 513 : struct notify_entry e = *e0;
356 0 : NTSTATUS status;
357 513 : char *tmp_path = NULL;
358 0 : struct notify_list *listel;
359 0 : size_t len;
360 0 : int depth;
361 0 : struct db_record *locked;
362 :
363 : /* see if change notify is enabled at all */
364 513 : if (notify == NULL) {
365 0 : return NT_STATUS_NOT_IMPLEMENTED;
366 : }
367 :
368 513 : locked = notify_lock(notify);
369 513 : if (!locked) {
370 0 : return NT_STATUS_INTERNAL_DB_CORRUPTION;
371 : }
372 :
373 513 : status = notify_load(notify);
374 513 : if (!NT_STATUS_IS_OK(status)) {
375 0 : goto done;
376 : }
377 :
378 : /* cope with /. on the end of the path */
379 513 : len = strlen(e.path);
380 513 : if (len > 1 && e.path[len-1] == '.' && e.path[len-2] == '/') {
381 0 : tmp_path = talloc_strndup(notify, e.path, len-2);
382 0 : if (tmp_path == NULL) {
383 0 : status = NT_STATUS_NO_MEMORY;
384 0 : goto done;
385 : }
386 0 : e.path = tmp_path;
387 : }
388 :
389 513 : depth = count_chars(e.path, '/');
390 :
391 513 : listel = talloc_zero(notify, struct notify_list);
392 513 : if (listel == NULL) {
393 0 : status = NT_STATUS_NO_MEMORY;
394 0 : goto done;
395 : }
396 :
397 513 : listel->private_data = private_data;
398 513 : listel->callback = callback;
399 513 : listel->depth = depth;
400 513 : DLIST_ADD(notify->list, listel);
401 :
402 : /* ignore failures from sys_notify */
403 513 : if (notify->sys_notify_ctx != NULL) {
404 : /*
405 : this call will modify e.filter and e.subdir_filter
406 : to remove bits handled by the backend
407 : */
408 513 : status = sys_notify_watch(notify->sys_notify_ctx, &e,
409 : sys_notify_callback, listel,
410 513 : &listel->sys_notify_handle);
411 513 : if (NT_STATUS_IS_OK(status)) {
412 0 : talloc_steal(listel, listel->sys_notify_handle);
413 : }
414 : }
415 :
416 : /* if the system notify handler couldn't handle some of the
417 : filter bits, or couldn't handle a request for recursion
418 : then we need to install it in the array used for the
419 : intra-samba notify handling */
420 513 : if (e.filter != 0 || e.subdir_filter != 0) {
421 513 : status = notify_add_array(notify, &e, private_data, depth);
422 : }
423 :
424 0 : done:
425 513 : notify_unlock(locked);
426 513 : talloc_free(tmp_path);
427 :
428 513 : return status;
429 : }
430 :
431 : /*
432 : remove a notify watch. Called when the directory handle is closed
433 : */
434 513 : NTSTATUS notify_remove(struct notify_context *notify, void *private_data)
435 : {
436 0 : NTSTATUS status;
437 0 : struct notify_list *listel;
438 0 : int i, depth;
439 0 : struct notify_depth *d;
440 0 : struct db_record *locked;
441 :
442 : /* see if change notify is enabled at all */
443 513 : if (notify == NULL) {
444 0 : return NT_STATUS_NOT_IMPLEMENTED;
445 : }
446 :
447 514 : for (listel=notify->list;listel;listel=listel->next) {
448 514 : if (listel->private_data == private_data) {
449 513 : DLIST_REMOVE(notify->list, listel);
450 513 : break;
451 : }
452 : }
453 513 : if (listel == NULL) {
454 0 : return NT_STATUS_OBJECT_NAME_NOT_FOUND;
455 : }
456 :
457 513 : depth = listel->depth;
458 :
459 513 : talloc_free(listel);
460 :
461 513 : locked = notify_lock(notify);
462 513 : if (!locked) {
463 0 : return NT_STATUS_INTERNAL_DB_CORRUPTION;
464 : }
465 :
466 513 : status = notify_load(notify);
467 513 : if (!NT_STATUS_IS_OK(status)) {
468 0 : notify_unlock(locked);
469 0 : return status;
470 : }
471 :
472 513 : if (depth >= notify->array->num_depths) {
473 0 : notify_unlock(locked);
474 0 : return NT_STATUS_OBJECT_NAME_NOT_FOUND;
475 : }
476 :
477 : /* we only have to search at the depth of this element */
478 513 : d = ¬ify->array->depth[depth];
479 :
480 546 : for (i=0;i<d->num_entries;i++) {
481 546 : if (private_data == d->entries[i].private_data &&
482 513 : cluster_id_equal(¬ify->server, &d->entries[i].server)) {
483 513 : break;
484 : }
485 : }
486 513 : if (i == d->num_entries) {
487 0 : notify_unlock(locked);
488 0 : return NT_STATUS_OBJECT_NAME_NOT_FOUND;
489 : }
490 :
491 513 : if (i < d->num_entries-1) {
492 9 : memmove(&d->entries[i], &d->entries[i+1],
493 9 : sizeof(d->entries[i])*(d->num_entries-(i+1)));
494 : }
495 513 : d->num_entries--;
496 :
497 513 : status = notify_save(notify);
498 :
499 513 : notify_unlock(locked);
500 :
501 513 : return status;
502 : }
503 :
504 : /*
505 : remove all notify watches for this messaging server
506 : */
507 1328 : static NTSTATUS notify_remove_all(struct notify_context *notify)
508 : {
509 0 : NTSTATUS status;
510 1328 : int i, depth, del_count=0;
511 0 : struct db_record *locked;
512 :
513 1328 : if (notify->list == NULL) {
514 1328 : return NT_STATUS_OK;
515 : }
516 :
517 0 : locked = notify_lock(notify);
518 0 : if (!locked) {
519 0 : return NT_STATUS_INTERNAL_DB_CORRUPTION;
520 : }
521 :
522 0 : status = notify_load(notify);
523 0 : if (!NT_STATUS_IS_OK(status)) {
524 0 : notify_unlock(locked);
525 0 : return status;
526 : }
527 :
528 : /* we have to search for all entries across all depths, looking for matches
529 : for our server id */
530 0 : for (depth=0;depth<notify->array->num_depths;depth++) {
531 0 : struct notify_depth *d = ¬ify->array->depth[depth];
532 0 : for (i=0;i<d->num_entries;i++) {
533 0 : if (cluster_id_equal(¬ify->server, &d->entries[i].server)) {
534 0 : if (i < d->num_entries-1) {
535 0 : memmove(&d->entries[i], &d->entries[i+1],
536 0 : sizeof(d->entries[i])*(d->num_entries-(i+1)));
537 : }
538 0 : i--;
539 0 : d->num_entries--;
540 0 : del_count++;
541 : }
542 : }
543 : }
544 :
545 0 : if (del_count > 0) {
546 0 : status = notify_save(notify);
547 : }
548 :
549 0 : notify_unlock(locked);
550 :
551 0 : return status;
552 : }
553 :
554 :
555 : /*
556 : send a notify message to another messaging server
557 : */
558 429 : static void notify_send(struct notify_context *notify, struct notify_entry *e,
559 : const char *path, uint32_t action)
560 : {
561 0 : struct notify_event ev;
562 0 : DATA_BLOB data;
563 0 : NTSTATUS status;
564 0 : enum ndr_err_code ndr_err;
565 0 : TALLOC_CTX *tmp_ctx;
566 :
567 429 : ev.action = action;
568 429 : ev.dir = discard_const_p(char, "");
569 429 : ev.path = path;
570 429 : ev.private_data = e->private_data;
571 :
572 429 : tmp_ctx = talloc_new(notify);
573 :
574 429 : ndr_err = ndr_push_struct_blob(&data, tmp_ctx, &ev, (ndr_push_flags_fn_t)ndr_push_notify_event);
575 429 : if (!NDR_ERR_CODE_IS_SUCCESS(ndr_err)) {
576 0 : talloc_free(tmp_ctx);
577 0 : return;
578 : }
579 :
580 429 : status = imessaging_send(notify->imessaging_ctx, e->server,
581 : MSG_PVFS_NOTIFY, &data);
582 429 : if (!NT_STATUS_IS_OK(status)) {
583 0 : talloc_free(tmp_ctx);
584 0 : return;
585 : }
586 :
587 429 : talloc_free(tmp_ctx);
588 : }
589 :
590 :
591 : /*
592 : trigger a notify message for anyone waiting on a matching event
593 :
594 : This function is called a lot, and needs to be very fast. The unusual data structure
595 : and traversal is designed to be fast in the average case, even for large numbers of
596 : notifies
597 : */
598 210360 : void notify_trigger(struct notify_context *notify,
599 : uint32_t action, uint32_t filter, const char *path)
600 : {
601 0 : NTSTATUS status;
602 0 : int depth;
603 0 : const char *p, *next_p;
604 :
605 : /* see if change notify is enabled at all */
606 210360 : if (notify == NULL) {
607 0 : return;
608 : }
609 :
610 210360 : status = notify_load(notify);
611 210360 : if (!NT_STATUS_IS_OK(status)) {
612 0 : return;
613 : }
614 :
615 : /* loop along the given path, working with each directory depth separately */
616 210360 : for (depth=0,p=path;
617 221167 : p && depth < notify->array->num_depths;
618 10807 : p=next_p,depth++) {
619 10807 : int p_len = p - path;
620 0 : int min_i, max_i, i;
621 10807 : struct notify_depth *d = ¬ify->array->depth[depth];
622 10807 : next_p = strchr(p+1, '/');
623 :
624 : /* see if there are any entries at this depth */
625 10807 : if (d->num_entries == 0) continue;
626 :
627 : /* try to skip based on the maximum mask. If next_p is
628 : NULL then we know it will be a 'this directory'
629 : match, otherwise it must be a subdir match */
630 1252 : if (next_p != NULL) {
631 76 : if (0 == (filter & d->max_mask_subdir)) {
632 6 : continue;
633 : }
634 : } else {
635 1176 : if (0 == (filter & d->max_mask)) {
636 937 : continue;
637 : }
638 : }
639 :
640 : /* we know there is an entry here worth looking
641 : for. Use a bisection search to find the first entry
642 : with a matching path */
643 309 : min_i = 0;
644 309 : max_i = d->num_entries-1;
645 :
646 577 : while (min_i < max_i) {
647 0 : struct notify_entry *e;
648 0 : int cmp;
649 268 : i = (min_i+max_i)/2;
650 268 : e = &d->entries[i];
651 268 : cmp = strncmp(path, e->path, p_len);
652 268 : if (cmp == 0) {
653 196 : if (p_len == e->path_len) {
654 196 : max_i = i;
655 : } else {
656 0 : max_i = i-1;
657 : }
658 72 : } else if (cmp < 0) {
659 26 : max_i = i-1;
660 : } else {
661 46 : min_i = i+1;
662 : }
663 : }
664 :
665 309 : if (min_i != max_i) {
666 : /* none match */
667 0 : continue;
668 : }
669 :
670 : /* we now know that the entries start at min_i */
671 826 : for (i=min_i;i<d->num_entries;i++) {
672 583 : struct notify_entry *e = &d->entries[i];
673 583 : if (p_len != e->path_len ||
674 521 : strncmp(path, e->path, p_len) != 0) break;
675 517 : if (next_p != NULL) {
676 200 : if (0 == (filter & e->subdir_filter)) {
677 68 : continue;
678 : }
679 : } else {
680 317 : if (0 == (filter & e->filter)) {
681 20 : continue;
682 : }
683 : }
684 429 : notify_send(notify, e, path + e->path_len + 1, action);
685 : }
686 : }
687 : }
|