Merge lp:~jamesodhunt/upstart/file-bridge-MP into lp:upstart

Proposed by James Hunt
Status: Merged
Merged at revision: 1451
Proposed branch: lp:~jamesodhunt/upstart/file-bridge-MP
Merge into: lp:upstart
Diff against target: 2336 lines (+2280/-4)
6 files modified
ChangeLog (+22/-0)
extra/Makefile.am (+17/-4)
extra/conf/upstart-file-bridge.conf (+19/-0)
extra/man/file-event.7 (+99/-0)
extra/man/upstart-file-bridge.8 (+221/-0)
extra/upstart-file-bridge.c (+1902/-0)
To merge this branch: bzr merge lp:~jamesodhunt/upstart/file-bridge-MP
Reviewer Review Type Date Requested Status
Dimitri John Ledkov Approve
Review via email: mp+152767@code.launchpad.net

Description of the change

New bridge that allows jobs to react to file creation, modification and deletion events on files and directories. Basic globbing is also supported. See the manual pages for details.

* extra/Makefile.am: Add file bridge and conf file.
* extra/upstart-file-bridge.c: Inotify file bridge.
* extra/conf/upstart-file-bridge.conf: Conf file for
  file bridge.
* extra/man/file-event.7: New man page.
* extra/man/upstart-file-bridge.8: New man page.

To post a comment you must log in.
Revision history for this message
James Hunt (jamesodhunt) wrote :

Note that to test this bridge, a build of NIH containing a fix for bug 777097 is required (use lp:~upstart-devel/libnih/nih for example).

Additionally, a proper fix for bug 1103406 is needed.

Revision history for this message
James Hunt (jamesodhunt) wrote :

That should be bug 776532, not 1103406.

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

On 11 March 2013 20:08, James Hunt <email address hidden> wrote:
> James Hunt has proposed merging lp:~jamesodhunt/upstart/file-bridge-MP into lp:upstart.
>
> === added file 'extra/man/file-event.7'
> --- extra/man/file-event.7 1970-01-01 00:00:00 +0000
> +++ extra/man/file-event.7 2013-03-11 20:07:26 +0000
> +.IP \(bu
> +If you wish to match on
> +.BR FMATCH ", "
> +ensure that
> +.B FPATH
> +does not contain multiple contiguous runs of slashes since otherwise
> +your job will find it difficult to perform such a match.
> +.\"

I didn't know the word "contiguous". I would have used "consecutive"
or "continuous".
Same for the comments in the code, where "contiguous" is also used.

> +/**
> + * expand_path:
> + *
> + * @parent: parent,
> + * @path: path.
> + *
> + * Expand @path by replacing a leading '~/', './' or no path prefix by
> + * the users home directory.
> + *
> + * Limitations: Does not expand '~user'.
> + *
> + * Returns: Newly-allocated fully-expanded path, or NULL on error.
> + **/

> +/**
> + * path_valid:
> + *
> + * @path: path.
> + *
> + * Perform basic tests to determine if @path is valid for
> + * the purposes of this bridge.
> + *
> + * Returns: TRUE if @path is acceptable, else FALSE.
> + **/

So in both of the above, no variable substitutions? Or the upstart
side of things would have replaced variables already for us (didn't
check thoughtfully)?
I am thinking about stuff like watching for file or directory that has
$(INSTANCE_NAME) in it.
I guess there should always be a "one up" location to watch for.

Overall the approach taken is sound and easy to follow and the code is
beautiful as usually. Building and testing here locally for now.

Have you considered using dbusmock for testing this bridge? (i guess
same applies to other bridges as well)

Regards,

Dmitrijs.

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

# stop upstart-file-bridge

# cat /etc/init/subdir1.conf
start on file FPATH=/foo/a/test FEVENT=create
exec echo FPATH=$FPATH FEVENT=$FEVENT FMATCH=$FMATCH

# start upstart-file-bridge
# mkdir /foo
# mkdir /foo/a
# touch /foo/a/test

In the upstart-file-bridge.log:
Job got added /com/ubuntu/Upstart/jobs/subdir1
upstart-file-bridge: Could not create watch for path /: No such file or directory

Is this a limitation or a bug?

review: Needs Information
Revision history for this message
Steve Langasek (vorlon) wrote :

On Thu, Mar 14, 2013 at 06:53:22PM -0000, Dmitrijs Ledkovs wrote:
> Review: Needs Information

> # stop upstart-file-bridge

> # cat /etc/init/subdir1.conf
> start on file FPATH=/foo/a/test FEVENT=create
> exec echo FPATH=$FPATH FEVENT=$FEVENT FMATCH=$FMATCH

> # start upstart-file-bridge
> # mkdir /foo
> # mkdir /foo/a
> # touch /foo/a/test

> In the upstart-file-bridge.log:
> Job got added /com/ubuntu/Upstart/jobs/subdir1
> upstart-file-bridge: Could not create watch for path /: No such file or directory

> Is this a limitation or a bug?

Limitation. You're expected to only be able to watch for a) modification of
a single path in an existing directory, or b) creation/deletion of files in
an existing directory. Monitoring for recursive directory creation is
unwieldly via inotify, and out of scope for what we understand the desktop
requirements to be.

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

Moving start of upstart-file-bridge, after /foo is created, doesn't emit event either.
Moving start of upstart-file-bridge, after /foo/a is created, event is emitted and subdir1 job is started.

If /foo/a/ is created, file bridge started, and /a/ subfolder is deleted & recreated, touching /foo/a/test does not emit an event.

I'm not entirely sure about emitting create events, upon start up either.
For example, in my ssh config I have ControlPath set to ~/.cache/ssh/%r@%h:%p, which can be a stale file. Upon connecting ssh client will notice that it's stale and recreate it and that's when I want my upstart job to start.

I think simple things like this can be easily scripted into a shell based integration test.
I have been using inotifywait -m -r /foo, to check that inotify events are actually emitted.

review: Needs Fixing
Revision history for this message
Dimitri John Ledkov (xnox) wrote :

On 14 March 2013 19:13, Steve Langasek <email address hidden> wrote:
> On Thu, Mar 14, 2013 at 06:53:22PM -0000, Dmitrijs Ledkovs wrote:
>> Review: Needs Information
>
>> # stop upstart-file-bridge
>
>> # cat /etc/init/subdir1.conf
>> start on file FPATH=/foo/a/test FEVENT=create
>> exec echo FPATH=$FPATH FEVENT=$FEVENT FMATCH=$FMATCH
>
>> # start upstart-file-bridge
>> # mkdir /foo
>> # mkdir /foo/a
>> # touch /foo/a/test
>
>> In the upstart-file-bridge.log:
>> Job got added /com/ubuntu/Upstart/jobs/subdir1
>> upstart-file-bridge: Could not create watch for path /: No such file or directory
>
>> Is this a limitation or a bug?
>
> Limitation. You're expected to only be able to watch for a) modification of
> a single path in an existing directory, or b) creation/deletion of files in
> an existing directory. Monitoring for recursive directory creation is
> unwieldly via inotify, and out of scope for what we understand the desktop
> requirements to be.
>

Me and steve chatted about this a little on irc on #ubuntu-devel.
In short, we believe we should be supporting the fact that all
/dir/ect/ori/es may not exist at first, but if they got finally
created we stop caring about changes to them (e.g. no support for
tracking removals, re-additions, etc of the dir being watched).
Prototyping here with inotifywait the solution is: if the dir exists
establish the watch & horay emit events as the jobs want.
If that dir does not exist, transverse to the first one that does and
establish a watch which is waiting for "CREATE,ISDIR $subdir", if that
fires establish a new watch on the $subdir, if that's the target we
suppose to watch huray, otherwise wait for "CREATE,ISDIR $nextsubdir",
the old watch can be simply destroyed. It looks like that's what the
code intended to do, but somehow failing.

I did a test for "file FPATH=/foobar.txt FEVENT=create" and that
failed, although that should also be working correctly.

Regards,

Dmitrijs.

1450. By James Hunt

* extra/man/file-event.7: Simplify language.
* extra/upstart-file-bridge.c:
  - skip_slashes(): New macro to make path matching more reliable.
  - file_filter(): Call skip_slashes().
  - create_handler(): Call skip_slashes().
  - modify_handler(): Call skip_slashes().
  - delete_handler(): Call skip_slashes().
  - watched_dir_new(): Special case watching the root directory.

1451. By James Hunt

* extra/conf/upstart-file-bridge.conf: Change start on condition
  to ensure all filesystems are mounted before it starts.

Revision history for this message
James Hunt (jamesodhunt) wrote :

No, the bridge doesn't expand variables since it doesn't have access to the jobs environment.

> upstart-file-bridge: Could not create watch for path /: No such file or directory
This was actually a bug which I have now fixed. Your other examples should now work too :)

Regarding your comments on how the bridge should work - that is exactly how it does work (as documented at the top of the file ;-)

As Steve has mentioned, the bridge as it currently stands is very simple: it should provide "just enough" functionality to be useful but there is certainly scope for future enhancement.

Revision history for this message
Dimitri John Ledkov (xnox) wrote :

> No, the bridge doesn't expand variables since it doesn't have access to the
> jobs environment.
>

Ok. Good point.

> > upstart-file-bridge: Could not create watch for path /: No such file or
> directory
> This was actually a bug which I have now fixed. Your other examples should now
> work too :)
>

yeah \o/

> Regarding your comments on how the bridge should work - that is exactly how it
> does work (as documented at the top of the file ;-)
>

awesome, that was my understand, but I got lost a bit in watch removal.

> As Steve has mentioned, the bridge as it currently stands is very simple: it
> should provide "just enough" functionality to be useful but there is certainly
> scope for future enhancement.

Ok. Let me retest.

