Merge lp:~jamesodhunt/upstart/bug-1079715 into lp:~upstart-devel/upstart/1.6

Proposed by James Hunt
Status: Merged
Merged at revision: 1392
Proposed branch: lp:~jamesodhunt/upstart/bug-1079715
Merge into: lp:~upstart-devel/upstart/1.6
Diff against target: 1119 lines (+722/-64)
10 files modified
dbus/com.ubuntu.Upstart.xml (+4/-0)
init/Makefile.am (+9/-2)
init/control.c (+62/-0)
init/control.h (+5/-0)
init/job_class.c (+36/-28)
init/session.h (+2/-1)
init/state.c (+111/-20)
init/state.h (+12/-0)
init/tests/data/upstart-1.6.json (+1/-0)
init/tests/test_state.c (+480/-13)
To merge this branch: bzr merge lp:~jamesodhunt/upstart/bug-1079715
Reviewer Review Type Date Requested Status
Steve Langasek Needs Fixing
Review via email: mp+137897@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Steve Langasek (vorlon) wrote :

+ /* Get the relevant session */
+ session = session_from_dbus (NULL, message);
+
+ /* We don't want chroot sessions snooping outside their domain.
+ *
+ * Ideally, we'd allow them to query their own session, but the
+ * current implementation doesn't lend itself to that.
+ */
+ if (session && session->chroot) {
+ nih_warn (_("Ignoring state query from chroot session"));
+ return 0;
+ }
+
+ /* Disallow users from obtaining state details, unless they
+ * happen to own this process (which they may do in the test
+ * scenario and when running Upstart as a non-privileged user).
+ */
+ if (session && session->user && session->user != uid) {

Is there ever any possibility of a non-null session, where session->chroot is null and session->user is 0? If such a session exists, all users will be able to read it.

It's probably better to just do:

+ if (session && session->user != uid) {

this way, we always block access unless the uid matches - much more future-proof.

+ control_bus_flush ();

I'm not sure why this is needed in the GetState case. Is this because we can't serialize dbus state? Why would that be important in this context? Since we don't need to be able to deserialize the resulting state, I think this should be omitted unless not flushing somehow causes state_to_string() to fail.

+ while (TRUE) {
+ bytes = write (fd, buffer->buf, buffer->len);
+
+ if (! bytes)
+ break;
+ else if (bytes > 0)
+ nih_io_buffer_shrink (buffer, (size_t)bytes);
+ else if (bytes < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) {
+ break;
+ }
+ }
+

nih_io_buffer_shrink() seems an awfully inefficient way to do this, but I guess it's consistent with the rest of the code.

Shouldn't EINTR be handled here? Also, EWOULDBLOCK is AIUI only a specified behavior in the case of a socket, which we know this is not. So I think you want (errno != EAGAIN && errno != EINTR).

Otherwise, this looks good. I hope you didn't have to construct that json test data by hand ("Christmas"? :).

review: Needs Fixing
lp:~jamesodhunt/upstart/bug-1079715 updated
1395. By James Hunt

* init/control.c: control_get_state():
  - Simplified session check.
  - Changed error message to refer to 'state', not 'restart'.
  - Don't call control_bus_flush() since it's not technically required
    in this non-re-exec scenario.
* init/state.c: state_write_file():
  - Added comment.
  - Check write for EINTR, not EAGAIN/EWOULDBLOCK.

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

> + /* Get the relevant session */
> + session = session_from_dbus (NULL, message);
> +
> + /* We don't want chroot sessions snooping outside their domain.
> + *
> + * Ideally, we'd allow them to query their own session, but the
> + * current implementation doesn't lend itself to that.
> + */
> + if (session && session->chroot) {
> + nih_warn (_("Ignoring state query from chroot session"));
> + return 0;
> + }
> +
> + /* Disallow users from obtaining state details, unless they
> + * happen to own this process (which they may do in the test
> + * scenario and when running Upstart as a non-privileged user).
> + */
> + if (session && session->user && session->user != uid) {
>
> Is there ever any possibility of a non-null session, where session->chroot is null and session->user is 0? If such a session exists, all users will be able to read it.
>
> It's probably better to just do:
>
> + if (session && session->user != uid) {
>
> this way, we always block access unless the uid matches - much more future-proof.

Agreed - fixed.

>
> + control_bus_flush ();
>
> I'm not sure why this is needed in the GetState case. Is this because we can't serialize dbus state? Why would that be important in this context? Since we don't need to be able to deserialize the resulting state, I think this should be omitted unless not flushing somehow causes state_to_string() to fail.

This call is consistent with the re-exec behaviour, but you are correct in that it's not technically required (and a facility to query state shouldn't potentially be changing it), so I've removed the call for this scenario.

>
> + while (TRUE) {
> + bytes = write (fd, buffer->buf, buffer->len);
> +
> + if (! bytes)
> + break;
> + else if (bytes > 0)
> + nih_io_buffer_shrink (buffer, (size_t)bytes);
> + else if (bytes < 0 && (errno != EAGAIN && errno != EWOULDBLOCK)) {
> + break;
> + }
> + }
> +
>
> nih_io_buffer_shrink() seems an awfully inefficient way to do this, but I guess it's consistent with the rest of the code.

This could indeed be written in a more efficient manner, but it's using the standard NIH idiom and is simple and clear I think. Also recall that this code only gets exercised in what is hopefully an impossible failure scenario anyway :-)

>
> Shouldn't EINTR be handled here? Also, EWOULDBLOCK is AIUI only a specified behavior in the case of a socket, which we know this is not. So I think you want (errno != EAGAIN && errno != EINTR).
>

Good catch! Had my socket cap on. Fixed :)

> Otherwise, this looks good. I hope you didn't have to construct that json test data by hand ("Christmas"? :).

Thankfully not ;-)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'dbus/com.ubuntu.Upstart.xml'
2--- dbus/com.ubuntu.Upstart.xml 2012-09-09 21:22:06 +0000
3+++ dbus/com.ubuntu.Upstart.xml 2012-12-06 09:42:22 +0000
4@@ -34,6 +34,10 @@
5 <arg name="jobs" type="ao" direction="out" />
6 </method>
7
8+ <method name="GetState">
9+ <arg name="state" type="s" direction="out" />
10+ </method>
11+
12 <!-- Signals for changes to the job list -->
13 <signal name="JobAdded">
14 <arg name="job" type="o" />
15
16=== modified file 'init/Makefile.am'
17--- init/Makefile.am 2012-11-18 05:57:58 +0000
18+++ init/Makefile.am 2012-12-06 09:42:22 +0000
19@@ -126,8 +126,15 @@
20 $(com_ubuntu_Upstart_Job_OUTPUTS) \
21 $(com_ubuntu_Upstart_Instance_OUTPUTS)
22
23-
24-EXTRA_DIST = init.supp
25+TEST_DATA_DIR = $(PWD)/tests/data
26+
27+AM_CPPFLAGS += -DTEST_DATA_DIR="\"$(TEST_DATA_DIR)\""
28+
29+TEST_DATA_FILES = \
30+ $(TEST_DATA_DIR)/upstart-1.6-blocked.json
31+
32+EXTRA_DIST = init.supp $(TEST_DATA_FILES)
33+
34
35 test_util_SOURCES = \
36 tests/test_util.c tests/test_util.h
37
38=== modified file 'init/control.c'
39--- init/control.c 2012-09-20 15:16:52 +0000
40+++ init/control.c 2012-12-06 09:42:22 +0000
41@@ -949,3 +949,65 @@
42
43 return 0;
44 }
45+
46+/**
47+ * control_get_state:
48+ *
49+ * @data: not used,
50+ * @message: D-Bus connection and message received,
51+ * @state: output string returned to client.
52+ *
53+ * Convert internal state to JSON string.
54+ *
55+ * Returns: zero on success, negative value on raised error.
56+ **/
57+int
58+control_get_state (void *data,
59+ NihDBusMessage *message,
60+ char **state)
61+{
62+ Session *session;
63+ uid_t uid;
64+ size_t len;
65+
66+ nih_assert (message);
67+ nih_assert (state);
68+
69+ uid = getuid ();
70+
71+ /* Get the relevant session */
72+ session = session_from_dbus (NULL, message);
73+
74+ /* We don't want chroot sessions snooping outside their domain.
75+ *
76+ * Ideally, we'd allow them to query their own session, but the
77+ * current implementation doesn't lend itself to that.
78+ */
79+ if (session && session->chroot) {
80+ nih_warn (_("Ignoring state query from chroot session"));
81+ return 0;
82+ }
83+
84+ /* Disallow users from obtaining state details, unless they
85+ * happen to own this process (which they may do in the test
86+ * scenario and when running Upstart as a non-privileged user).
87+ */
88+ if (session && session->user != uid) {
89+ nih_dbus_error_raise_printf (
90+ DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
91+ _("You do not have permission to request state"));
92+ return -1;
93+ }
94+
95+ if (state_to_string (state, &len) < 0)
96+ goto error;
97+
98+ nih_ref (*state, message);
99+
100+ return 0;
101+
102+error:
103+ nih_dbus_error_raise_printf (DBUS_ERROR_NO_MEMORY,
104+ _("Out of Memory"));
105+ return -1;
106+}
107
108=== modified file 'init/control.h'
109--- init/control.h 2012-09-11 14:59:40 +0000
110+++ init/control.h 2012-12-06 09:42:22 +0000
111@@ -105,6 +105,11 @@
112 int control_bus_release_name (void)
113 __attribute__ ((warn_unused_result));
114
115+int control_get_state (void *data,
116+ NihDBusMessage *message,
117+ char **state)
118+ __attribute__ ((warn_unused_result));
119+
120 NIH_END_EXTERN
121
122 #endif /* INIT_CONTROL_H */
123
124=== modified file 'init/job_class.c'
125--- init/job_class.c 2012-11-14 14:47:19 +0000
126+++ init/job_class.c 2012-12-06 09:42:22 +0000
127@@ -271,9 +271,7 @@
128
129 /* If we found an entry, ensure we only consider the appropriate session */
130 while (registered && registered->session != class->session)
131- {
132 registered = (JobClass *)nih_hash_search (job_classes, class->name, &registered->entry);
133- }
134
135 if (registered != best) {
136 if (registered)
137@@ -314,9 +312,7 @@
138
139 /* If we found an entry, ensure we only consider the appropriate session */
140 while (registered && registered->session != class->session)
141- {
142 registered = (JobClass *)nih_hash_search (job_classes, class->name, &registered->entry);
143- }
144
145 if (registered == class) {
146 if (class != best) {
147@@ -364,7 +360,7 @@
148 * @class: new class to select.
149 *
150 * Adds @class to the hash table iff no existing entry of the
151- * same name exists.
152+ * same name exists for the same session.
153 **/
154 void
155 job_class_add_safe (JobClass *class)
156@@ -376,7 +372,11 @@
157
158 control_init ();
159
160- existing = (JobClass *)nih_hash_search (job_classes, class->name, NULL);
161+ /* Ensure no existing class exists for the same session */
162+ do {
163+ existing = (JobClass *)nih_hash_search (job_classes,
164+ class->name, existing ? &existing->entry : NULL);
165+ } while (existing && existing->session != class->session);
166
167 nih_assert (! existing);
168
169@@ -1592,6 +1592,15 @@
170 json = json_object_new_object ();
171 if (! json)
172 return NULL;
173+
174+ /* XXX: user and chroot jobs are not currently supported
175+ * due to ConfSources not currently being serialised.
176+ */
177+ if (class->session) {
178+ nih_info ("WARNING: serialisation of user jobs and "
179+ "chroot sessions not currently supported");
180+ goto error;
181+ }
182
183 session_index = session_get_index (class->session);
184 if (session_index < 0)
185@@ -1797,6 +1806,7 @@
186 {
187 json_object *json_normalexit;
188 JobClass *class = NULL;
189+ Session *session;
190 int session_index = -1;
191 int ret;
192 nih_local char *name = NULL;
193@@ -1814,21 +1824,24 @@
194 if (session_index < 0)
195 goto error;
196
197+ session = session_from_index (session_index);
198+
199+ /* XXX: user and chroot jobs are not currently supported
200+ * due to ConfSources not currently being serialised.
201+ */
202+ if (session) {
203+ nih_info ("WARNING: deserialisation of user jobs and "
204+ "chroot sessions not currently supported");
205+ goto error;
206+ }
207+
208 if (! state_get_json_string_var_strict (json, "name", NULL, name))
209 goto error;
210
211- class = NIH_MUST (job_class_new (NULL, name,
212- session_from_index (session_index)));
213+ class = job_class_new (NULL, name, session);
214 if (! class)
215 goto error;
216
217- if (class->session != NULL) {
218- nih_warn ("XXX: WARNING (%s:%d): deserialisation of "
219- "user jobs and chroot sessions not currently supported",
220- __func__, __LINE__);
221- goto error;
222- }
223-
224 /* job_class_new() sets path */
225 if (! state_get_json_string_var_strict (json, "path", NULL, path))
226 goto error;
227@@ -2002,7 +2015,9 @@
228 return class;
229
230 error:
231- nih_free (class);
232+ if (class)
233+ nih_free (class);
234+
235 return NULL;
236 }
237
238@@ -2043,19 +2058,12 @@
239 goto error;
240
241 class = job_class_deserialise (json_class);
242+
243+ /* For parity with the serialisation code, don't treat
244+ * errors as fatal for the entire deserialisation.
245+ */
246 if (! class)
247- goto error;
248-
249- /* FIXME:
250- *
251- * If user sessions exist (ie 'initctl --session list'
252- * has been run), we get this failure:
253- *
254- * serialised path='/com/ubuntu/Upstart/jobs/1000/bang'
255- * path set by job_class_new()='/com/ubuntu/Upstart/jobs/_/1000/bang'
256- *
257- */
258-
259+ continue;
260 }
261
262 return 0;
263
264=== modified file 'init/session.h'
265--- init/session.h 2012-06-29 16:19:33 +0000
266+++ init/session.h 2012-12-06 09:42:22 +0000
267@@ -39,7 +39,8 @@
268 * with a chroot path)).
269 *
270 * This structure is used to identify collections of jobs
271- * that share either a common @chroot and/or common @user.
272+ * that share either a common @chroot and/or common @user. Note that
273+ * @conf_path is unique across all sessions.
274 *
275 * Summary of Session values for different environments:
276 *
277
278=== modified file 'init/state.c'
279--- init/state.c 2012-11-23 11:36:47 +0000
280+++ init/state.c 2012-12-06 09:42:22 +0000
281@@ -2,7 +2,7 @@
282 *
283 * state.c - serialisation and deserialisation support.
284 *
285- * Copyright © 2012 Canonical Ltd.
286+ * Copyright 2012 Canonical Ltd.
287 * Author: James Hunt <james.hunt@canonical.com>.
288 *
289 * This program is free software; you can redistribute it and/or modify
290@@ -48,7 +48,8 @@
291 json_object *json_events = NULL;
292 json_object *json_classes = NULL;
293
294-extern int use_session_bus;
295+extern int use_session_bus;
296+extern char *log_dir;
297
298 /**
299 * args_copy:
300@@ -68,22 +69,17 @@
301 int restart = FALSE;
302
303 /* Prototypes for static functions */
304-static json_object *
305-state_serialise_blocked (const Blocked *blocked)
306- __attribute__ ((malloc, warn_unused_result));
307-
308-static Blocked *
309-state_deserialise_blocked (void *parent, json_object *json, NihList *list)
310- __attribute__ ((malloc, warn_unused_result));
311-
312 static JobClass *
313 state_index_to_job_class (int job_class_index)
314 __attribute__ ((warn_unused_result));
315
316 static Job *
317-state_get_job (const char *job_class, const char *job_name)
318+state_get_job (const Session *session, const char *job_class,
319+ const char *job_name)
320 __attribute__ ((warn_unused_result));
321
322+static void state_write_file (NihIoBuffer *buffer);
323+
324 /**
325 * state_read:
326 *
327@@ -246,10 +242,59 @@
328 return 0;
329
330 error:
331+ /* Failed to reconstruct internal state so attempt to write
332+ * the JSON state data to a file to allow for manual post
333+ * re-exec analysis.
334+ */
335+ if (buffer->len && log_dir)
336+ state_write_file (buffer);
337+
338 return -1;
339 }
340
341 /**
342+ * state_write_file:
343+ *
344+ * @buffer: NihIoBuffer containing JSON data.
345+ *
346+ * Write JSON data contained in @buffer to STATE_FILE below log_dir.
347+ *
348+ * Failures are ignored since this is designed to be called in an error
349+ * scenario anyway.
350+ **/
351+void
352+state_write_file (NihIoBuffer *buffer)
353+{
354+ int fd;
355+ ssize_t bytes;
356+ nih_local char *state_file = NULL;
357+
358+ nih_assert (buffer);
359+
360+ state_file = nih_sprintf (NULL, "%s/%s", log_dir, STATE_FILE);
361+ if (! state_file)
362+ return;
363+
364+ /* Note the very restrictive permissions */
365+ fd = open (state_file, (O_CREAT|O_WRONLY|O_TRUNC), S_IRUSR);
366+ if (fd < 0)
367+ return;
368+
369+ while (TRUE) {
370+ bytes = write (fd, buffer->buf, buffer->len);
371+
372+ if (! bytes)
373+ break;
374+ else if (bytes > 0)
375+ nih_io_buffer_shrink (buffer, (size_t)bytes);
376+ else if (bytes < 0 && errno != EINTR)
377+ break;
378+ }
379+
380+ close (fd);
381+}
382+
383+/**
384 * state_write_objects:
385 *
386 * @fd: file descriptor to write serialisation data on,
387@@ -1139,6 +1184,12 @@
388 if (! class)
389 goto error;
390
391+ /* XXX: user and chroot jobs are not currently supported
392+ * due to ConfSources not currently being serialised.
393+ */
394+ if (class->session)
395+ continue;
396+
397 if (! state_get_json_var_full (json_class, "jobs", array, json_jobs))
398 goto error;
399
400@@ -1164,7 +1215,7 @@
401 goto error;
402
403 /* lookup job */
404- job = state_get_job (class->name, job_name);
405+ job = state_get_job (class->session, class->name, job_name);
406 if (! job)
407 goto error;
408
409@@ -1200,11 +1251,12 @@
410 *
411 * Returns: JSON-serialised Blocked object, or NULL on error.
412 **/
413-static json_object *
414+json_object *
415 state_serialise_blocked (const Blocked *blocked)
416 {
417 json_object *json;
418 json_object *json_blocked_data;
419+ int session_index;
420
421 nih_assert (blocked);
422
423@@ -1228,6 +1280,18 @@
424 blocked->job->class->name))
425 goto error;
426
427+ session_index = session_get_index (blocked->job->class->session);
428+ if (session_index < 0)
429+ goto error;
430+
431+ /* Encode parent classes session index to aid in
432+ * finding the correct job on deserialisation.
433+ */
434+ if (! state_set_json_int_var (json_blocked_data,
435+ "session",
436+ session_index))
437+ goto error;
438+
439 if (! state_set_json_string_var (json_blocked_data,
440 "name",
441 blocked->job->name))
442@@ -1389,7 +1453,7 @@
443 *
444 * Returns: new Blocked object, or NULL on error.
445 **/
446-static Blocked *
447+Blocked *
448 state_deserialise_blocked (void *parent, json_object *json,
449 NihList *list)
450 {
451@@ -1397,7 +1461,7 @@
452 Blocked *blocked = NULL;
453 nih_local char *blocked_type_str = NULL;
454 BlockedType blocked_type;
455- int ret;
456+ int ret;
457
458 nih_assert (parent);
459 nih_assert (json);
460@@ -1421,6 +1485,8 @@
461 nih_local char *job_name = NULL;
462 nih_local char *job_class_name = NULL;
463 Job *job;
464+ Session *session;
465+ int session_index;
466
467 if (! state_get_json_string_var_strict (json_blocked_data,
468 "name", NULL, job_name))
469@@ -1430,7 +1496,19 @@
470 "class", NULL, job_class_name))
471 goto error;
472
473- job = state_get_job (job_class_name, job_name);
474+ /* On error, assume NULL session since the likelihood
475+ * is we're upgrading from Upstart 1.6 that did not set
476+ * the 'session' JSON object.
477+ */
478+ if (! state_get_json_int_var (json_blocked_data, "session", session_index))
479+ session_index = 0;
480+
481+ if (session_index < 0)
482+ goto error;
483+
484+ session = session_from_index (session_index);
485+
486+ job = state_get_job (session, job_class_name, job_name);
487 if (! job)
488 goto error;
489
490@@ -1583,8 +1661,14 @@
491 if (! json_blocked)
492 goto error;
493
494+
495+ /* Don't error in this scenario to allow for possibility
496+ * that version of Upstart that performed the
497+ * serialisation did not correctly handle user and
498+ * chroot jobs.
499+ */
500 if (! state_deserialise_blocked (parent, json_blocked, list))
501- goto error;
502+ ;
503 }
504
505 return 0;
506@@ -1625,6 +1709,7 @@
507 /**
508 * state_get_job:
509 *
510+ * @session: session of job class,
511 * @job_class: name of job class,
512 * @job_name: name of job instance.
513 *
514@@ -1635,15 +1720,21 @@
515 * job not found.
516 **/
517 static Job *
518-state_get_job (const char *job_class, const char *job_name)
519+state_get_job (const Session *session,
520+ const char *job_class,
521+ const char *job_name)
522 {
523- JobClass *class;
524+ JobClass *class = NULL;
525 Job *job;
526
527 nih_assert (job_class);
528 nih_assert (job_classes);
529
530- class = (JobClass *)nih_hash_lookup (job_classes, job_class);
531+ do {
532+ class = (JobClass *)nih_hash_search (job_classes,
533+ job_class, class ? &class->entry : NULL);
534+ } while (class && class->session != session);
535+
536 if (! class)
537 goto error;
538
539
540=== modified file 'init/state.h'
541--- init/state.h 2012-11-14 14:47:19 +0000
542+++ init/state.h 2012-12-06 09:42:22 +0000
543@@ -367,6 +367,18 @@
544 #define STATE_WAIT_SECS_ENV "UPSTART_STATE_WAIT_SECS"
545
546 /**
547+ * STATE_FILE:
548+ *
549+ * Name of file that is written below the job log directory if the
550+ * newly re-exec'ed init instance failed to understand the JSON sent to
551+ * it by the old instance.
552+ *
553+ * This could happen for example if the old instance generated invalid
554+ * JSON, or JSON in an unexected format.
555+ **/
556+#define STATE_FILE "upstart.state"
557+
558+/**
559 * state_get_timeout:
560 *
561 * @var: name of long integer var to set to timeout value.
562
563=== added directory 'init/tests/data'
564=== added file 'init/tests/data/upstart-1.6.json'
565--- init/tests/data/upstart-1.6.json 1970-01-01 00:00:00 +0000
566+++ init/tests/data/upstart-1.6.json 2012-12-06 09:42:22 +0000
567@@ -0,0 +1,1 @@
568+{ "sessions": [ ], "events": [ { "session": 0, "name": "Christmas", "fd": -1, "progress": "EVENT_PENDING", "failed": 0, "blockers": 0, "blocking": [ { "data": { "class": "bar", "name": "" }, "type": "BLOCKED_JOB" } ] } ], "job_classes": [ { "session": 0, "name": "bar", "path": "\/com\/ubuntu\/Upstart\/jobs\/bar", "instance": "", "jobs": [ { "name": "", "path": "\/com\/ubuntu\/Upstart\/jobs\/bar\/_", "goal": "JOB_STOP", "state": "JOB_WAITING", "env": [ ], "start_env": [ ], "stop_env": [ ], "fds": [ ], "pid": [ 0, 0, 0, 0, 0 ], "blocker": 0, "kill_process": "PROCESS_INVALID", "failed": 0, "failed_process": "PROCESS_INVALID", "exit_status": 0, "respawn_time": 0, "respawn_count": 0, "trace_forks": 0, "trace_state": "TRACE_NONE", "log": [ { "path": null }, { "path": null }, { "path": null }, { "path": null }, { "path": null } ] } ], "description": null, "author": null, "version": null, "env": [ ], "export": [ ], "emits": [ ], "process": [ { "script": 0, "command": null }, { "script": 0, "command": null }, { "script": 0, "command": null }, { "script": 0, "command": null }, { "script": 0, "command": null } ], "expect": "EXPECT_NONE", "task": 0, "kill_timeout": 5, "kill_signal": 15, "respawn": 0, "respawn_limit": 10, "respawn_interval": 5, "normalexit": [ ], "console": "CONSOLE_LOG", "umask": 18, "nice": 0, "oom_score_adj": 0, "limits": [ { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 }, { "rlim_cur": 0, "rlim_max": 0 } ], "chroot": null, "chdir": null, "setuid": null, "setgid": null, "deleted": 0, "debug": 0, "usage": null } ] }
569
570=== modified file 'init/tests/test_job_process.c'
571=== modified file 'init/tests/test_state.c'
572--- init/tests/test_state.c 2012-11-14 14:47:19 +0000
573+++ init/tests/test_state.c 2012-12-06 09:42:22 +0000
574@@ -50,11 +50,27 @@
575 #include "control.h"
576 #include "test_util.h"
577
578+#ifndef TEST_DATA_DIR
579+#error ERROR: TEST_DATA_DIR not defined
580+#endif
581+
582+/* These functions are 'protected'.
583+ *
584+ * The test code needs access, but they cannot be public due to
585+ * header-file complications.
586+ */
587+json_object *
588+state_serialise_blocked (const Blocked *blocked)
589+ __attribute__ ((malloc, warn_unused_result));
590+
591+Blocked *
592+state_deserialise_blocked (void *parent, json_object *json, NihList *list)
593+ __attribute__ ((malloc, warn_unused_result));
594
595 /**
596 * AlreadySeen:
597 *
598- * Used to allow objects that directly or indirectly reference on
599+ * Used to allow objects that directly or indirectly reference
600 * another to be inspected and compared without causing infinite
601 * recursion.
602 *
603@@ -127,6 +143,43 @@
604 int blocked_diff (const Blocked *a, const Blocked *b, AlreadySeen seen)
605 __attribute__ ((warn_unused_result));
606
607+void test_upstart1_6_upgrade (const char *conf_file, const char *path);
608+
609+/**
610+ * TestDataFile:
611+ *
612+ * @conf_file: Name of ConfFile that must be created prior to
613+ * deserialising JSON data in @filename.
614+ * @filename: basename of data file,
615+ * @func: function to run to test @filename.
616+ *
617+ * Representation of a JSON data file used for ensuring that the current
618+ * version of Upstart is able to deserialise all previous JSON data file
619+ * format versions.
620+ *
621+ * @conf_file is required since we do not currently serialise ConfFile
622+ * and ConfSource objects so these entities must be created immediately
623+ * prior to attempting deserialisation.
624+ *
625+ * @func returns nothing so is expected to assert on any error.
626+ **/
627+typedef struct test_data_file {
628+ char *conf_file;
629+ char *filename;
630+ void (*func) (const char *conf_file, const char *path);
631+} TestDataFile;
632+
633+/**
634+ * test_data_files:
635+ *
636+ * Array of data files to test.
637+ **/
638+TestDataFile test_data_files[] = {
639+ { "bar", "upstart-1.6.json", test_upstart1_6_upgrade },
640+
641+ { NULL, NULL, NULL }
642+};
643+
644 /* Data with some embedded nulls */
645 const char test_data[] = {
646 'h', 'e', 'l', 'l', 'o', 0x0, 0x0, 0x0, ' ', 'w', 'o', 'r', 'l', 'd', '\n', '\r', '\0'
647@@ -139,7 +192,7 @@
648 int64_t values64[] = {INT64_MIN, -1, 0, 1, INT64_MAX};
649 const Process test_procs[] = {
650 { 0, "echo hello" },
651- { 1, "echo hello" },
652+ { 1, "echo hello" }
653 };
654 rlim_t rlimit_values[] = { 0, 1, 2, 3, 7, RLIM_INFINITY };
655
656@@ -993,17 +1046,23 @@
657 void
658 test_blocking (void)
659 {
660- nih_local char *json_string = NULL;
661- ConfSource *source = NULL;
662- ConfFile *file;
663- JobClass *class;
664- JobClass *new_class;
665- Job *job;
666- Job *new_job;
667- Event *event;
668- Event *new_event;
669- Blocked *blocked;
670- size_t len;
671+ nih_local char *json_string = NULL;
672+ nih_local char *parent_str = NULL;
673+ ConfSource *source = NULL;
674+ ConfFile *file;
675+ JobClass *class;
676+ JobClass *new_class;
677+ Job *job;
678+ Job *new_job;
679+ Event *event;
680+ Event *new_event;
681+ Blocked *blocked;
682+ Blocked *new_blocked;
683+ NihList blocked_list;
684+ size_t len;
685+ json_object *json_blocked;
686+ Session *session;
687+ Session *new_session;
688
689 conf_init ();
690 session_init ();
691@@ -1014,6 +1073,278 @@
692 TEST_GROUP ("Blocked serialisation and deserialisation");
693
694 /*******************************/
695+ TEST_FEATURE ("BLOCKED_JOB serialisation and deserialisation");
696+
697+ nih_list_init (&blocked_list);
698+ TEST_LIST_EMPTY (&blocked_list);
699+
700+ source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
701+ TEST_NE_P (source, NULL);
702+
703+ file = conf_file_new (source, "/tmp/foo/bar");
704+ TEST_NE_P (file, NULL);
705+ class = file->job = job_class_new (file, "bar", NULL);
706+
707+ TEST_NE_P (class, NULL);
708+ TEST_HASH_EMPTY (job_classes);
709+ TEST_TRUE (job_class_consider (class));
710+ TEST_HASH_NOT_EMPTY (job_classes);
711+
712+ job = job_new (class, "");
713+ TEST_NE_P (job, NULL);
714+
715+ parent_str = nih_strdup (NULL, "parent");
716+ TEST_NE_P (parent_str, NULL);
717+
718+ blocked = blocked_new (NULL, BLOCKED_JOB, job);
719+ TEST_NE_P (blocked, NULL);
720+
721+ json_blocked = state_serialise_blocked (blocked);
722+ TEST_NE_P (json_blocked, NULL);
723+
724+ new_blocked = state_deserialise_blocked (parent_str,
725+ json_blocked, &blocked_list);
726+ TEST_NE_P (new_blocked, NULL);
727+ TEST_LIST_NOT_EMPTY (&blocked_list);
728+
729+ assert0 (blocked_diff (blocked, new_blocked, ALREADY_SEEN_SET));
730+
731+ json_object_put (json_blocked);
732+ nih_free (source);
733+
734+ /*******************************/
735+ TEST_FEATURE ("BLOCKED_EVENT serialisation and deserialisation");
736+
737+ event = event_new (NULL, "event", NULL);
738+ TEST_NE_P (event, NULL);
739+
740+ nih_list_init (&blocked_list);
741+
742+ source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
743+ TEST_NE_P (source, NULL);
744+
745+ file = conf_file_new (source, "/tmp/foo/bar");
746+ TEST_NE_P (file, NULL);
747+ class = file->job = job_class_new (file, "bar", NULL);
748+
749+ TEST_NE_P (class, NULL);
750+ TEST_HASH_EMPTY (job_classes);
751+ TEST_TRUE (job_class_consider (class));
752+ TEST_HASH_NOT_EMPTY (job_classes);
753+
754+ job = job_new (class, "");
755+ TEST_NE_P (job, NULL);
756+
757+ TEST_LIST_EMPTY (&job->blocking);
758+
759+ blocked = blocked_new (NULL, BLOCKED_EVENT, event);
760+ TEST_NE_P (blocked, NULL);
761+
762+ nih_list_add (&job->blocking, &blocked->entry);
763+ TEST_LIST_NOT_EMPTY (&job->blocking);
764+
765+ event->blockers = 1;
766+
767+ parent_str = nih_strdup (NULL, "parent");
768+ TEST_NE_P (parent_str, NULL);
769+
770+ json_blocked = state_serialise_blocked (blocked);
771+ TEST_NE_P (json_blocked, NULL);
772+
773+ TEST_LIST_EMPTY (&blocked_list);
774+ new_blocked = state_deserialise_blocked (parent_str,
775+ json_blocked, &blocked_list);
776+ TEST_NE_P (new_blocked, NULL);
777+ TEST_LIST_NOT_EMPTY (&blocked_list);
778+
779+ assert0 (blocked_diff (blocked, new_blocked, ALREADY_SEEN_SET));
780+
781+ json_object_put (json_blocked);
782+ nih_free (source);
783+ nih_free (event);
784+
785+ /*******************************/
786+ /* Test Upstart 1.6+ behaviour
787+ *
788+ * The data serialisation format for this version now includes
789+ * a "session" entry in the JSON for the blocked job.
790+ *
791+ * Note that this test is NOT testing whether a JobClass with an
792+ * associated Upstart session is handled correctly, it is merely
793+ * testing that a JobClass with the NULL session is handled
794+ * correctly!
795+ */
796+ TEST_FEATURE ("BLOCKED_JOB with JSON session object");
797+
798+ TEST_LIST_EMPTY (sessions);
799+ TEST_LIST_EMPTY (events);
800+ TEST_LIST_EMPTY (conf_sources);
801+ TEST_HASH_EMPTY (job_classes);
802+
803+ event = event_new (NULL, "Christmas", NULL);
804+ TEST_NE_P (event, NULL);
805+ TEST_LIST_EMPTY (&event->blocking);
806+
807+ source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
808+ TEST_NE_P (source, NULL);
809+
810+ file = conf_file_new (source, "/tmp/foo/bar");
811+ TEST_NE_P (file, NULL);
812+ /* Create class with NULL session */
813+ class = file->job = job_class_new (NULL, "bar", NULL);
814+
815+ TEST_NE_P (class, NULL);
816+ TEST_HASH_EMPTY (job_classes);
817+ TEST_TRUE (job_class_consider (class));
818+ TEST_HASH_NOT_EMPTY (job_classes);
819+
820+ job = job_new (class, "");
821+ TEST_NE_P (job, NULL);
822+ TEST_HASH_NOT_EMPTY (class->instances);
823+
824+ blocked = blocked_new (event, BLOCKED_JOB, job);
825+ TEST_NE_P (blocked, NULL);
826+
827+ nih_list_add (&event->blocking, &blocked->entry);
828+ job->blocker = event;
829+
830+ TEST_LIST_NOT_EMPTY (&event->blocking);
831+
832+ assert0 (state_to_string (&json_string, &len));
833+ TEST_GT (len, 0);
834+
835+ /* XXX: We don't remove the source as these are not
836+ * recreated on re-exec, so we'll re-use the existing one.
837+ */
838+ nih_list_remove (&class->entry);
839+ nih_list_remove (&event->entry);
840+
841+ TEST_LIST_EMPTY (events);
842+ TEST_LIST_NOT_EMPTY (conf_sources);
843+ TEST_HASH_EMPTY (job_classes);
844+
845+ assert0 (state_from_string (json_string));
846+
847+ TEST_LIST_NOT_EMPTY (conf_sources);
848+ TEST_LIST_NOT_EMPTY (events);
849+ TEST_HASH_NOT_EMPTY (job_classes);
850+ TEST_LIST_EMPTY (sessions);
851+
852+ new_class = (JobClass *)nih_hash_lookup (job_classes, "bar");
853+ TEST_NE_P (new_class, NULL);
854+ nih_list_remove (&new_class->entry);
855+
856+ /* Upstart 1.6 can only deserialise the NULL session */
857+ TEST_EQ_P (class->session, NULL);
858+
859+ new_event = (Event *)nih_list_remove (events->next);
860+ TEST_LIST_EMPTY (events);
861+ TEST_LIST_NOT_EMPTY (&new_event->blocking);
862+ assert0 (event_diff (event, new_event, ALREADY_SEEN_SET));
863+
864+ nih_free (event);
865+ nih_free (new_event);
866+ nih_free (source);
867+ nih_free (new_class);
868+
869+ TEST_HASH_EMPTY (job_classes);
870+
871+ TEST_LIST_EMPTY (sessions);
872+ TEST_LIST_EMPTY (events);
873+ TEST_LIST_EMPTY (conf_sources);
874+ TEST_HASH_EMPTY (job_classes);
875+
876+ /*******************************/
877+ /* We don't currently handle user+chroot jobs, so let's assert
878+ * that behaviour.
879+ */
880+ TEST_FEATURE ("ensure BLOCKED_JOB with non-NULL session is ignored");
881+
882+ TEST_LIST_EMPTY (sessions);
883+ TEST_LIST_EMPTY (events);
884+ TEST_LIST_EMPTY (conf_sources);
885+ TEST_HASH_EMPTY (job_classes);
886+
887+ session = session_new (NULL, "/my/session", getuid ());
888+ TEST_NE_P (session, NULL);
889+ session->conf_path = NIH_MUST (nih_strdup (session, "/lives/here"));
890+ TEST_LIST_NOT_EMPTY (sessions);
891+
892+ /* We simulate a user job being blocked by a system event, hence
893+ * the session is not associated with the event.
894+ */
895+ event = event_new (NULL, "Christmas", NULL);
896+ TEST_NE_P (event, NULL);
897+ TEST_LIST_EMPTY (&event->blocking);
898+
899+ source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
900+ source->session = session;
901+ TEST_NE_P (source, NULL);
902+
903+ file = conf_file_new (source, "/tmp/foo/bar");
904+ TEST_NE_P (file, NULL);
905+
906+ /* Create class with non-NULL session, simulating a user job */
907+ class = file->job = job_class_new (NULL, "bar", session);
908+ TEST_NE_P (class, NULL);
909+
910+ TEST_HASH_EMPTY (job_classes);
911+ TEST_TRUE (job_class_consider (class));
912+ TEST_HASH_NOT_EMPTY (job_classes);
913+
914+ job = job_new (class, "");
915+ TEST_NE_P (job, NULL);
916+ TEST_HASH_NOT_EMPTY (class->instances);
917+
918+ blocked = blocked_new (event, BLOCKED_JOB, job);
919+ TEST_NE_P (blocked, NULL);
920+
921+ nih_list_add (&event->blocking, &blocked->entry);
922+ job->blocker = event;
923+
924+ TEST_LIST_NOT_EMPTY (&event->blocking);
925+
926+ assert0 (state_to_string (&json_string, &len));
927+ TEST_GT (len, 0);
928+
929+ /* XXX: We don't remove the source as these are not
930+ * recreated on re-exec, so we'll re-use the existing one.
931+ */
932+ nih_list_remove (&class->entry);
933+ nih_free (event);
934+ nih_list_remove (&session->entry);
935+
936+ TEST_LIST_EMPTY (events);
937+ TEST_LIST_NOT_EMPTY (conf_sources);
938+ TEST_HASH_EMPTY (job_classes);
939+
940+ assert0 (state_from_string (json_string));
941+
942+ TEST_LIST_NOT_EMPTY (conf_sources);
943+ TEST_LIST_NOT_EMPTY (events);
944+
945+ /* We don't expect any job_classes since the serialised one
946+ * related to a user session.
947+ */
948+ TEST_HASH_EMPTY (job_classes);
949+
950+ /* However, the session itself will exist */
951+ TEST_LIST_NOT_EMPTY (sessions);
952+
953+ new_session = (Session *)nih_list_remove (sessions->next);
954+
955+ nih_free (session);
956+ nih_free (new_session);
957+ event = (Event *)nih_list_remove (events->next);
958+ nih_free (event);
959+ nih_free (source);
960+
961+ TEST_LIST_EMPTY (sessions);
962+ TEST_LIST_EMPTY (events);
963+ TEST_LIST_EMPTY (conf_sources);
964+ TEST_HASH_EMPTY (job_classes);
965+
966+ /*******************************/
967 TEST_FEATURE ("event blocking a job");
968
969 TEST_LIST_EMPTY (sessions);
970@@ -2608,6 +2939,141 @@
971 /*******************************/
972 }
973
974+/**
975+ * test_upgrade:
976+ *
977+ * Run tests that simulate an upgrade by attempting to deserialise an
978+ * older version of the JSON data format than is currently used.
979+ **/
980+void
981+test_upgrade (void)
982+{
983+ TestDataFile *datafile;
984+
985+ TEST_GROUP ("upgrade tests");
986+
987+ for (datafile = test_data_files; datafile && datafile->filename; datafile++) {
988+ nih_local char *path = NULL;
989+ nih_local char *name = NULL;
990+
991+ nih_assert (datafile->func != NULL);
992+
993+ name = NIH_MUST (nih_sprintf (NULL, "with data file '%s'",
994+ datafile->filename));
995+ TEST_FEATURE (name);
996+
997+ path = NIH_MUST (nih_sprintf (NULL, "%s/%s",
998+ TEST_DATA_DIR, datafile->filename));
999+
1000+ datafile->func (datafile->conf_file, path);
1001+ }
1002+}
1003+
1004+/**
1005+ * test_upstart1_6_upgrade:
1006+ *
1007+ * @conf_file: name of ConfFile to create prior to running test,
1008+ * @path: full path to JSON data file to deserialise.
1009+ *
1010+ * Test for original Upstart 1.6 serialisation data format containing
1011+ * a blocked object that does not contain a 'session' element.
1012+ *
1013+ * Note that this test is NOT testing whether a JobClass with an
1014+ * associated Upstart session is handled correctly, it is merely
1015+ * testing that a JobClass with the NULL session encoded in the JSON
1016+ * is handled correctly.
1017+ **/
1018+void
1019+test_upstart1_6_upgrade (const char *conf_file, const char *path)
1020+{
1021+ nih_local char *json_string = NULL;
1022+ Event *event;
1023+ ConfSource *source;
1024+ ConfFile *file;
1025+ nih_local char *conf_file_path = NULL;
1026+ struct stat statbuf;
1027+ size_t len;
1028+
1029+ nih_assert (conf_file);
1030+ nih_assert (path);
1031+
1032+ conf_init ();
1033+ session_init ();
1034+ event_init ();
1035+ control_init ();
1036+ job_class_init ();
1037+
1038+ TEST_LIST_EMPTY (sessions);
1039+ TEST_LIST_EMPTY (events);
1040+ TEST_LIST_EMPTY (conf_sources);
1041+ TEST_HASH_EMPTY (job_classes);
1042+
1043+ /* Check data file exists */
1044+ TEST_EQ (stat (path, &statbuf), 0);
1045+
1046+ json_string = nih_file_read (NULL, path, &len);
1047+ TEST_NE_P (json_string, NULL);
1048+
1049+ /* Create the ConfSource and ConfFile objects to simulate
1050+ * Upstart reading /etc/init on startup. Required since we
1051+ * don't currently serialise these objects.
1052+ */
1053+ source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
1054+ TEST_NE_P (source, NULL);
1055+
1056+ conf_file_path = NIH_MUST (nih_sprintf (NULL, "%s/%s",
1057+ "/tmp/foo", conf_file));
1058+
1059+ file = conf_file_new (source, conf_file_path);
1060+ TEST_NE_P (file, NULL);
1061+
1062+ /* Recreate state from JSON data file */
1063+ assert0 (state_from_string (json_string));
1064+
1065+ TEST_LIST_NOT_EMPTY (conf_sources);
1066+ TEST_LIST_NOT_EMPTY (events);
1067+ TEST_HASH_NOT_EMPTY (job_classes);
1068+ TEST_LIST_EMPTY (sessions);
1069+
1070+ event = (Event *)nih_list_remove (events->next);
1071+ TEST_NE_P (event, NULL);
1072+ TEST_EQ_STR (event->name, "Christmas");
1073+
1074+ NIH_HASH_FOREACH (job_classes, iter) {
1075+ JobClass *class = (JobClass *)iter;
1076+
1077+ TEST_EQ_STR (class->name, "bar");
1078+ TEST_EQ_STR (class->path, "/com/ubuntu/Upstart/jobs/bar");
1079+ TEST_HASH_NOT_EMPTY (class->instances);
1080+
1081+ NIH_HASH_FOREACH (class->instances, iter2) {
1082+ Blocked *blocked;
1083+ Job *job = (Job *)iter2;
1084+ nih_local char *instance_path = NULL;
1085+
1086+ /* instance name */
1087+ TEST_EQ_STR (job->name, "");
1088+
1089+ instance_path = NIH_MUST (nih_sprintf (NULL, "%s/_", class->path));
1090+ TEST_EQ_STR (job->path, instance_path);
1091+
1092+ /* job is blocked by the event */
1093+ TEST_EQ (job->blocker, event);
1094+
1095+ /* First entry in list should be a Blocked
1096+ * object pointing to the job.
1097+ */
1098+ TEST_LIST_NOT_EMPTY (&event->blocking);
1099+ blocked = (Blocked *)(&event->blocking)->next;
1100+ TEST_EQ (blocked->type, BLOCKED_JOB);
1101+ TEST_EQ (blocked->job, job);
1102+ }
1103+ }
1104+
1105+ nih_free (event);
1106+ nih_free (conf_sources);
1107+}
1108+
1109 int
1110 main (int argc,
1111 char *argv[])
1112@@ -2632,6 +3098,7 @@
1113 test_log_serialise ();
1114 test_job_serialise ();
1115 test_job_class_serialise ();
1116+ test_upgrade ();
1117
1118 return 0;
1119 }

Subscribers

People subscribed via source and target branches

to all changes: