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

Subscribers

People subscribed via source and target branches