Revision history for this message
Dimitri John Ledkov (xnox) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ChangeLog'
2--- ChangeLog 2013-03-04 11:53:08 +0000
3+++ ChangeLog 2013-03-15 12:53:28 +0000
4@@ -1,3 +1,25 @@
5+2013-03-15 James Hunt <james.hunt@ubuntu.com>
6+
7+ * extra/man/file-event.7: Simplify language.
8+ * extra/upstart-file-bridge.c:
9+ - skip_slashes(): New macro to make path matching more reliable.
10+ - file_filter(): Call skip_slashes().
11+ - create_handler(): Call skip_slashes().
12+ - modify_handler(): Call skip_slashes().
13+ - delete_handler(): Call skip_slashes().
14+ - watched_dir_new(): Special case watching the root directory.
15+ * extra/conf/upstart-file-bridge.conf: Change start on condition
16+ to ensure all filesystems are mounted before it starts.
17+
18+2013-03-11 James Hunt <james.hunt@ubuntu.com>
19+
20+ * extra/Makefile.am: Add file bridge and conf file.
21+ * extra/upstart-file-bridge.c: Inotify file bridge.
22+ * extra/conf/upstart-file-bridge.conf: Conf file for
23+ file bridge.
24+ * extra/man/file-event.7: New man page.
25+ * extra/man/upstart-file-bridge.8: New man page.
26+
27 2013-03-04 James Hunt <james.hunt@ubuntu.com>
28
29 * init/session.c: session_from_dbus(): Fixed off-by-one
30
31=== modified file 'extra/Makefile.am'
32--- extra/Makefile.am 2012-12-19 14:46:53 +0000
33+++ extra/Makefile.am 2013-03-15 12:53:28 +0000
34@@ -18,16 +18,20 @@
35
36 sbin_PROGRAMS = \
37 upstart-socket-bridge \
38- upstart-event-bridge
39+ upstart-event-bridge \
40+ upstart-file-bridge
41
42 dist_init_DATA = \
43 conf/upstart-socket-bridge.conf \
44- conf/upstart-event-bridge.conf
45+ conf/upstart-event-bridge.conf \
46+ conf/upstart-file-bridge.conf
47
48 dist_man_MANS = \
49 man/upstart-socket-bridge.8 \
50 man/upstart-event-bridge.8 \
51- man/socket-event.7
52+ man/upstart-file-bridge.8 \
53+ man/socket-event.7 \
54+ man/file-event.7
55
56 upstart_socket_bridge_SOURCES = \
57 upstart-socket-bridge.c
58@@ -51,7 +55,16 @@
59 $(NIH_DBUS_LIBS) \
60 $(DBUS_LIBS)
61
62-
63+upstart_file_bridge_SOURCES = \
64+ upstart-file-bridge.c
65+nodist_upstart_file_bridge_SOURCES = \
66+ $(com_ubuntu_Upstart_OUTPUTS) \
67+ $(com_ubuntu_Upstart_Job_OUTPUTS)
68+upstart_file_bridge_LDADD = \
69+ $(LTLIBINTL) \
70+ $(NIH_LIBS) \
71+ $(NIH_DBUS_LIBS) \
72+ $(DBUS_LIBS)
73
74 if HAVE_UDEV
75 dist_init_DATA += \
76
77=== added file 'extra/conf/upstart-file-bridge.conf'
78--- extra/conf/upstart-file-bridge.conf 1970-01-01 00:00:00 +0000
79+++ extra/conf/upstart-file-bridge.conf 2013-03-15 12:53:28 +0000
80@@ -0,0 +1,19 @@
81+# upstart-file-bridge - Bridge file events into upstart
82+#
83+# This helper daemon receives inotify(7) events and
84+# emits equivalent Upstart events.
85+
86+description "Bridge file events into upstart"
87+
88+emits file
89+
90+# the bridge does not currently handle dealing with mounts that overlay
91+# already-watched directories.
92+start on mounted
93+
94+stop on runlevel [!2345]
95+
96+expect daemon
97+respawn
98+
99+exec upstart-file-bridge --daemon
100
101=== added file 'extra/man/file-event.7'
102--- extra/man/file-event.7 1970-01-01 00:00:00 +0000
103+++ extra/man/file-event.7 2013-03-15 12:53:28 +0000
104@@ -0,0 +1,99 @@
105+.TH file\-event 7 2013-03-11 upstart
106+.\"
107+.SH NAME
108+file \- event signalling that a file has changed
109+.\"
110+.SH SYNOPSIS
111+.B file
112+.BI FPATH\fR= PATH
113+.BI FEVENT\fR= TYPE
114+.IB \fR[ FMATCH\fR= PATH \fR]
115+.\"
116+.SH DESCRIPTION
117+
118+The
119+.B file
120+event is generated by the
121+.BR upstart\-file\-bridge (8)
122+daemon when a file whose details match the
123+file event condition and environment specified in a jobs
124+.B start on
125+or
126+.B stop on
127+stanza is modified.
128+
129+The
130+.BR FPATH " and " FEVENT
131+environment variables will be set to the same values as specified by the
132+job. Note that if the job did not specify
133+.B FEVENT
134+this will still be set to one of
135+.BR create ", "
136+.BR modify " or "
137+.B delete
138+depending on what type of file event caused the event to be emitted.
139+
140+The
141+.B FEVENT
142+environment variable will be set to the value of the corresponding
143+
144+If the job specified a glob pattern in the file part of the
145+.B FPATH
146+environment variable, the event will contain the
147+.B FMATCH
148+environment variable which will be set to the full path of the file that
149+matched the pattern in
150+.BR FPATH "."
151+.\"
152+.SH NOTES
153+
154+.IP \(bu 4
155+When specifying a path that contains spaces, ensure that the path is
156+quoted.
157+.\"
158+.IP \(bu
159+.B FPATH
160+values specified by jobs are not canonicalised; they cannot be reliably as
161+they may not exist so cannot be fully resolved.
162+.\"
163+.IP \(bu
164+If you wish to match on
165+.BR FMATCH ", "
166+ensure that
167+.B FPATH
168+does not contain multiple consecutive runs of slashes since otherwise
169+your job will find it difficult to perform such a match.
170+.\"
171+.SH EXAMPLES
172+.\"
173+.IP "start on file FPATH=/run/app.pid FEVENT=created" 0.4i
174+Event emitted when file is created.
175+.IP "start on file FPATH=/run/app.pid"
176+Event emitted when file is created, modified or deleted.
177+.IP "start on file FPATH=/var/log/"
178+Event emitted when files within a directory are created, modified or
179+deleted.
180+.IP "start on file FPATH=/var/crash/*.crash FEVENT=created"
181+Event emitted when files in a directory matching a glob pattern are
182+created.
183+.IP "start on file FPATH=""/this/path/contains whitespace.txt"""
184+Specify a file that contains a space character.
185+.\"
186+.SH AUTHOR
187+Written by James Hunt
188+.RB < james.hunt@canonical.com >
189+.\"
190+.SH BUGS
191+Report bugs at
192+.RB < https://launchpad.net/upstart/+bugs >
193+.\"
194+.SH COPYRIGHT
195+Copyright \(co 2013 Canonical Ltd.
196+.PP
197+This is free software; see the source for copying conditions. There is NO
198+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
199+.\"
200+.SH SEE ALSO
201+.BR init (5)
202+.BR init (8)
203+.BR upstart\-file\-bridge (8)
204
205=== added file 'extra/man/upstart-file-bridge.8'
206--- extra/man/upstart-file-bridge.8 1970-01-01 00:00:00 +0000
207+++ extra/man/upstart-file-bridge.8 2013-03-15 12:53:28 +0000
208@@ -0,0 +1,221 @@
209+.TH upstart\-file\-bridge 8 2013-03-11 upstart
210+.\"
211+.SH NAME
212+upstart\-file\-bridge \- Bridge between Upstart and inotify
213+.\"
214+.SH SYNOPSIS
215+.B upstart\-file\-bridge
216+.RI [ OPTIONS ]...
217+.\"
218+.SH DESCRIPTION
219+.B upstart\-file\-bridge
220+receives information about kernel file events that
221+.BR inotify (7)
222+has received and creates
223+.BR init (8)
224+events for them.
225+
226+Supported events exposed to Upstart allow jobs to detect creation,
227+modification and deletion. See
228+.BR file\-event (7)
229+for further details.
230+
231+The bridge works by querying the
232+.BR init (8)
233+daemon at bridge startup time to determine a list of all jobs whose
234+.B start on
235+or
236+.B stop on
237+conditions reference the
238+.B file
239+event. Further, the bridge arranges to be notified automatically when
240+new jobs are created such that any subsequent jobs which reference the
241+.B file
242+event are also handled (as are job deletions).
243+
244+See \fBinotify\fP(7) and for further details.
245+
246+.\"
247+.SH OPTIONS
248+.\"
249+.TP
250+.B \-\-daemon
251+Detach and run in the background.
252+.\"
253+.TP
254+.B \-\-debug
255+Enable debugging output.
256+.\"
257+.TP
258+.B \-\-help
259+Show brief usage summary.
260+.\"
261+.TP
262+.B \-\-user
263+User-session mode: connect to Upstart via the user session rather than
264+over the D\-Bus system bus.
265+.\"
266+.TP
267+.B \-\-verbose
268+Enable verbose output.
269+.\"
270+.SH JOB ENVIRONMENT VARIABLES
271+.TP
272+.B FPATH
273+Path to file to watch. When run without
274+.BR \-\-user ","
275+this must be an absolute path. If
276+.BR \-\-user
277+is specified, certain relative path types are supported:
278+.RS
279+.IP \[bu] 2
280+If the path begins with \(aq~/\(aq, the value will be expanded as would
281+be performed by a shell for matching purposes (although when the event
282+is emitted, the original value will be used).
283+.\"
284+.IP \[bu]
285+If the path begins with \(aq./\(aq, \(aq.\(aq will be assumed to be the
286+users home directory.
287+.\"
288+.IP \[bu]
289+In all other scenarios, if the path does not begin with \(aq/\(aq, it
290+will be assumed to represent a file below the users home directory.
291+.P
292+If the path ends with a slash, it is considered to be a directory which
293+changes the match behaviour (see below).
294+.P
295+If the path is not a directory, the file (but not the directory) portion
296+of the path may contain wildcard matches. See
297+.BR fnmatch (3)
298+and
299+.BR glob (7)
300+for further details.
301+.RE
302+.\"
303+.TP
304+.B FEVENT
305+Event relating to
306+.B FPATH
307+that job is interested in. If this variable is specified the value must
308+be set to
309+.BR create ", "
310+.BR modify " or "
311+.B delete
312+depending on what type of file event the job is interested in. If
313+.B FPATH
314+is not specified, the bridge will watch for all types and set this
315+variable to the appropriate value when emitting the event.
316+.\"
317+.SH WATCH TYPE BEHAVIOUR
318+
319+The bridge emits events depending not only on the value of
320+.BR FEVENT ", "
321+but also on the entity specified by
322+.BR FPATH ":"
323+.\"
324+.SS File
325+
326+An event will be emitted when the named file is created, modified or
327+deleted depending on the value of \fBFEVENT\fR. If
328+.B FEVENT
329+is not specified, react to creation, modification and deletion.
330+
331+If the file already exists when the job is registered, and
332+.B FEVENT
333+either specifies
334+.I create
335+or the variable is not specified, the event will be emitted.
336+.\"
337+.SS Directory
338+
339+An event will be emitted when the named directory is created, modified
340+(files within it are created, modified or deleted) or deleted depending
341+on the value of
342+\fBFEVENT\fR. If
343+.B FEVENT
344+is not specified, react to creation, modification and deletion.
345+
346+If the directory already exists when the job is registered, and
347+.B FEVENT
348+either specifies
349+.I create
350+or the variable is not specified, the event will be emitted.
351+.\"
352+.SS Glob
353+
354+One event will be emitted per match when the glob wildcard matches any
355+files in the directory part is created, modified or deleted, depending
356+on the value of
357+\fBFEVENT\fR. If
358+.B FEVENT
359+is not specified, react to creation, modification and deletion.
360+
361+If any matches already exist when the job is registered, and
362+.B FEVENT
363+either specifies
364+.I create
365+or the variable is not specified, events will be emitted.
366+.\"
367+.SH NOTES
368+
369+.IP \(bu 4
370+A single instance of the bridge may be run at the system level, but
371+multiple further instances may be run per user session instance by using
372+the
373+.BR \-\-user "."
374+.IP \(bu
375+All job conditions specifying the
376+.B file
377+event are multi-shot: if the same file event occurs multiple times, the
378+bridge will emit an Upstart event each time.
379+.\"
380+.SH LIMITATIONS
381+
382+.IP \(bu 4
383+Since the bridge currently uses
384+.BR inotify (7) "" ","
385+it is subject to the same limitations; namely that recursive watches
386+cannot be created reliably in all circumstances. As such, pathological
387+scenarios such as deep directory trees being created and then quickly
388+removed
389+.B cannot
390+be handled reliably. The following provides advice to minimise
391+unexpected behaviour:
392+.RS
393+.IP \(bu 4
394+Attempt to only watch for files to be created, modified or deleted
395+in directories that are guaranteed to already exist at the time
396+the job is registered by the bridge.
397+.\"
398+.IP \(bu
399+If the system cannot guarantee that the directory will exist at job
400+registration time, arrange for the directory to be created by an Upstart
401+job before the bridge itself starts.
402+.\"
403+.IP \(bu
404+In user session mode, if a job specifies a file to watch for and that
405+file is created but inaccessible to the user running the bridge, no
406+event will be emitted.
407+.RE
408+.IP \(bu
409+Tilde expansion is only supported for the current user; that is
410+\(aq~otheruser\(aq will not work.
411+.\"
412+.SH AUTHOR
413+Written by James Hunt
414+.RB < james.hunt@canonical.com >
415+.\"
416+.SH BUGS
417+Report bugs at
418+.RB < https://launchpad.net/ubuntu/+source/upstart/+bugs >
419+.\"
420+.SH COPYRIGHT
421+Copyright \(co 2013 Canonical Ltd.
422+.PP
423+This is free software; see the source for copying conditions. There is NO
424+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
425+.SH SEE ALSO
426+.BR init (5)
427+.BR init (8)
428+.BR inotify (7)
429+.BR file-event (7)
430
431=== added file 'extra/upstart-file-bridge.c'
432--- extra/upstart-file-bridge.c 1970-01-01 00:00:00 +0000
433+++ extra/upstart-file-bridge.c 2013-03-15 12:53:28 +0000
434@@ -0,0 +1,1902 @@
435+/* upstart
436+ *
437+ * Copyright © 2013 Canonical Ltd.
438+ * Author: James Hunt <james.hunt@canonical.com>.
439+ *
440+ * This program is free software; you can redistribute it and/or modify
441+ * it under the terms of the GNU General Public License version 2, as
442+ * published by the Free Software Foundation.
443+ *
444+ * This program is distributed in the hope that it will be useful,
445+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
446+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
447+ * GNU General Public License for more details.
448+ *
449+ * You should have received a copy of the GNU General Public License along
450+ * with this program; if not, write to the Free Software Foundation, Inc.,
451+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
452+ */
453+
454+/**
455+ * This is the Upstart File Bridge which allows jobs to react to files
456+ * being created, modified and deleted.
457+ *
458+ * = Design =
459+ *
460+ * This bridge creates inotify watches on the _first existing parent
461+ * directory_ for the file (normal or directory) being watched for. As
462+ * directories are created, the watch is moved to become more specific
463+ * (closer to the actually requested file path) and as directories are
464+ * deleted, the watch is correspondingly changed to a less specific, but
465+ * existing, directory.
466+ *
467+ * This is necessary since:
468+ *
469+ * - It conserves system resources.
470+ *
471+ * There is little point creating 'n' watches on existing files when a
472+ * single watch on the parent directory will suffice.
473+ *
474+ * - It is not possible to create an inotify watch for a non-existent
475+ * entity (*).
476+ *
477+ * - In a sense, it simplifies the design.
478+ *
479+ * Otherwise the bridge would have to put a watch on each existing
480+ * file for modify and delete requests, but watch the parent for
481+ * create requests. And for a combination of requests who share
482+ * a parent directory, it's easier to just watch the parent alone.
483+ *
484+ * = Limitations =
485+ *
486+ * Since inotify is used, this bridge has a number of significant
487+ * limitations:
488+ *
489+ * 1) It cannot be anything but inherently racy.
490+ *
491+ * inotify(7) does not support recursive watches, so in some -- and not
492+ * necessarily pathological -- cases, events may be missed. This is
493+ * unfortunately exacerbated by the design of the bridge which creates
494+ * watches on the parent directory. This takes time, but in the window
495+ * when the watch is being created, files may have been modified
496+ * undetectably.
497+ *
498+ * For example, if the user requests a watch on '/var/log/app/foo.log',
499+ * the following might happen:
500+ *
501+ * (1) Watch is created for existing directory '/var/log/'.
502+ * (2) A process creates '/var/log/app/'.
503+ * (3) The bridge detects this and moves the watch from
504+ * '/var/log/' to '/var/log/app/'.
505+ * (4) Whilst (3) is happening, some process removes '/var/log/app/'.
506+ * (5) The bridge now has an impotent watch on the now-deleted
507+ * '/var/log/app/'.
508+ * (6) The app starts and (re-)creates '/var/log/app/'.
509+ * (7) The app now creates '/var/log/app/foo.log'.
510+ * (8) No event is emitted due to the impotent watch in (5).
511+ *
512+ * The situation is sadly actually worse than this: if a job watches for
513+ * a deep directory, if any one of the directory elements that is
514+ * created gets missed due to a race between the directory creation and
515+ * this bridge creating or moving a watch, the event will not be
516+ * emitted.
517+ *
518+ * = Advice =
519+ *
520+ * - Attempt to only watch for files to be created/modified/deleted
521+ * in directories that are guaranteed to already exist at
522+ * system startup. This avoids the racy behaviour between
523+ * directory creation and inotify watch manipulation.
524+ *
525+ * - If the directory is not guaranteed to exist at system startup,
526+ * create an Upstart job that creates the directory before the bridge
527+ * starts ('start on starting upstart-file-bridge').
528+ *
529+ * = Alternative Approaches =
530+ *
531+ * fanotify is an alternative but again, it is limited:
532+ *
533+ * == Pros ==
534+ *
535+ * + Supports recursive watches.
536+ *
537+ * == Cons ==
538+ *
539+ * - Does not support a file delete event.
540+ *
541+ * - Potentially high system performance impact since _every_ file
542+ * operation on the partition (except delete) is inspected.
543+ *
544+ *----------
545+ *
546+ * (*) - this is half true: inotify alarmingly does allow a watch to be
547+ * created on a non-existent entity, but is impotent - if that entity is
548+ * ever created, no event is received.
549+ **/
550+
551+#ifdef HAVE_CONFIG_H
552+# include <config.h>
553+#endif /* HAVE_CONFIG_H */
554+
555+#include <stdlib.h>
556+#include <string.h>
557+#include <syslog.h>
558+#include <unistd.h>
559+#include <libgen.h>
560+#include <sys/types.h>
561+#include <sys/stat.h>
562+#include <fnmatch.h>
563+#include <glob.h>
564+#include <pwd.h>
565+
566+#include <nih/alloc.h>
567+#include <nih/command.h>
568+#include <nih/error.h>
569+#include <nih/hash.h>
570+#include <nih/io.h>
571+#include <nih/list.h>
572+#include <nih/logging.h>
573+#include <nih/macros.h>
574+#include <nih/main.h>
575+#include <nih/option.h>
576+#include <nih/string.h>
577+#include <nih/test.h>
578+#include <nih/timer.h>
579+#include <nih/watch.h>
580+
581+#include <nih-dbus/dbus_connection.h>
582+#include <nih-dbus/dbus_proxy.h>
583+
584+#include "dbus/upstart.h"
585+#include "com.ubuntu.Upstart.h"
586+#include "com.ubuntu.Upstart.Job.h"
587+
588+/**
589+ * FILE_EVENT:
590+ *
591+ * Name of event this program handles
592+ **/
593+#define FILE_EVENT "file"
594+
595+/**
596+ * ALL_FILE_EVENTS:
597+ *
598+ * All the inotify file events we care about.
599+ **/
600+#define ALL_FILE_EVENTS (IN_CREATE|IN_MODIFY|IN_CLOSE_WRITE|IN_DELETE)
601+
602+/**
603+ * GLOB_CHARS:
604+ *
605+ * Wildcard characters recognised by glob(3) and fnmatch(3).
606+ **/
607+#define GLOB_CHARS "*?[]"
608+
609+/**
610+ * original_path:
611+ *
612+ * @file: WatchedFile:
613+ *
614+ * Obtain the appropriate WatchedFile path: either the original if the
615+ * path underwent expansion, else the initial unexpanded path.
616+ *
617+ * Required for emitting events since jobs need the unexpanded path to
618+ * allow their start/stop condition to match even if the path has
619+ * subsequently been expanded by this bridge.
620+ **/
621+#define original_path(file) \
622+ (file->original ? file->original : file->path)
623+
624+/**
625+ * skip_slashes:
626+ *
627+ * @path: pointer to path string.
628+ *
629+ * Increment @path to skip over multiple leading slashes.
630+ **/
631+#define skip_slashes(path) \
632+ while (*(path) == '/' && (path)+1 && *((path)+1) == '/') \
633+ (path)++
634+
635+/**
636+ * Job:
637+ *
638+ * @entry: list header,
639+ * @path: D-Bus path of Upstart job,
640+ * @files: list of pointers to WatchedFile files Job will watch.
641+ *
642+ * Structure we use for tracking Upstart jobs.
643+ **/
644+typedef struct job {
645+ NihList entry;
646+ char *path;
647+ NihList files;
648+} Job;
649+
650+/**
651+ * WatchedDir:
652+ *
653+ * @entry: list header,
654+ * @path: full path of directory being watched,
655+ * @files: hash of WatchedFile objects representing all files
656+ * watched in directory @path and sub-directories,
657+ * @watch: watch object.
658+ *
659+ * Every watched file is handled by watching the first parent
660+ * directory that currently exists. This allows use to:
661+ *
662+ * - minimise watch descriptors
663+ * - easily handle the case where a job wants to watch for a file being
664+ * created when that file doesn't yet exist (*).
665+ *
666+ * The drawback to this strategy is the complexity of handling watched
667+ * files and directories when files are created and deleted.
668+ *
669+ * Note that the WatchedFiles in @files are not necessarily _immediate_
670+ * children of @path, but they are children.
671+ *
672+ * (*) Irritatingly, inotify _does_ allow for a watch on a
673+ * non-existing file to be created, but the watch is
674+ * impotent in that when the file _is_ created, no inotify
675+ * event results.
676+ *
677+ **/
678+typedef struct watched_dir {
679+ NihList entry;
680+ char *path;
681+ NihHash *files;
682+ NihWatch *watch;
683+} WatchedDir;
684+
685+/**
686+ * WatchedFile:
687+ *
688+ * @entry: list header,
689+ * @path: full path to file being watched (or a glob),
690+ * @original: original (relative) path as specified by job
691+ * (or NULL if path expansion was not necessary),
692+ * @glob: glob file pattern (or NULL if globbing disabled),
693+ * @dir: TRUE if @path is a directory,
694+ * @events: mask of inotify events file is interested in,
695+ * @parent: parent who is watching over us.
696+ *
697+ * Details of the file being watched.
698+ **/
699+typedef struct watched_file {
700+ NihList entry;
701+ char *path;
702+ char *original;
703+ char *glob;
704+ int dir;
705+ uint32_t events;
706+ WatchedDir *parent;
707+} WatchedFile;
708+
709+/**
710+ * FileEvent:
711+ *
712+ * @entry: list header,
713+ * @path: full path to file being watched,
714+ * @event: event to emit,
715+ * @match: optional file match if @path is a directory or glob.
716+ *
717+ * Details of the event to be emitted.
718+ **/
719+typedef struct file_event {
720+ NihList entry;
721+ char *path;
722+ uint32_t event;
723+ char *match;
724+} FileEvent;
725+
726+/* Prototypes for static functions */
727+static WatchedDir *watched_dir_new (const char *path, const struct stat *statbuf)
728+ __attribute__ ((warn_unused_result));
729+
730+static WatchedFile *watched_file_new (const char *path,
731+ const char *original,
732+ uint32_t events,
733+ const char *glob)
734+ __attribute__ ((warn_unused_result));
735+
736+static Job *job_new (const char *class_path)
737+ __attribute__ ((warn_unused_result));
738+
739+static int file_filter (WatchedDir *dir, const char *path, int is_dir);
740+
741+static void create_handler (WatchedDir *dir, NihWatch *watch,
742+ const char *path, struct stat *statbuf);
743+
744+static void modify_handler (WatchedDir *dir, NihWatch *watch,
745+ const char *path, struct stat *statbuf);
746+
747+static void delete_handler (WatchedDir *dir, NihWatch *watch,
748+ const char *path);
749+
750+static void upstart_job_added (void *data, NihDBusMessage *message,
751+ const char *job_path);
752+
753+static void upstart_job_removed (void *data, NihDBusMessage *message,
754+ const char *job_path);
755+
756+static void job_add_file (Job *job, char **file_info);
757+
758+static void emit_event_error (void *data, NihDBusMessage *message);
759+static int emit_event (const char *path, uint32_t event_type,
760+ const char *match);
761+
762+static FileEvent *file_event_new (void *parent, const char *path,
763+ uint32_t event, const char *match);
764+
765+static void upstart_disconnected (DBusConnection *connection);
766+
767+static void handle_event (NihHash *handled, const char *path,
768+ uint32_t event, const char *match);
769+
770+static int job_destroy (Job *job);
771+
772+static char * find_first_parent (const char *path)
773+ __attribute__ ((warn_unused_result));
774+
775+void watched_dir_init (void);
776+
777+static void ensure_watched (Job *job, WatchedFile *file);
778+
779+static int string_match (const char *a, const char *b)
780+ __attribute__ ((warn_unused_result));
781+
782+char * expand_path (const void *parent, const char *path)
783+ __attribute__ ((warn_unused_result));
784+
785+static int path_valid (const char *path)
786+ __attribute__ ((warn_unused_result));
787+
788+/**
789+ * daemonise:
790+ *
791+ * Set to TRUE if we should become a daemon, rather than just running
792+ * in the foreground.
793+ **/
794+static int daemonise = FALSE;
795+
796+/**
797+ * jobs:
798+ *
799+ * Hash of Upstart jobs that we're monitoring.
800+ **/
801+static NihHash *jobs = NULL;
802+
803+/**
804+ * watched_dirs:
805+ *
806+ * Hash of WatchedDir objects representing the minimum set of existing
807+ * parent directories that allow all WatchedFiles to be watched for.
808+ **/
809+static NihHash *watched_dirs = NULL;
810+
811+/**
812+ * upstart:
813+ *
814+ * Proxy to Upstart daemon.
815+ **/
816+static NihDBusProxy *upstart = NULL;
817+
818+/**
819+ * user:
820+ *
821+ * If TRUE, run in User Session mode connecting to the Session Init
822+ * rather than PID 1. In this mode, certain relative paths are also
823+ * expanded.
824+ **/
825+static int user = FALSE;
826+
827+/**
828+ * home_dir:
829+ *
830+ * Full path to home directory.
831+ **/
832+char home_dir[PATH_MAX];
833+
834+/**
835+ * options:
836+ *
837+ * Command-line options accepted by this program.
838+ **/
839+static NihOption options[] = {
840+ { 0, "daemon", N_("Detach and run in the background"),
841+ NULL, NULL, &daemonise, NULL },
842+ { 0, "user", N_("Connect to user session"),
843+ NULL, NULL, &user, NULL },
844+
845+ NIH_OPTION_LAST
846+};
847+
848+
849+int
850+main (int argc,
851+ char *argv[])
852+{
853+ char **args;
854+ DBusConnection *connection;
855+ char **job_class_paths;
856+ int ret;
857+ char *user_session_addr = NULL;
858+
859+ nih_main_init (argv[0]);
860+
861+ nih_option_set_synopsis (_("Bridge inotify events into upstart"));
862+ nih_option_set_help (
863+ _("By default, upstart-inotify-bridge does not detach from the "
864+ "console and remains in the foreground. Use the --daemon "
865+ "option to have it detach."));
866+
867+ args = nih_option_parser (NULL, argc, argv, options, FALSE);
868+ if (! args)
869+ exit (EXIT_FAILURE);
870+
871+ if (user) {
872+ struct passwd *pw;
873+ user_session_addr = getenv ("UPSTART_SESSION");
874+ if (! user_session_addr) {
875+ nih_fatal (_("UPSTART_SESSION isn't set in environment"));
876+ exit (EXIT_FAILURE);
877+ }
878+
879+ pw = getpwuid (getuid ());
880+
881+ if (! pw) {
882+ nih_error ("Failed to get password entry");
883+ exit (EXIT_FAILURE);
884+ }
885+
886+ nih_assert (pw->pw_dir);
887+
888+ strcpy (home_dir, (pw->pw_dir));
889+ }
890+
891+ /* Allocate jobs hash table */
892+ jobs = NIH_MUST (nih_hash_string_new (NULL, 0));
893+
894+ /* Initialise the connection to Upstart */
895+ connection = NIH_SHOULD (nih_dbus_connect (user
896+ ? user_session_addr
897+ : DBUS_ADDRESS_UPSTART,
898+ upstart_disconnected));
899+ if (! connection) {
900+ NihError *err;
901+
902+ err = nih_error_get ();
903+ nih_fatal ("%s: %s", _("Could not connect to Upstart"),
904+ err->message);
905+ nih_free (err);
906+
907+ exit (EXIT_FAILURE);
908+ }
909+
910+ upstart = NIH_SHOULD (nih_dbus_proxy_new (NULL, connection,
911+ NULL, DBUS_PATH_UPSTART,
912+ NULL, NULL));
913+ if (! upstart) {
914+ NihError *err;
915+
916+ err = nih_error_get ();
917+ nih_fatal ("%s: %s", _("Could not create Upstart proxy"),
918+ err->message);
919+ nih_free (err);
920+
921+ exit (EXIT_FAILURE);
922+ }
923+
924+ /* Connect signals to be notified when jobs come and go */
925+ if (! nih_dbus_proxy_connect (upstart, &upstart_com_ubuntu_Upstart0_6, "JobAdded",
926+ (NihDBusSignalHandler)upstart_job_added, NULL)) {
927+ NihError *err;
928+
929+ err = nih_error_get ();
930+ nih_fatal ("%s: %s", _("Could not create JobAdded signal connection"),
931+ err->message);
932+ nih_free (err);
933+
934+ exit (EXIT_FAILURE);
935+ }
936+
937+ if (! nih_dbus_proxy_connect (upstart, &upstart_com_ubuntu_Upstart0_6, "JobRemoved",
938+ (NihDBusSignalHandler)upstart_job_removed, NULL)) {
939+ NihError *err;
940+
941+ err = nih_error_get ();
942+ nih_fatal ("%s: %s", _("Could not create JobRemoved signal connection"),
943+ err->message);
944+ nih_free (err);
945+
946+ exit (EXIT_FAILURE);
947+ }
948+
949+ /* Request a list of all current jobs */
950+ if (upstart_get_all_jobs_sync (NULL, upstart, &job_class_paths) < 0) {
951+ NihError *err;
952+
953+ err = nih_error_get ();
954+ nih_fatal ("%s: %s", _("Could not obtain job list"),
955+ err->message);
956+ nih_free (err);
957+
958+ exit (EXIT_FAILURE);
959+ }
960+
961+ /* Look for jobs that specify the FILE_EVENT event and handle
962+ * them.
963+ */
964+ for (char **job_class_path = job_class_paths;
965+ job_class_path && *job_class_path; job_class_path++) {
966+ upstart_job_added (NULL, NULL, *job_class_path);
967+ }
968+
969+ nih_free (job_class_paths);
970+
971+ /* Become daemon */
972+ if (daemonise) {
973+ if (nih_main_daemonise () < 0) {
974+ NihError *err;
975+
976+ err = nih_error_get ();
977+ nih_fatal ("%s: %s", _("Unable to become daemon"),
978+ err->message);
979+ nih_free (err);
980+
981+ exit (EXIT_FAILURE);
982+ }
983+
984+ /* Send all logging output to syslog */
985+ openlog (program_name, LOG_PID, LOG_DAEMON);
986+ nih_log_set_logger (nih_logger_syslog);
987+ }
988+
989+ if (user) {
990+ /* Ensure we are sitting in $HOME so relative FPATH
991+ * values work as expected.
992+ */
993+ if (chdir (home_dir) < 0) {
994+ nih_error ("Failed to change working directory");
995+ exit (EXIT_FAILURE);
996+ }
997+ }
998+
999+ /* Handle TERM and INT signals gracefully */
1000+ nih_signal_set_handler (SIGTERM, nih_signal_handler);
1001+ NIH_MUST (nih_signal_add_handler (NULL, SIGTERM, nih_main_term_signal, NULL));
1002+
1003+ if (! daemonise) {
1004+ nih_signal_set_handler (SIGINT, nih_signal_handler);
1005+ NIH_MUST (nih_signal_add_handler (NULL, SIGINT, nih_main_term_signal, NULL));
1006+ }
1007+
1008+ ret = nih_main_loop ();
1009+
1010+ return ret;
1011+}
1012+
1013+/**
1014+ * upstart_job_added:
1015+ *
1016+ * @data: (unused),
1017+ * @message: Nih D-Bus message (unused),
1018+ * @job_path: Upstart job class (D-Bus) path associated with job.
1019+ *
1020+ * Called automatically when a new Upstart job appears on D-Bus ("JobAdded" signal).
1021+ **/
1022+static void
1023+upstart_job_added (void *data,
1024+ NihDBusMessage *message,
1025+ const char *job_path)
1026+{
1027+ Job *job;
1028+ nih_local NihDBusProxy *job_class = NULL;
1029+ nih_local char ***start_on = NULL;
1030+ nih_local char ***stop_on = NULL;
1031+
1032+ nih_assert (job_path);
1033+
1034+ /* Obtain a proxy to the job */
1035+ job_class = nih_dbus_proxy_new (NULL, upstart->connection,
1036+ upstart->name, job_path,
1037+ NULL, NULL);
1038+ if (! job_class) {
1039+ NihError *err;
1040+
1041+ err = nih_error_get ();
1042+ nih_error ("Could not create proxy for job %s: %s",
1043+ job_path, err->message);
1044+ nih_free (err);
1045+
1046+ return;
1047+ }
1048+
1049+ job_class->auto_start = FALSE;
1050+
1051+ /* Obtain the start_on and stop_on properties of the job */
1052+ if (job_class_get_start_on_sync (NULL, job_class, &start_on) < 0) {
1053+ NihError *err;
1054+
1055+ err = nih_error_get ();
1056+ nih_error ("Could not obtain job start condition %s: %s",
1057+ job_path, err->message);
1058+ nih_free (err);
1059+
1060+ return;
1061+ }
1062+
1063+ if (job_class_get_stop_on_sync (NULL, job_class, &stop_on) < 0) {
1064+ NihError *err;
1065+
1066+ err = nih_error_get ();
1067+ nih_error ("Could not obtain job stop condition %s: %s",
1068+ job_path, err->message);
1069+ nih_free (err);
1070+
1071+ return;
1072+ }
1073+
1074+ /* Free any existing record for the job (should never happen,
1075+ * but worth being safe).
1076+ */
1077+ job = (Job *)nih_hash_lookup (jobs, job_path);
1078+ if (job)
1079+ nih_free (job);
1080+
1081+ /* Create new record for the job */
1082+ job = job_new (job_path);
1083+ if (! job) {
1084+ nih_error ("%s %s",
1085+ _("Failed to create job"), job_path);
1086+ return;
1087+ }
1088+
1089+ /* Find out whether this job listens for any FILE_EVENT events */
1090+ for (char ***event = start_on; event && *event && **event; event++) {
1091+ if (! strcmp (**event, FILE_EVENT))
1092+ job_add_file (job, *event);
1093+ }
1094+
1095+ for (char ***event = stop_on; event && *event && **event; event++)
1096+ if (! strcmp (**event, FILE_EVENT))
1097+ job_add_file (job, *event);
1098+
1099+ /* If we didn't end up with any files, free the job and move on */
1100+ if (NIH_LIST_EMPTY (&job->files)) {
1101+ nih_free (job);
1102+ return;
1103+ }
1104+
1105+ nih_message ("Job got added %s", job_path);
1106+}
1107+
1108+/**
1109+ * upstart_job_removed:
1110+ *
1111+ * @data: (unused),
1112+ * @message: Nih D-Bus message (unused),
1113+ * @job_path: Upstart job class (D-Bus) path associated with job.
1114+ *
1115+ * Called automatically when an Upstart job disappears from D-Bus
1116+ * ("JobRemoved" signal).
1117+ *
1118+ **/
1119+static void
1120+upstart_job_removed (void *data,
1121+ NihDBusMessage *message,
1122+ const char *job_path)
1123+{
1124+ Job *job;
1125+
1126+ nih_assert (job_path);
1127+
1128+ job = (Job *)nih_hash_lookup (jobs, job_path);
1129+
1130+ if (! job)
1131+ return;
1132+
1133+ nih_message ("Job went away %s", job_path);
1134+
1135+ nih_free (job);
1136+}
1137+
1138+
1139+/**
1140+ * job_add_file:
1141+ *
1142+ * @job: Job,
1143+ * @file_info: environment variables Upstart job has specified
1144+ * relating to FILE_EVENT.
1145+ *
1146+ * Create a WatchedFile object based on @file_info and ensure that
1147+ * WatchedFile file (or glob) is watched.
1148+ **/
1149+static void
1150+job_add_file (Job *job,
1151+ char **file_info)
1152+{
1153+ uint32_t events;
1154+ WatchedFile *file = NULL;
1155+ nih_local char *error = NULL;
1156+ nih_local char *glob_expr = NULL;
1157+ nih_local char *expanded = NULL;
1158+ char path[PATH_MAX];
1159+
1160+ nih_assert (job);
1161+ nih_assert (job->path);
1162+ nih_assert (file_info);
1163+ nih_assert (! strcmp (file_info[0], FILE_EVENT));
1164+
1165+ memset (path, '\0', sizeof (path));
1166+
1167+ for (char **env = file_info + 1; env && *env; env++) {
1168+ char *val;
1169+ size_t name_len;
1170+
1171+ val = strchr (*env, '=');
1172+ if (! val) {
1173+ nih_warn ("%s: Ignored %s event without variable name",
1174+ job->path, FILE_EVENT);
1175+ goto error;
1176+ }
1177+
1178+ name_len = val - *env;
1179+ val++;
1180+
1181+ if (! strncmp (*env, "FPATH", name_len)) {
1182+ char dirpart[PATH_MAX];
1183+ char basepart[PATH_MAX];
1184+ char *dir;
1185+ char *base;
1186+ size_t len2;
1187+
1188+ strcpy (path, val);
1189+
1190+ if (user && path[0] != '/') {
1191+ expanded = expand_path (NULL, path);
1192+ if (! expanded) {
1193+ nih_error ("Failed to expand path");
1194+ goto error;
1195+ }
1196+ }
1197+
1198+ if (! path_valid (path))
1199+ goto error;
1200+
1201+ strcpy (dirpart, path);
1202+ dir = dirname (dirpart);
1203+
1204+ /* See dirname(3) */
1205+ nih_assert (*dir != '.');
1206+
1207+ len2 = strlen (dir);
1208+
1209+ if (strcspn (dir, GLOB_CHARS) < len2) {
1210+ nih_warn ("%s: %s", job->path, _("Directory globbing not supported"));
1211+ goto error;
1212+ }
1213+
1214+ strcpy (basepart, path);
1215+ base = basename (basepart);
1216+
1217+ /* See dirname(3) */
1218+ nih_assert (strcmp (base, basepart));
1219+
1220+ len2 = strlen (base);
1221+
1222+ if (strcspn (base, GLOB_CHARS) < len2) {
1223+ strcpy (path, dir);
1224+ glob_expr = NIH_MUST (nih_strdup (NULL, base));
1225+ }
1226+ } else if (! strncmp (*env, "FEVENT", name_len)) {
1227+ if (! strcmp (val, "create")) {
1228+ events = IN_CREATE;
1229+ } else if (! strcmp (val, "modify")) {
1230+ events = (IN_MODIFY|IN_CLOSE_WRITE);
1231+ } else if (! strcmp (val, "delete")) {
1232+ events |= IN_DELETE;
1233+ }
1234+ }
1235+ }
1236+
1237+ if (! *path)
1238+ goto error;
1239+
1240+ if (! events)
1241+ events = ALL_FILE_EVENTS;
1242+
1243+ file = watched_file_new (expanded ? expanded : path,
1244+ expanded ? path : NULL,
1245+ events, glob_expr);
1246+
1247+ if (! file) {
1248+ nih_warn ("%s: %s",
1249+ _("Failed to add new file"), path);
1250+ goto error;
1251+ }
1252+
1253+ /* If the job cares about the file or directory existing and it
1254+ * _already_ exists, emit the event.
1255+ *
1256+ * Although technically fraudulent (the file might not have _just
1257+ * been created_ - it may have existed forever), it is necessary
1258+ * since otherwise jobs will hang around wating for the file to
1259+ * be 'freshly-created'. However, although nih_watch_new() has
1260+ * been told to run the create handler for pre-existing files
1261+ * that doesn't help as we don't watch the files, we watch
1262+ * their first existing parent directory.
1263+ **/
1264+ if ((file->events & IN_CREATE)) {
1265+ struct stat statbuf;
1266+
1267+ if (glob_expr) {
1268+ glob_t globbuf;
1269+ char pattern[PATH_MAX];
1270+
1271+ sprintf (pattern, "%s/%s",
1272+ expanded ? expanded : path, glob_expr);
1273+
1274+ if (! glob (pattern, 0, NULL, &globbuf)) {
1275+ size_t i;
1276+ char **results;
1277+
1278+ results = globbuf.gl_pathv;
1279+
1280+ /* emit one event per matching file */
1281+ for (i = 0; i < globbuf.gl_pathc; i++) {
1282+ emit_event (pattern, IN_CREATE, results[i]);
1283+ }
1284+ }
1285+
1286+ globfree (&globbuf);
1287+ } else {
1288+ if (! stat (file->path, &statbuf))
1289+ emit_event (file->path, IN_CREATE, NULL);
1290+ }
1291+ }
1292+
1293+ ensure_watched (job, file);
1294+
1295+ return;
1296+
1297+error:
1298+ if (file)
1299+ nih_free (file);
1300+}
1301+
1302+/**
1303+ * file_filter:
1304+ *
1305+ * @dir: WatchedDir,
1306+ * @path: full path to file to consider,
1307+ * @is_dir: TRUE if @path is a directory, else FALSE.
1308+ *
1309+ * Watch handler function to sift the wheat from the chaff.
1310+ *
1311+ * Returns: TRUE if @path should be ignored, FALSE otherwise.
1312+ **/
1313+int
1314+file_filter (WatchedDir *dir,
1315+ const char *path,
1316+ int is_dir)
1317+{
1318+ nih_assert (dir);
1319+ nih_assert (path);
1320+
1321+ skip_slashes (path);
1322+
1323+ NIH_HASH_FOREACH_SAFE (dir->files, iter) {
1324+ WatchedFile *file = (WatchedFile *)iter;
1325+
1326+ if (strstr (file->path, path) == file->path) {
1327+ /* Either an exact match or path is a child of the watched file.
1328+ * Paths in the latter category will be inspected more closely by
1329+ * the handlers.
1330+ */
1331+ return FALSE;
1332+ } else if ((file->dir || file->glob) && strstr (path, file->path) == path) {
1333+ return FALSE;
1334+ }
1335+ }
1336+
1337+ return TRUE;
1338+}
1339+
1340+/**
1341+ * create_handler:
1342+ *
1343+ * @dir: WatchedDir,
1344+ * @watch: NihWatch for directory tree,
1345+ * @path: full path to file,
1346+ * @statbuf: stat of @path.
1347+ *
1348+ * Watch handler function called when a WatchedFile is created in @dir.
1349+ **/
1350+void
1351+create_handler (WatchedDir *dir,
1352+ NihWatch *watch,
1353+ const char *path,
1354+ struct stat *statbuf)
1355+{
1356+ WatchedDir *new_dir;
1357+ char *p;
1358+ int add_dir = FALSE;
1359+ int empty;
1360+
1361+ /* Hash of events already emitted (required to avoid sending
1362+ * same event multiple times).
1363+ */
1364+ nih_local NihHash *handled = NULL;
1365+
1366+ /* List of existing WatchedFiles that need to be added against
1367+ * @path (since @path either exactly matches their path, or
1368+ * @path is more specific ancestor of their path).
1369+ */
1370+ NihList entries;
1371+
1372+ nih_assert (dir);
1373+ nih_assert (watch);
1374+ nih_assert (path);
1375+ nih_assert (statbuf);
1376+
1377+ skip_slashes (path);
1378+
1379+ /* path should be a file below the WatchedDir */
1380+ nih_assert (strstr (path, dir->path) == path);
1381+
1382+ nih_list_init (&entries);
1383+ handled = NIH_MUST (nih_hash_string_new (NULL, 0));
1384+
1385+ NIH_HASH_FOREACH_SAFE (dir->files, iter) {
1386+ WatchedFile *file = (WatchedFile *)iter;
1387+
1388+ if (file->dir) {
1389+ if (! strcmp (file->path, dir->path)) {
1390+ /* Watch is on the directory itself and a file within that
1391+ * watched directory was created, hence emit the _directory_
1392+ * was modified.
1393+ */
1394+ if (file->events & IN_MODIFY)
1395+ handle_event (handled, file->path, IN_MODIFY, path);
1396+ } else if (! strcmp (file->path, path)) {
1397+ /* Directory has been created */
1398+ handle_event (handled, file->path, IN_CREATE, NULL);
1399+ add_dir = TRUE;
1400+ nih_list_add (&entries, &file->entry);
1401+ }
1402+ } else if (file->glob) {
1403+ char full_path[PATH_MAX];
1404+
1405+ /* reconstruct the full path */
1406+ strcpy (full_path, file->path);
1407+ strcat (full_path, "/");
1408+ strcat (full_path, file->glob);
1409+
1410+ if (! fnmatch (full_path, path, FNM_PATHNAME) && (file->events & IN_CREATE))
1411+ handle_event (handled, full_path, IN_CREATE, path);
1412+ } else {
1413+ if (! strcmp (file->path, path) && (file->events & IN_CREATE)) {
1414+ /* exact match, so emit event */
1415+ handle_event (handled, file->path, IN_CREATE, NULL);
1416+
1417+ } else if ((p=strstr (file->path, path)) && p == file->path
1418+ && S_ISDIR (statbuf->st_mode)) {
1419+ /* The created file is actually a directory
1420+ * more specific that the current watch
1421+ * directory associated with @file.
1422+ *
1423+ * As such, we can make the watch on @file more
1424+ * specific by dropping the old watch, creating
1425+ * a new WatchedDir for @path and adding @file
1426+ * to the new WatchedDir's files hash.
1427+ *
1428+ * This has to be handled carefully due to NIH
1429+ * list/hash handling constraints. First, the
1430+ * new directory is marked as needing to be
1431+ * added to the directory hash and secondly we
1432+ * add the WatchedFile to a list representing
1433+ * all WatchedFiles that need to be added for
1434+ * the new path.
1435+ */
1436+ add_dir = TRUE;
1437+ nih_list_add (&entries, &file->entry);
1438+ }
1439+ }
1440+ }
1441+
1442+ if (! add_dir)
1443+ return;
1444+
1445+ /* we should have atleast 1 file to add to the new watch */
1446+ nih_assert (! NIH_LIST_EMPTY (&entries));
1447+
1448+ new_dir = watched_dir_new (path, statbuf);
1449+ if (! new_dir) {
1450+ nih_warn ("%s: %s",
1451+ _("Failed to watch directory"), path);
1452+ return;
1453+ }
1454+
1455+ /* Add all list entries to the newly-created WatchedDir */
1456+ NIH_LIST_FOREACH_SAFE (&entries, iter) {
1457+ WatchedFile *file = (WatchedFile *)iter;
1458+
1459+ nih_hash_add (new_dir->files, &file->entry);
1460+ }
1461+
1462+ empty = TRUE;
1463+ NIH_HASH_FOREACH (dir->files, iter) {
1464+ empty = FALSE;
1465+ break;
1466+ }
1467+
1468+ if (empty) {
1469+ /* Remove the old directory watch */
1470+ nih_free (dir);
1471+ }
1472+}
1473+
1474+/**
1475+ * modify_handler:
1476+ *
1477+ * @dir: WatchedDir,
1478+ * @watch: NihWatch for directory tree,
1479+ * @path: full path to file,
1480+ * @statbuf: stat of @path.
1481+ *
1482+ * Watch handler function called when a WatchedFile is modified in @dir.
1483+ **/
1484+void
1485+modify_handler (WatchedDir *dir,
1486+ NihWatch *watch,
1487+ const char *path,
1488+ struct stat *statbuf)
1489+{
1490+ nih_local NihHash *handled = NULL;
1491+
1492+ nih_assert (dir);
1493+ nih_assert (watch);
1494+ nih_assert (path);
1495+ nih_assert (statbuf);
1496+
1497+ /* path should be a file below the WatchedDir */
1498+ nih_assert (strstr (path, dir->path) == path);
1499+
1500+ skip_slashes (path);
1501+
1502+ handled = NIH_MUST (nih_hash_string_new (NULL, 0));
1503+
1504+ NIH_HASH_FOREACH_SAFE (dir->files, iter) {
1505+ WatchedFile *file = (WatchedFile *)iter;
1506+
1507+ if (! (file->events & IN_MODIFY))
1508+ continue;
1509+
1510+ if (file->dir) {
1511+ if (! strcmp (file->path, dir->path)) {
1512+ /* Watch is on the directory itself and a file within that
1513+ * watched directory was modified, hence emit the _directory_
1514+ * was modified.
1515+ */
1516+ handle_event (handled, original_path (file), IN_MODIFY, path);
1517+ }
1518+ } else if (file->glob) {
1519+ char full_path[PATH_MAX];
1520+
1521+ /* reconstruct the full path */
1522+ strcpy (full_path, file->path);
1523+ strcat (full_path, "/");
1524+ strcat (full_path, file->glob);
1525+ if (! fnmatch (full_path, path, FNM_PATHNAME) && (file->events & IN_MODIFY))
1526+ handle_event (handled, full_path, IN_MODIFY, path);
1527+ } else {
1528+ if (! strcmp (file->path, path)) {
1529+ /* exact match, so emit event */
1530+ handle_event (handled, original_path (file), IN_MODIFY, NULL);
1531+ } else if (file->dir && strstr (path, file->path) == path) {
1532+ /* file in watched directory modified, so emit event */
1533+ handle_event (handled, path, IN_MODIFY, NULL);
1534+ }
1535+ }
1536+ }
1537+}
1538+
1539+/**
1540+ * delete_handler:
1541+ *
1542+ * @dir: WatchedDir,
1543+ * @watch: NihWatch for directory tree,
1544+ * @path: full path to file that was deleted.
1545+ *
1546+ * Watch handler function called when a WatchedFile is deleted in @dir.
1547+ */
1548+void
1549+delete_handler (WatchedDir *dir,
1550+ NihWatch *watch,
1551+ const char *path)
1552+{
1553+ WatchedDir *new_dir;
1554+ char *parent;
1555+ char *p;
1556+ struct stat statbuf;
1557+ int rm_dir = FALSE;
1558+ nih_local NihHash *handled = NULL;
1559+
1560+ /* List of existing WatchedFiles that need to be added against
1561+ * @path (since @path either exactly matches their path, or
1562+ * @path is more specific ancestor of their path).
1563+ */
1564+ NihList entries;
1565+
1566+ nih_assert (dir);
1567+ nih_assert (watch);
1568+ nih_assert (path);
1569+
1570+ /* path should be a file below the WatchedDir */
1571+ nih_assert (strstr (path, dir->path) == path);
1572+
1573+ skip_slashes (path);
1574+
1575+ nih_list_init (&entries);
1576+ handled = NIH_MUST (nih_hash_string_new (NULL, 0));
1577+
1578+ NIH_HASH_FOREACH_SAFE (dir->files, iter) {
1579+ WatchedFile *file = (WatchedFile *)iter;
1580+
1581+ if (file->dir) {
1582+ if (! strcmp (file->path, path)) {
1583+ /* Directory itself was deleted */
1584+ handle_event (handled, original_path (file), IN_DELETE, NULL);
1585+ } else if (! strcmp (file->path, dir->path)) {
1586+ /* Watch is on the directory itself and a file within that
1587+ * watched directory was deleted, hence emit the directory was
1588+ * modified.
1589+ */
1590+ if (file->events & IN_MODIFY)
1591+ handle_event (handled, original_path (file), IN_MODIFY, path);
1592+ }
1593+ } else if (file->glob) {
1594+ char full_path[PATH_MAX];
1595+
1596+ /* reconstruct the full path */
1597+ strcpy (full_path, file->path);
1598+ strcat (full_path, "/");
1599+ strcat (full_path, file->glob);
1600+
1601+ if (! fnmatch (full_path, path, FNM_PATHNAME) && (file->events & IN_DELETE))
1602+ handle_event (handled, full_path, IN_DELETE, path);
1603+ } else {
1604+ if (! strcmp (file->path, path) && (file->events & IN_DELETE)) {
1605+ handle_event (handled, original_path (file), IN_DELETE, NULL);
1606+ } else if ((p=strstr (file->path, path)) && p == file->path) {
1607+ /* Create a new directory watch for all
1608+ * WatchedFiles whose immediate parent directory
1609+ * matches @path (in other words,
1610+ * make the watch looking after a WatchedFile
1611+ * less specific). This has to be handled
1612+ * carefully due to NIH list/hash handling
1613+ * constraints. First, the new directory is
1614+ * marked as needing to be added to the
1615+ * directory hash and secondly we add the
1616+ * WatchedFile to a list representing all
1617+ * WatchedFiles that need to be added for the
1618+ * new path.
1619+ */
1620+ rm_dir = TRUE;
1621+ nih_list_add (&entries, &file->entry);
1622+ } else if (file->dir && strstr (path, file->path) == path && (file->events & IN_DELETE)) {
1623+ /* file in watched directory deleted, so emit event */
1624+ handle_event (handled, path, IN_DELETE, NULL);
1625+ }
1626+ }
1627+ }
1628+
1629+ if (! rm_dir)
1630+ return;
1631+
1632+ /* Remove the old directory watch */
1633+ nih_free (dir);
1634+
1635+ nih_assert (! NIH_LIST_EMPTY (&entries));
1636+
1637+ parent = find_first_parent (dir->path);
1638+ if (! parent) {
1639+ nih_warn ("%s: %s",
1640+ _("Failed to find parent directory"), dir->path);
1641+ return;
1642+ }
1643+
1644+ /* Check to see if there is already an existing watch for the
1645+ * parent.
1646+ */
1647+ new_dir = (WatchedDir *)nih_hash_lookup (watched_dirs, parent);
1648+
1649+ if (! new_dir) {
1650+ if (stat (parent, &statbuf) < 0) {
1651+ nih_warn ("%s: %s",
1652+ _("Failed to stat directory"), parent);
1653+ return;
1654+ }
1655+
1656+ new_dir = watched_dir_new (parent, &statbuf);
1657+ if (! new_dir) {
1658+ nih_warn ("%s: %s",
1659+ _("Failed to watch directory"), parent);
1660+ return;
1661+ }
1662+ }
1663+
1664+ /* Add all list entries to the newly-created WatchedDir. */
1665+ NIH_LIST_FOREACH_SAFE (&entries, iter) {
1666+ WatchedFile *file = (WatchedFile *)iter;
1667+
1668+ nih_hash_add (new_dir->files, &file->entry);
1669+ }
1670+}
1671+
1672+/**
1673+ * upstart_disconnected:
1674+ *
1675+ * @connection: connection to Upstart.
1676+ *
1677+ * Handler called when bridge disconnected from Upstart.
1678+ **/
1679+static void
1680+upstart_disconnected (DBusConnection *connection)
1681+{
1682+ nih_fatal (_("Disconnected from Upstart"));
1683+ nih_main_loop_exit (1);
1684+}
1685+
1686+/**
1687+ * ensure_watched:
1688+ *
1689+ * @job: job,
1690+ * @file: file we want to watch.
1691+ *
1692+ * Ensure that the WatchedFile file specified is watched.
1693+ *
1694+ * For regular files, this is achieved by adding a watch to
1695+ * the first *existing* _parent_ directory encountered and adding
1696+ * that WatchedDir to the watched_dirs hash.
1697+ *
1698+ * For directories, if they do not yet exist, the strategy is as for
1699+ * regular files. If the directories do exist, the watch is placed on
1700+ * the directory itself.
1701+ **/
1702+static void
1703+ensure_watched (Job *job,
1704+ WatchedFile *file)
1705+{
1706+ WatchedDir *dir = NULL;
1707+ nih_local char *path = NULL;
1708+ NihListEntry *entry;
1709+ struct stat statbuf;
1710+
1711+ nih_assert (job);
1712+ nih_assert (file);
1713+
1714+ watched_dir_init ();
1715+
1716+ if (file->dir || file->glob) {
1717+ if (! stat (file->path, &statbuf)) {
1718+ /* Directory already exists, so we can watch it,
1719+ * not its parent as is done for file watches.
1720+ */
1721+ path = file->path;
1722+ goto lookup;
1723+ }
1724+ }
1725+
1726+ path = find_first_parent (file->path);
1727+ if (! path) {
1728+ nih_warn ("%s: %s",
1729+ _("Failed to find parent directory"), file->path);
1730+ return;
1731+ }
1732+
1733+lookup:
1734+ dir = (WatchedDir *)nih_hash_lookup (watched_dirs, path);
1735+ if (! dir) {
1736+ dir = watched_dir_new (path, &statbuf);
1737+ if (! dir)
1738+ return;
1739+ }
1740+
1741+ /* Associate the WatchedFile with the job such that when the job
1742+ * is freed, the corresponding files are removed from their
1743+ * containing WatchedDirs.
1744+ */
1745+ nih_ref (file, job);
1746+
1747+ file->parent = dir;
1748+ nih_hash_add (dir->files, &file->entry);
1749+
1750+ /* Create a link from the job to the WatchedFile.
1751+ */
1752+ entry = NIH_MUST (nih_list_entry_new (job));
1753+ entry->data = file;
1754+ nih_list_add (&job->files, &entry->entry);
1755+}
1756+
1757+/**
1758+ * dir_watched_init:
1759+ *
1760+ * Initialise the watched_dirs hash table.
1761+ **/
1762+void
1763+watched_dir_init (void)
1764+{
1765+ if (! watched_dirs)
1766+ watched_dirs = NIH_MUST (nih_hash_string_new (NULL, 0));
1767+}
1768+
1769+/**
1770+ * emit_event:
1771+ *
1772+ * @path: original path as specified by a registered job,
1773+ * @event_type: inotify event type that occured,
1774+ * @match: file match that resulted from @path if it contains glob
1775+ * wildcards (or NULL).
1776+ *
1777+ * Emit an Upstart event.
1778+ **/
1779+static int
1780+emit_event (const char *path,
1781+ uint32_t event_type,
1782+ const char *match)
1783+{
1784+ DBusPendingCall *pending_call;
1785+ nih_local char **env = NULL;
1786+ nih_local char *var = NULL;
1787+ size_t env_len = 0;
1788+
1789+ nih_assert (path);
1790+ nih_assert (event_type == IN_CREATE ||
1791+ event_type == IN_MODIFY ||
1792+ event_type == IN_DELETE);
1793+
1794+ env = NIH_MUST (nih_str_array_new (NULL));
1795+
1796+ var = NIH_MUST (nih_sprintf (NULL, "FPATH=%s", path));
1797+ NIH_MUST (nih_str_array_addp (&env, NULL, &env_len, var));
1798+
1799+ var = NIH_MUST (nih_sprintf (NULL, "FEVENT=%s",
1800+ event_type == IN_CREATE ? "create" :
1801+ event_type == IN_MODIFY ? "modify" :
1802+ "delete"));
1803+ NIH_MUST (nih_str_array_addp (&env, NULL, &env_len, var));
1804+
1805+ if (match) {
1806+ var = NIH_MUST (nih_sprintf (NULL, "FMATCH=%s", match));
1807+ NIH_MUST (nih_str_array_addp (&env, NULL, &env_len, var));
1808+ }
1809+
1810+ pending_call = NIH_SHOULD (upstart_emit_event (upstart,
1811+ FILE_EVENT, env, FALSE,
1812+ NULL, emit_event_error, NULL,
1813+ NIH_DBUS_TIMEOUT_NEVER));
1814+ if (! pending_call) {
1815+ NihError *err;
1816+
1817+ err = nih_error_get ();
1818+ nih_warn ("%s", err->message);
1819+ nih_free (err);
1820+ return FALSE;
1821+ }
1822+
1823+ return TRUE;
1824+}
1825+
1826+/**
1827+ * emit_event_error:
1828+ *
1829+ * @data: (unused),
1830+ * @message: Nih D-Bus message (unused),
1831+ *
1832+ * Handle failure to emit an event by consuming raised error and
1833+ * displaying its details.
1834+ **/
1835+static void
1836+emit_event_error (void *data,
1837+ NihDBusMessage *message)
1838+{
1839+ NihError *err;
1840+
1841+ err = nih_error_get ();
1842+ nih_warn ("%s", err->message);
1843+ nih_free (err);
1844+}
1845+
1846+/**
1847+ * watched_dir_new:
1848+ *
1849+ * @path: Absolute path to watch.
1850+ * @statbuf: stat of @path.
1851+ *
1852+ * Create a new directory watch object for @path.
1853+ *
1854+ * Returns: WatchedDir or NULL on error.
1855+ **/
1856+static WatchedDir *
1857+watched_dir_new (const char *path,
1858+ const struct stat *statbuf)
1859+{
1860+ char watched_path[PATH_MAX];
1861+ size_t len;
1862+ WatchedDir *dir;
1863+
1864+ nih_assert (path);
1865+ nih_assert (statbuf);
1866+
1867+ /* we shouldn't already be watching this directory */
1868+ nih_assert (! nih_hash_lookup (watched_dirs, path));
1869+
1870+ watched_dir_init ();
1871+
1872+ strcpy (watched_path, path);
1873+ len = strlen (watched_path);
1874+
1875+ if (len > 1 && watched_path[len-1] == '/') {
1876+ /* Better to remove a trailing slash before handing to
1877+ * inotify since although all works as expected, the
1878+ * path handed to inotify also gets given to the
1879+ * create/modify/delete handlers which can then lead to
1880+ * multiple consecutive slashes which could result in
1881+ * jobs failing to start as they would not expect FMATCH
1882+ * to contain such values.
1883+ *
1884+ * Note that we do not (cannot) do this if @path is
1885+ * the root directory.
1886+ */
1887+ watched_path[len-1] = '\0';
1888+ }
1889+
1890+ dir = nih_new (watched_dirs, WatchedDir);
1891+ if (! dir)
1892+ return NULL;
1893+
1894+ nih_list_init (&dir->entry);
1895+
1896+ nih_alloc_set_destructor (dir, nih_list_destroy);
1897+
1898+ dir->path = nih_strdup (dir, path);
1899+ if (! dir->path)
1900+ goto error;
1901+
1902+ dir->files = nih_hash_string_new (dir, 0);
1903+ if (! dir->files)
1904+ goto error;
1905+
1906+ nih_hash_add (watched_dirs, &dir->entry);
1907+
1908+ /* Create a watch on the specified directory.
1909+ *
1910+ * Don't set a recursive watch as there is no need
1911+ * (individual jobs only care about a single directory,
1912+ * and anyway the parent directory may be arbitrarily
1913+ * deep so it could be prohibitively expensive).
1914+ */
1915+ dir->watch = nih_watch_new (dir, watched_path,
1916+ FALSE, TRUE,
1917+ (NihFileFilter)file_filter,
1918+ (NihCreateHandler)create_handler,
1919+ (NihModifyHandler)modify_handler,
1920+ (NihDeleteHandler)delete_handler,
1921+ dir);
1922+ if (! dir->watch) {
1923+ NihError *err;
1924+
1925+ err = nih_error_get ();
1926+ nih_fatal ("%s %s: %s", _("Could not create watch for path"),
1927+ path,
1928+ err->message);
1929+ nih_free (err);
1930+
1931+ goto error;
1932+ }
1933+
1934+ return dir;
1935+
1936+error:
1937+ nih_free (dir);
1938+ return NULL;
1939+}
1940+
1941+/**
1942+ * watched_file_new:
1943+ *
1944+ * @path: full path to file,
1945+ * @original: original (relative) path specified by job,
1946+ * @events: events job wishes to watch for @path,
1947+ * @glob: glob file pattern, or NULL,
1948+ *
1949+ * Create a WatchedFile object representing @path.
1950+ *
1951+ * If path expansion was required, @original must specify the original
1952+ * path as specified by the job else it may be NULL.
1953+ *
1954+ * If @glob is set, @path will be the directory portion of the original
1955+ * path with @glob being the file (or basename) portion.
1956+ *
1957+ * Returns: WatchedFile object, or NULL on insufficient memory.
1958+ **/
1959+static WatchedFile *
1960+watched_file_new (const char *path,
1961+ const char *original,
1962+ uint32_t events,
1963+ const char *glob)
1964+{
1965+ size_t len;
1966+ WatchedFile *file;
1967+
1968+ nih_assert (path);
1969+ nih_assert (events);
1970+
1971+ file = nih_new (NULL, WatchedFile);
1972+ if (! file)
1973+ return NULL;
1974+
1975+ nih_list_init (&file->entry);
1976+
1977+ nih_alloc_set_destructor (file, nih_list_destroy);
1978+
1979+ len = strlen (path);
1980+
1981+ file->dir = (path[len-1] == '/');
1982+
1983+ /* optionally one or the other, but not both */
1984+ if (file->dir || file->glob)
1985+ nih_assert (file->dir || file->glob);
1986+
1987+ file->path = nih_strdup (file, path);
1988+ if (! file->path)
1989+ goto error;
1990+
1991+ file->original = NULL;
1992+ if (original) {
1993+ file->original = nih_strdup (file, original);
1994+ if (! file->original)
1995+ goto error;
1996+ }
1997+
1998+ file->glob = NULL;
1999+ if (glob) {
2000+ file->glob = nih_strdup (file, glob);
2001+ if (! file->glob)
2002+ goto error;
2003+ }
2004+
2005+ file->events = events;
2006+
2007+ return file;
2008+
2009+error:
2010+ nih_free (file);
2011+ return NULL;
2012+}
2013+
2014+/**
2015+ * job_new:
2016+ *
2017+ * @path: Upstart job class (D-Bus) path job is registered on.
2018+ *
2019+ * Create a new Job object representing an Upstart job.
2020+ *
2021+ * Returns: job, or NULL on insufficient memory.
2022+ **/
2023+static Job *
2024+job_new (const char *path)
2025+{
2026+ Job *job;
2027+
2028+ nih_assert (path);
2029+
2030+ job = nih_new (NULL, Job);
2031+ if (! job)
2032+ return NULL;
2033+
2034+ nih_list_init (&job->entry);
2035+ nih_list_init (&job->files);
2036+
2037+ nih_alloc_set_destructor (job, job_destroy);
2038+
2039+ job->path = nih_strdup (job, path);
2040+ if (! job->path)
2041+ goto error;
2042+
2043+ nih_hash_add (jobs, &job->entry);
2044+
2045+ return job;
2046+
2047+error:
2048+ nih_free (job);
2049+ return NULL;
2050+}
2051+
2052+/**
2053+ * job_destroy:
2054+ *
2055+ * @job: job.
2056+ *
2057+ * Destructor that handles the replacement and deletion of a Job,
2058+ * ensuring that it is removed from the containing linked list and that
2059+ * the item attached to it is destroyed if not currently in use.
2060+ *
2061+ * Normally used or called from an nih_alloc() destructor so that the
2062+ * list item is automatically removed from its containing list when
2063+ * freed.
2064+ *
2065+ * Returns: zero.
2066+ **/
2067+static int
2068+job_destroy (Job *job)
2069+{
2070+ nih_assert (job);
2071+
2072+ nih_list_destroy (&job->entry);
2073+
2074+ NIH_LIST_FOREACH_SAFE (&job->files, iter) {
2075+ NihListEntry *entry = (NihListEntry *)iter;
2076+ WatchedFile *file;
2077+
2078+ nih_assert (entry->data);
2079+
2080+ file = (WatchedFile *)entry->data;
2081+
2082+ /* Remove file from associated WatchedDir */
2083+ nih_free (file);
2084+ }
2085+
2086+ return 0;
2087+}
2088+
2089+/**
2090+ * find_first_parent:
2091+ * @path: initial absolute path to start search from.
2092+ *
2093+ * Starting at @path, search for the first existing path by
2094+ * progressively removing individual path elements until an existing
2095+ * path is found.
2096+ *
2097+ * Returns: Newly-allocated string representing path closest to @path
2098+ * that currently exists, or NULL on insufficient memory.
2099+ **/
2100+static char *
2101+find_first_parent (const char *path)
2102+{
2103+ char current[PATH_MAX];
2104+ char tmp[PATH_MAX];
2105+ char *parent;
2106+ WatchedDir *dir = NULL;
2107+ struct stat statbuf;
2108+
2109+ nih_assert (path);
2110+
2111+ /* Ensure path is absolute */
2112+ nih_assert (path[0] == '/');
2113+
2114+ strncpy (current, path, sizeof (current));
2115+ /* ensure termination */
2116+ current[PATH_MAX-1] = '\0';
2117+
2118+ do {
2119+ /* save parent for next time through the loop */
2120+ strcpy (tmp, current);
2121+ parent = dirname (tmp);
2122+
2123+ /* Ensure dirname returned something sane */
2124+ nih_assert (strcmp (parent, "."));
2125+
2126+ dir = (WatchedDir *)nih_hash_lookup (watched_dirs, current);
2127+
2128+ if (dir || ! stat (current, &statbuf)) {
2129+ /* either path is already a watched directory
2130+ * (and hence must exist), or it actually does exist.
2131+ */
2132+ return nih_strdup (NULL, current);
2133+ }
2134+
2135+ /* Failed to find path, so make parent the path to look
2136+ * for.
2137+ */
2138+ memmove (current, parent, 1+strlen (parent));
2139+ } while (TRUE);
2140+
2141+ /* If your root directory doesn't exist, you have problems :) */
2142+ nih_assert_not_reached ();
2143+}
2144+
2145+/**
2146+ * file_event_new:
2147+ *
2148+ * @parent: parent,
2149+ * @path: path that event should contain,
2150+ * @event: inotify event,
2151+ * @match: file match if @path contains glob wildcards.
2152+ *
2153+ * Returns: newly-allocated FileEvent or NULL on insufficient memory.
2154+ **/
2155+static FileEvent *
2156+file_event_new (void *parent, const char *path, uint32_t event, const char *match)
2157+{
2158+ FileEvent *file_event;
2159+
2160+ nih_assert (path);
2161+ nih_assert (event);
2162+
2163+ file_event = nih_new (parent, FileEvent);
2164+ if (! file_event)
2165+ return NULL;
2166+
2167+ nih_list_init (&file_event->entry);
2168+
2169+ nih_alloc_set_destructor (file_event, nih_list_destroy);
2170+
2171+ file_event->path = NIH_MUST (nih_strdup (file_event, path));
2172+ file_event->event = event;
2173+ file_event->match = match
2174+ ? NIH_MUST (nih_strdup (file_event, match))
2175+ : NULL;
2176+
2177+ return file_event;
2178+}
2179+
2180+/**
2181+ * handle_event:
2182+ *
2183+ * @handled: hash of FileEvents already handled,
2184+ * @file_event: FileEvent to consider.
2185+ *
2186+ * Determine if @file_event has already been handled; if not emit the
2187+ * event and record its details in @handled.
2188+ **/
2189+static void
2190+handle_event (NihHash *handled,
2191+ const char *path,
2192+ uint32_t event,
2193+ const char *match)
2194+{
2195+ FileEvent *file_event;
2196+
2197+ nih_assert (handled);
2198+ nih_assert (path);
2199+ nih_assert (event);
2200+
2201+ file_event = (FileEvent *)nih_hash_search (handled, path, NULL);
2202+
2203+ while (file_event) {
2204+ if ((file_event->event & event) && string_match (file_event->match, match)) {
2205+ return;
2206+ }
2207+
2208+ file_event = (FileEvent *)nih_hash_search (handled, path,
2209+ &file_event->entry);
2210+ }
2211+
2212+ nih_assert (! file_event);
2213+
2214+ /* Event has not yet been handled, so emit it and record fact
2215+ * it's now been handled.
2216+ */
2217+ file_event = NIH_MUST (file_event_new (handled, path, event, match));
2218+ nih_hash_add (handled, &file_event->entry);
2219+
2220+ emit_event (path, event, match);
2221+}
2222+
2223+/**
2224+ * string_match:
2225+ *
2226+ * @a: first string,
2227+ * @b: second string.
2228+ *
2229+ * Compare @a and @b either or both of which may be NULL.
2230+ *
2231+ * Returns TRUE if strings are identical or both NULL, else FALSE.
2232+ **/
2233+static int
2234+string_match (const char *a, const char *b)
2235+{
2236+ if (!a && !b)
2237+ return TRUE;
2238+
2239+ if (!a || !b)
2240+ return FALSE;
2241+
2242+ if (strcmp (a, b))
2243+ return FALSE;
2244+
2245+ return TRUE;
2246+}
2247+
2248+/**
2249+ * expand_path:
2250+ *
2251+ * @parent: parent,
2252+ * @path: path.
2253+ *
2254+ * Expand @path by replacing a leading '~/', './' or no path prefix by
2255+ * the users home directory.
2256+ *
2257+ * Limitations: Does not expand '~user'.
2258+ *
2259+ * Returns: Newly-allocated fully-expanded path, or NULL on error.
2260+ **/
2261+char *
2262+expand_path (const void *parent, const char *path)
2263+{
2264+ char *new;
2265+ const char *p;
2266+
2267+ nih_assert (path);
2268+
2269+ /* Only user instances support this limited form of relative
2270+ * path.
2271+ */
2272+ nih_assert (user);
2273+
2274+ /* Avoid looking up users password entry again */
2275+ nih_assert (home_dir[0]);
2276+
2277+ /* absolute path so nothing to do */
2278+ nih_assert (path[0] != '/');
2279+
2280+ p = path;
2281+
2282+ if (strstr (path, "~/") == path || strstr (path, "./") == path)
2283+ p += 2;
2284+
2285+ new = nih_sprintf (parent, "%s/%s", home_dir, p);
2286+
2287+ return new;
2288+}
2289+
2290+/**
2291+ * path_valid:
2292+ *
2293+ * @path: path.
2294+ *
2295+ * Perform basic tests to determine if @path is valid for
2296+ * the purposes of this bridge.
2297+ *
2298+ * Returns: TRUE if @path is acceptable, else FALSE.
2299+ **/
2300+static int
2301+path_valid (const char *path)
2302+{
2303+ size_t len;
2304+
2305+ nih_assert (path);
2306+
2307+ len = strlen (path);
2308+
2309+ if (len > PATH_MAX-1) {
2310+ nih_debug ("%s: %.*s...",
2311+ _("Path too long"), PATH_MAX-1, path);
2312+ return FALSE;
2313+ }
2314+
2315+ if (user) {
2316+ /* Support absolute or relative paths where the latter
2317+ * begins with a directory name implicitly below $HOME.
2318+ */
2319+ if (*path == '.') {
2320+ nih_warn ("%s: %s", _("Path must be absolute"), path);
2321+ return FALSE;
2322+ }
2323+ } else {
2324+ if (*path != '/') {
2325+ nih_warn ("%s: %s", _("Path must be absolute"), path);
2326+ return FALSE;
2327+ }
2328+ }
2329+
2330+ if (strstr (path, "../")) {
2331+ nih_warn ("%s: %s", _("Path must not contain parent reference"), path);
2332+ return FALSE;
2333+ }
2334+
2335+ return TRUE;
2336+}

Subscribers

People subscribed via source and target branches