Merge authors:
  James Hunt (jamesodhunt)
Related merge proposals:
  https://code.launchpad.net/~jamesodhunt/upstart/bug-1079715/+merge/135388
  proposed by: James Hunt (jamesodhunt)
  review: Needs Fixing - Steve Langasek (vorlon)
------------------------------------------------------------
revno: 1398 [merge]
fixes bug: https://launchpad.net/bugs/1079715
committer: Steve Langasek <[email protected]>
branch nick: upstream
timestamp: Thu 2012-12-06 08:40:17 -0800
message:
  Merge fix for bug #1079715
added:
  init/tests/data/
  init/tests/data/upstart-1.6.json
modified:
  ChangeLog
  dbus/com.ubuntu.Upstart.xml
  init/Makefile.am
  init/control.c
  init/control.h
  init/job_class.c
  init/session.h
  init/state.c
  init/state.h
  init/tests/test_job_process.c
  init/tests/test_state.c


--
lp:upstart
https://code.launchpad.net/~upstart-devel/upstart/trunk

Your team Upstart Reviewers is subscribed to branch lp:upstart.
To unsubscribe from this branch go to 
https://code.launchpad.net/~upstart-devel/upstart/trunk/+edit-subscription
=== modified file 'ChangeLog'
--- ChangeLog	2012-11-26 14:04:27 +0000
+++ ChangeLog	2012-12-06 16:40:17 +0000
@@ -1,3 +1,50 @@
+2012-12-06  James Hunt  <[email protected]>
+
+	* init/job_class.c:
+	  - job_class_add_safe(): Don't assert on name collisions for jobs
+	    associated with a different session.  (LP: #109715).
+	  - job_class_serialise(): Explicitly disallow user and chroot
+	    sessions from being serialised since this scenario is not
+	    supported (due to our not serialising ConfSource objects yet).
+	  - job_class_deserialise(): Assert that we do not have user and
+	    chroot sessions to deal with, and fix potential invalid free if
+	    error occurs before JobClass is created.
+	  - job_class_deserialise_all(): Explicitly ignore attempted
+	    deserialisation of user and chroot sessions.
+	* init/state.c:
+	  - state_deserialise_resolve_deps(): Ignore classes associated with
+	    a user or chroot session.  Specify new session parameter to
+	    state_get_job().
+	  - state_serialise_blocked(): Encode session index for BLOCKED_JOB.
+	    Make function non-static for testing.
+	  - state_deserialise_blocked(): Extract session from index index for
+	    BLOCKED_JOB to pass to state_get_job().  Default session to NULL
+	    to handle upstart 1.6 serialization.  Make function non-static
+	    for testing.
+	  - state_get_job(): Add @session parameter to allow exact job match.
+	  - state_read_objects(): Attempt to write the state to file
+	    STATE_FILE if deserialisation fails as an aid to diagnosing the
+	    cause of the failure.
+	* init/tests/test_state.c: test_blocking(): Additional tests to check
+	  that it is possible to deserialise Upstart 1.6 JSON format (which
+	  does not include the "session" JSON attribute for blocked objects.
+	  Add infrastructure for testing deserialization of reference json
+	  files from disk.
+	  New tests:
+	    - "BLOCKED_JOB serialisation and deserialisation".
+	    - "BLOCKED_EVENT serialisation and deserialisation".
+	    - "BLOCKED_JOB with JSON session object".
+	    - "ensure BLOCKED_JOB with non-NULL session is ignored".
+	* dbus/com.ubuntu.Upstart.xml: Added 'GetState' method that returns
+	  internal state in JSON format.
+	* init/Makefile.am:
+	  - Added TEST_DATA_DIR to allow tests to find data files.
+	  - Added test data files to distribution.
+	* init/control.c: control_get_state(): Implementation for D-Bus
+	  'GetState' method.
+	* init/tests/data/upstart-1.6.json: Test data used by test_state.c
+	  for upgrade testing the upstart 1.6 serialization format.
+
 2012-11-23  James Hunt  <[email protected]>
 
 	[ Colin King <[email protected]> ]

=== modified file 'dbus/com.ubuntu.Upstart.xml'
--- dbus/com.ubuntu.Upstart.xml	2012-09-09 21:22:06 +0000
+++ dbus/com.ubuntu.Upstart.xml	2012-12-04 16:09:18 +0000
@@ -34,6 +34,10 @@
       <arg name="jobs" type="ao" direction="out" />
     </method>
 
+    <method name="GetState">
+      <arg name="state" type="s" direction="out" />
+    </method>
+
     <!-- Signals for changes to the job list -->
     <signal name="JobAdded">
       <arg name="job" type="o" />

=== modified file 'init/Makefile.am'
--- init/Makefile.am	2012-11-18 05:57:58 +0000
+++ init/Makefile.am	2012-12-04 16:09:18 +0000
@@ -126,8 +126,15 @@
 	$(com_ubuntu_Upstart_Job_OUTPUTS) \
 	$(com_ubuntu_Upstart_Instance_OUTPUTS)
 
-
-EXTRA_DIST = init.supp
+TEST_DATA_DIR = $(PWD)/tests/data
+
+AM_CPPFLAGS += -DTEST_DATA_DIR="\"$(TEST_DATA_DIR)\""
+
+TEST_DATA_FILES = \
+	$(TEST_DATA_DIR)/upstart-1.6-blocked.json
+
+EXTRA_DIST = init.supp $(TEST_DATA_FILES)
+	
 
 test_util_SOURCES = \
 	tests/test_util.c tests/test_util.h

=== modified file 'init/control.c'
--- init/control.c	2012-09-20 15:16:52 +0000
+++ init/control.c	2012-12-06 09:40:25 +0000
@@ -949,3 +949,65 @@
 
 	return 0;
 }
+
+/**
+ * control_get_state:
+ *
+ * @data: not used,
+ * @message: D-Bus connection and message received,
+ * @state: output string returned to client.
+ *
+ * Convert internal state to JSON string.
+ *
+ * Returns: zero on success, negative value on raised error.
+ **/
+int
+control_get_state (void           *data,
+		   NihDBusMessage  *message,
+		   char           **state)
+{
+	Session  *session;
+	uid_t     uid;
+	size_t    len;
+
+	nih_assert (message);
+	nih_assert (state);
+
+	uid = getuid ();
+
+	/* 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 != uid) {
+		nih_dbus_error_raise_printf (
+			DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
+			_("You do not have permission to request state"));
+		return -1;
+	}
+
+	if (state_to_string (state, &len) < 0)
+		goto error;
+
+	nih_ref (*state, message);
+
+	return 0;
+
+error:
+	nih_dbus_error_raise_printf (DBUS_ERROR_NO_MEMORY,
+			_("Out of Memory"));
+	return -1;
+}

=== modified file 'init/control.h'
--- init/control.h	2012-09-11 14:59:40 +0000
+++ init/control.h	2012-12-04 16:09:18 +0000
@@ -105,6 +105,11 @@
 int control_bus_release_name (void)
 	__attribute__ ((warn_unused_result));
 
+int control_get_state (void           *data,
+		   NihDBusMessage  *message,
+		   char           **state)
+	__attribute__ ((warn_unused_result));
+
 NIH_END_EXTERN
 
 #endif /* INIT_CONTROL_H */

=== modified file 'init/job_class.c'
--- init/job_class.c	2012-11-14 14:47:19 +0000
+++ init/job_class.c	2012-11-22 16:32:36 +0000
@@ -271,9 +271,7 @@
 
 	/* If we found an entry, ensure we only consider the appropriate session */
 	while (registered && registered->session != class->session)
-	{
 		registered = (JobClass *)nih_hash_search (job_classes, class->name, &registered->entry);
-	}
 
 	if (registered != best) {
 		if (registered)
@@ -314,9 +312,7 @@
 
 	/* If we found an entry, ensure we only consider the appropriate session */
 	while (registered && registered->session != class->session)
-	{
 		registered = (JobClass *)nih_hash_search (job_classes, class->name, &registered->entry);
-	}
 
 	if (registered == class) {
 		if (class != best) {
@@ -364,7 +360,7 @@
  * @class: new class to select.
  *
  * Adds @class to the hash table iff no existing entry of the
- * same name exists.
+ * same name exists for the same session.
  **/
 void
 job_class_add_safe (JobClass *class)
@@ -376,7 +372,11 @@
 
 	control_init ();
 
-	existing = (JobClass *)nih_hash_search (job_classes, class->name, NULL);
+	/* Ensure no existing class exists for the same session */
+	do {
+		existing = (JobClass *)nih_hash_search (job_classes,
+				class->name, existing ? &existing->entry : NULL);
+	} while (existing && existing->session != class->session);
 
 	nih_assert (! existing);
 
@@ -1592,6 +1592,15 @@
 	json = json_object_new_object ();
 	if (! json)
 		return NULL;
+	
+	/* XXX: user and chroot jobs are not currently supported
+	 * due to ConfSources not currently being serialised.
+	 */
+	if (class->session) {
+		nih_info ("WARNING: serialisation of user jobs and "
+			"chroot sessions not currently supported");
+		goto error;
+	}
 
 	session_index = session_get_index (class->session);
 	if (session_index < 0)
@@ -1797,6 +1806,7 @@
 {
 	json_object    *json_normalexit;
 	JobClass       *class = NULL;
+	Session        *session;
 	int             session_index = -1;
 	int             ret;
 	nih_local char *name = NULL;
@@ -1814,21 +1824,24 @@
 	if (session_index < 0)
 		goto error;
 
+	session = session_from_index (session_index);
+
+	/* XXX: user and chroot jobs are not currently supported
+	 * due to ConfSources not currently being serialised.
+	 */
+	if (session) {
+		nih_info ("WARNING: deserialisation of user jobs and "
+			"chroot sessions not currently supported");
+		goto error;
+	}
+
 	if (! state_get_json_string_var_strict (json, "name", NULL, name))
 		goto error;
 
-	class = NIH_MUST (job_class_new (NULL, name,
-	                                 session_from_index (session_index)));
+	class = job_class_new (NULL, name, session);
 	if (! class)
 		goto error;
 
-	if (class->session != NULL) {
-		nih_warn ("XXX: WARNING (%s:%d): deserialisation of "
-				"user jobs and chroot sessions not currently supported",
-				__func__, __LINE__);
-		goto error;
-	}
-
 	/* job_class_new() sets path */
 	if (! state_get_json_string_var_strict (json, "path", NULL, path))
 		goto error;
@@ -2002,7 +2015,9 @@
 	return class;
 
 error:
-	nih_free (class);
+	if (class)
+		nih_free (class);
+
 	return NULL;
 }
 
@@ -2043,19 +2058,12 @@
 			goto error;
 
 		class = job_class_deserialise (json_class);
+
+		/* For parity with the serialisation code, don't treat
+		 * errors as fatal for the entire deserialisation.
+		 */
 		if (! class)
-			goto error;
-
-		/* FIXME:
-		 *
-		 * If user sessions exist (ie 'initctl --session list'
-		 * has been run), we get this failure:
-		 *
-		 *             serialised path='/com/ubuntu/Upstart/jobs/1000/bang'
-		 * path set by job_class_new()='/com/ubuntu/Upstart/jobs/_/1000/bang'
-		 *
-		 */
-
+			continue;
 	}
 
 	return 0;

=== modified file 'init/session.h'
--- init/session.h	2012-06-29 16:19:33 +0000
+++ init/session.h	2012-11-22 16:32:36 +0000
@@ -39,7 +39,8 @@
  * with a chroot path)).
  *
  * This structure is used to identify collections of jobs
- * that share either a common @chroot and/or common @user.
+ * that share either a common @chroot and/or common @user. Note that
+ * @conf_path is unique across all sessions.
  *
  * Summary of Session values for different environments:
  *

=== modified file 'init/state.c'
--- init/state.c	2012-11-23 11:36:47 +0000
+++ init/state.c	2012-12-06 16:40:17 +0000
@@ -2,7 +2,7 @@
  *
  * state.c - serialisation and deserialisation support.
  *
- * Copyright © 2012 Canonical Ltd.
+ * Copyright  2012 Canonical Ltd.
  * Author: James Hunt <[email protected]>.
  *
  * This program is free software; you can redistribute it and/or modify
@@ -48,7 +48,8 @@
 json_object *json_events = NULL;
 json_object *json_classes = NULL;
 
-extern int use_session_bus;
+extern int   use_session_bus;
+extern char *log_dir;
 
 /**
  * args_copy:
@@ -68,22 +69,17 @@
 int restart = FALSE;
 
 /* Prototypes for static functions */
-static json_object *
-state_serialise_blocked (const Blocked *blocked)
-	__attribute__ ((malloc, warn_unused_result));
-
-static Blocked *
-state_deserialise_blocked (void *parent, json_object *json, NihList *list)
-	__attribute__ ((malloc, warn_unused_result));
-
 static JobClass *
 state_index_to_job_class (int job_class_index)
 	__attribute__ ((warn_unused_result));
 
 static Job *
-state_get_job (const char *job_class, const char *job_name)
+state_get_job (const Session *session, const char *job_class,
+	       const char *job_name)
 	__attribute__ ((warn_unused_result));
 
+static void state_write_file (NihIoBuffer *buffer);
+
 /**
  * state_read:
  *
@@ -246,10 +242,59 @@
 	return 0;
 
 error:
+	/* Failed to reconstruct internal state so attempt to write
+	 * the JSON state data to a file to allow for manual post
+	 * re-exec analysis.
+	 */
+	if (buffer->len && log_dir)
+		state_write_file (buffer);
+
 	return -1;
 }
 
 /**
+ * state_write_file:
+ *
+ * @buffer: NihIoBuffer containing JSON data.
+ *
+ * Write JSON data contained in @buffer to STATE_FILE below log_dir.
+ *
+ * Failures are ignored since this is designed to be called in an error
+ * scenario anyway.
+ **/
+void
+state_write_file (NihIoBuffer *buffer)
+{
+	int              fd;
+	ssize_t          bytes;
+	nih_local char  *state_file = NULL;
+
+	nih_assert (buffer);
+
+	state_file = nih_sprintf (NULL, "%s/%s", log_dir, STATE_FILE);
+	if (! state_file)
+		return;
+
+	/* Note the very restrictive permissions */
+	fd = open (state_file, (O_CREAT|O_WRONLY|O_TRUNC), S_IRUSR);
+	if (fd < 0)
+		return;
+
+	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 != EINTR)
+			break;
+	}
+
+	close (fd);
+}
+
+/**
  * state_write_objects:
  *
  * @fd: file descriptor to write serialisation data on,
@@ -1139,6 +1184,12 @@
 		if (! class)
 			goto error;
 
+		/* XXX: user and chroot jobs are not currently supported
+		 * due to ConfSources not currently being serialised.
+		 */
+		if (class->session)
+			continue;
+
 		if (! state_get_json_var_full (json_class, "jobs", array, json_jobs))
 			goto error;
 
@@ -1164,7 +1215,7 @@
 				goto error;
 
 			/* lookup job */
-			job = state_get_job (class->name, job_name);
+			job = state_get_job (class->session, class->name, job_name);
 			if (! job)
 				goto error;
 
@@ -1200,11 +1251,12 @@
  *
  * Returns: JSON-serialised Blocked object, or NULL on error.
  **/
-static json_object *
+json_object *
 state_serialise_blocked (const Blocked *blocked)
 {
 	json_object  *json;
 	json_object  *json_blocked_data;
+	int           session_index;
 
 	nih_assert (blocked);
 
@@ -1228,6 +1280,18 @@
 						blocked->job->class->name))
 				goto error;
 
+			session_index = session_get_index (blocked->job->class->session);
+			if (session_index < 0)
+				goto error;
+
+			/* Encode parent classes session index to aid in
+			 * finding the correct job on deserialisation.
+			 */
+			if (! state_set_json_int_var (json_blocked_data,
+						"session",
+						session_index))
+				goto error;
+
 			if (! state_set_json_string_var (json_blocked_data,
 						"name",
 						blocked->job->name))
@@ -1389,7 +1453,7 @@
  *
  * Returns: new Blocked object, or NULL on error.
  **/
-static Blocked *
+Blocked *
 state_deserialise_blocked (void *parent, json_object *json,
 		NihList *list)
 {
@@ -1397,7 +1461,7 @@
 	Blocked         *blocked = NULL;
 	nih_local char  *blocked_type_str = NULL;
 	BlockedType      blocked_type;
-	int             ret;
+	int              ret;
 
 	nih_assert (parent);
 	nih_assert (json);
@@ -1421,6 +1485,8 @@
 			nih_local char  *job_name = NULL;
 			nih_local char  *job_class_name = NULL;
 			Job             *job;
+			Session         *session;
+			int              session_index;
 
 			if (! state_get_json_string_var_strict (json_blocked_data,
 						"name", NULL, job_name))
@@ -1430,7 +1496,19 @@
 						"class", NULL, job_class_name))
 				goto error;
 
-			job = state_get_job (job_class_name, job_name);
+			/* On error, assume NULL session since the likelihood
+			 * is we're upgrading from Upstart 1.6 that did not set
+			 * the 'session' JSON object.
+			 */
+			if (! state_get_json_int_var (json_blocked_data, "session", session_index))
+				session_index = 0;
+
+			if (session_index < 0)
+				goto error;
+
+			session = session_from_index (session_index);
+
+			job = state_get_job (session, job_class_name, job_name);
 			if (! job)
 				goto error;
 
@@ -1583,8 +1661,14 @@
 		if (! json_blocked)
 			goto error;
 
+
+		/* Don't error in this scenario to allow for possibility
+		 * that version of Upstart that performed the
+		 * serialisation did not correctly handle user and
+		 * chroot jobs.
+		 */
 		if (! state_deserialise_blocked (parent, json_blocked, list))
-			goto error;
+			;
 	}
 
 	return 0;
@@ -1625,6 +1709,7 @@
 /**
  * state_get_job:
  *
+ * @session: session of job class,
  * @job_class: name of job class,
  * @job_name: name of job instance.
  *
@@ -1635,15 +1720,21 @@
  * job not found.
  **/
 static Job *
-state_get_job (const char *job_class, const char *job_name)
+state_get_job (const Session *session,
+	       const char *job_class,
+	       const char *job_name)
 {
-	JobClass  *class;
+	JobClass  *class = NULL;
 	Job       *job;
 
 	nih_assert (job_class);
 	nih_assert (job_classes);
 
-	class = (JobClass *)nih_hash_lookup (job_classes, job_class);
+	do {
+		class = (JobClass *)nih_hash_search (job_classes,
+				job_class, class ? &class->entry : NULL);
+	} while (class && class->session != session);
+
 	if (! class)
 		goto error;
 

=== modified file 'init/state.h'
--- init/state.h	2012-11-14 14:47:19 +0000
+++ init/state.h	2012-12-04 16:09:18 +0000
@@ -367,6 +367,18 @@
 #define STATE_WAIT_SECS_ENV "UPSTART_STATE_WAIT_SECS"
 
 /**
+ * STATE_FILE:
+ *
+ * Name of file that is written below the job log directory if the
+ * newly re-exec'ed init instance failed to understand the JSON sent to
+ * it by the old instance.
+ *
+ * This could happen for example if the old instance generated invalid
+ * JSON, or JSON in an unexected format.
+ **/
+#define STATE_FILE "upstart.state"
+
+/**
  * state_get_timeout:
  *
  * @var: name of long integer var to set to timeout value.

=== added directory 'init/tests/data'
=== added file 'init/tests/data/upstart-1.6.json'
--- init/tests/data/upstart-1.6.json	1970-01-01 00:00:00 +0000
+++ init/tests/data/upstart-1.6.json	2012-12-04 16:09:18 +0000
@@ -0,0 +1,1 @@
+{ "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 } ] }

=== modified file 'init/tests/test_job_process.c'
--- init/tests/test_job_process.c	2012-12-06 13:36:09 +0000
+++ init/tests/test_job_process.c	2012-12-06 16:40:17 +0000
@@ -175,10 +175,8 @@
        const char       *filename)
 {
 	FILE  *out;
-	FILE  *in;
 	char   tmpname[PATH_MAX], path[PATH_MAX];
 	int    i;
-	char   buffer[1024];
 	int    ret = EXIT_SUCCESS;
 
 	strcpy (tmpname, filename);

=== modified file 'init/tests/test_state.c'
--- init/tests/test_state.c	2012-11-14 14:47:19 +0000
+++ init/tests/test_state.c	2012-12-04 16:09:18 +0000
@@ -50,11 +50,27 @@
 #include "control.h"
 #include "test_util.h"
 
+#ifndef TEST_DATA_DIR
+#error ERROR: TEST_DATA_DIR not defined
+#endif
+
+/* These functions are 'protected'.
+ *
+ * The test code needs access, but they cannot be public due to
+ * header-file complications.
+ */
+json_object *
+state_serialise_blocked (const Blocked *blocked)
+	__attribute__ ((malloc, warn_unused_result));
+
+Blocked *
+state_deserialise_blocked (void *parent, json_object *json, NihList *list)
+	__attribute__ ((malloc, warn_unused_result));
 
 /**
  * AlreadySeen:
  *
- * Used to allow objects that directly or indirectly reference on
+ * Used to allow objects that directly or indirectly reference
  * another to be inspected and compared without causing infinite
  * recursion.
  *
@@ -127,6 +143,43 @@
 int blocked_diff (const Blocked *a, const Blocked *b, AlreadySeen seen)
 	__attribute__ ((warn_unused_result));
 
+void test_upstart1_6_upgrade (const char *conf_file, const char *path);
+
+/**
+ * TestDataFile:
+ *
+ * @conf_file: Name of ConfFile that must be created prior to
+ *  deserialising JSON data in @filename.
+ * @filename: basename of data file,
+ * @func: function to run to test @filename.
+ *
+ * Representation of a JSON data file used for ensuring that the current
+ * version of Upstart is able to deserialise all previous JSON data file
+ * format versions.
+ *
+ * @conf_file is required since we do not currently serialise ConfFile
+ * and ConfSource objects so these entities must be created immediately
+ * prior to attempting deserialisation.
+ *
+ * @func returns nothing so is expected to assert on any error.
+ **/
+typedef struct test_data_file {
+	char		*conf_file;
+	char		*filename;
+		  void (*func) (const char *conf_file, const char *path);
+} TestDataFile;
+
+/**
+ * test_data_files:
+ *
+ * Array of data files to test.
+ **/
+TestDataFile test_data_files[] = {
+	{ "bar", "upstart-1.6.json", test_upstart1_6_upgrade },
+
+	{ NULL, NULL, NULL }
+};
+
 /* Data with some embedded nulls */
 const    char test_data[] = {
 	'h', 'e', 'l', 'l', 'o', 0x0, 0x0, 0x0, ' ', 'w', 'o', 'r', 'l', 'd', '\n', '\r', '\0'
@@ -139,7 +192,7 @@
 int64_t  values64[] = {INT64_MIN, -1, 0, 1, INT64_MAX};
 const Process test_procs[] = {
 	{ 0, "echo hello" },
-	{ 1, "echo hello" },
+	{ 1, "echo hello" }
 };
 rlim_t rlimit_values[] = { 0, 1, 2, 3, 7, RLIM_INFINITY };
 
@@ -993,17 +1046,23 @@
 void
 test_blocking (void)
 {
-	nih_local char        *json_string = NULL;
-	ConfSource            *source = NULL;
-	ConfFile              *file;
-	JobClass              *class;
-	JobClass              *new_class;
-	Job                   *job;
-	Job                   *new_job;
-	Event                 *event;
-	Event                 *new_event;
-	Blocked               *blocked;
-	size_t                 len;
+	nih_local char          *json_string = NULL;
+	nih_local char          *parent_str = NULL;
+	ConfSource              *source = NULL;
+	ConfFile                *file;
+	JobClass                *class;
+	JobClass                *new_class;
+	Job                     *job;
+	Job                     *new_job;
+	Event                   *event;
+	Event                   *new_event;
+	Blocked                 *blocked;
+	Blocked                 *new_blocked;
+	NihList                  blocked_list;
+	size_t                   len;
+	json_object             *json_blocked;
+	Session                 *session;
+	Session                 *new_session;
 
 	conf_init ();
 	session_init ();
@@ -1014,6 +1073,278 @@
 	TEST_GROUP ("Blocked serialisation and deserialisation");
 
 	/*******************************/
+	TEST_FEATURE ("BLOCKED_JOB serialisation and deserialisation");
+
+	nih_list_init (&blocked_list);
+	TEST_LIST_EMPTY (&blocked_list);
+
+	source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
+	TEST_NE_P (source, NULL);
+
+	file = conf_file_new (source, "/tmp/foo/bar");
+	TEST_NE_P (file, NULL);
+	class = file->job = job_class_new (file, "bar", NULL);
+
+	TEST_NE_P (class, NULL);
+	TEST_HASH_EMPTY (job_classes);
+	TEST_TRUE (job_class_consider (class));
+	TEST_HASH_NOT_EMPTY (job_classes);
+
+	job = job_new (class, "");
+	TEST_NE_P (job, NULL);
+
+	parent_str = nih_strdup (NULL, "parent");
+	TEST_NE_P (parent_str, NULL);
+
+	blocked = blocked_new (NULL, BLOCKED_JOB, job);
+	TEST_NE_P (blocked, NULL);
+
+	json_blocked = state_serialise_blocked (blocked);
+	TEST_NE_P (json_blocked, NULL);
+
+	new_blocked = state_deserialise_blocked (parent_str,
+			json_blocked, &blocked_list);
+	TEST_NE_P (new_blocked, NULL);
+	TEST_LIST_NOT_EMPTY (&blocked_list);
+
+	assert0 (blocked_diff (blocked, new_blocked, ALREADY_SEEN_SET));
+
+	json_object_put (json_blocked);
+	nih_free (source);
+
+	/*******************************/
+	TEST_FEATURE ("BLOCKED_EVENT serialisation and deserialisation");
+
+	event = event_new (NULL, "event", NULL);
+	TEST_NE_P (event, NULL);
+
+	nih_list_init (&blocked_list);
+
+	source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
+	TEST_NE_P (source, NULL);
+
+	file = conf_file_new (source, "/tmp/foo/bar");
+	TEST_NE_P (file, NULL);
+	class = file->job = job_class_new (file, "bar", NULL);
+
+	TEST_NE_P (class, NULL);
+	TEST_HASH_EMPTY (job_classes);
+	TEST_TRUE (job_class_consider (class));
+	TEST_HASH_NOT_EMPTY (job_classes);
+
+	job = job_new (class, "");
+	TEST_NE_P (job, NULL);
+
+	TEST_LIST_EMPTY (&job->blocking);
+
+	blocked = blocked_new (NULL, BLOCKED_EVENT, event);
+	TEST_NE_P (blocked, NULL);
+
+	nih_list_add (&job->blocking, &blocked->entry);
+	TEST_LIST_NOT_EMPTY (&job->blocking);
+
+	event->blockers = 1;
+
+	parent_str = nih_strdup (NULL, "parent");
+	TEST_NE_P (parent_str, NULL);
+
+	json_blocked = state_serialise_blocked (blocked);
+	TEST_NE_P (json_blocked, NULL);
+
+	TEST_LIST_EMPTY (&blocked_list);
+	new_blocked = state_deserialise_blocked (parent_str,
+			json_blocked, &blocked_list);
+	TEST_NE_P (new_blocked, NULL);
+	TEST_LIST_NOT_EMPTY (&blocked_list);
+
+	assert0 (blocked_diff (blocked, new_blocked, ALREADY_SEEN_SET));
+
+	json_object_put (json_blocked);
+	nih_free (source);
+	nih_free (event);
+
+	/*******************************/
+	/* Test Upstart 1.6+ behaviour
+	 *
+	 * The data serialisation format for this version now includes
+	 * a "session" entry in the JSON for the blocked job.
+	 *
+	 * Note that this test is NOT testing whether a JobClass with an
+	 * associated Upstart session is handled correctly, it is merely
+	 * testing that a JobClass with the NULL session is handled
+	 * correctly!
+	 */
+	TEST_FEATURE ("BLOCKED_JOB with JSON session object");
+
+	TEST_LIST_EMPTY (sessions);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	event = event_new (NULL, "Christmas", NULL);
+	TEST_NE_P (event, NULL);
+	TEST_LIST_EMPTY (&event->blocking);
+
+	source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
+	TEST_NE_P (source, NULL);
+
+	file = conf_file_new (source, "/tmp/foo/bar");
+	TEST_NE_P (file, NULL);
+	/* Create class with NULL session */
+	class = file->job = job_class_new (NULL, "bar", NULL);
+
+	TEST_NE_P (class, NULL);
+	TEST_HASH_EMPTY (job_classes);
+	TEST_TRUE (job_class_consider (class));
+	TEST_HASH_NOT_EMPTY (job_classes);
+
+	job = job_new (class, "");
+	TEST_NE_P (job, NULL);
+	TEST_HASH_NOT_EMPTY (class->instances);
+
+	blocked = blocked_new (event, BLOCKED_JOB, job);
+	TEST_NE_P (blocked, NULL);
+
+	nih_list_add (&event->blocking, &blocked->entry);
+	job->blocker = event;
+
+	TEST_LIST_NOT_EMPTY (&event->blocking);
+
+	assert0 (state_to_string (&json_string, &len));
+	TEST_GT (len, 0);
+
+	/* XXX: We don't remove the source as these are not
+	 * recreated on re-exec, so we'll re-use the existing one.
+	 */
+	nih_list_remove (&class->entry);
+	nih_list_remove (&event->entry);
+
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_NOT_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	assert0 (state_from_string (json_string));
+
+	TEST_LIST_NOT_EMPTY (conf_sources);
+	TEST_LIST_NOT_EMPTY (events);
+	TEST_HASH_NOT_EMPTY (job_classes);
+	TEST_LIST_EMPTY (sessions);
+
+	new_class = (JobClass *)nih_hash_lookup (job_classes, "bar");
+	TEST_NE_P (new_class, NULL);
+	nih_list_remove (&new_class->entry);
+
+	/* Upstart 1.6 can only deserialise the NULL session */
+	TEST_EQ_P (class->session, NULL);
+
+	new_event = (Event *)nih_list_remove (events->next);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_NOT_EMPTY (&new_event->blocking);
+	assert0 (event_diff (event, new_event, ALREADY_SEEN_SET));
+
+	nih_free (event);
+	nih_free (new_event);
+	nih_free (source);
+	nih_free (new_class);
+
+	TEST_HASH_EMPTY (job_classes);
+
+	TEST_LIST_EMPTY (sessions);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	/*******************************/
+	/* We don't currently handle user+chroot jobs, so let's assert
+	 * that behaviour.
+	 */
+	TEST_FEATURE ("ensure BLOCKED_JOB with non-NULL session is ignored");
+
+	TEST_LIST_EMPTY (sessions);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	session = session_new (NULL, "/my/session", getuid ());
+	TEST_NE_P (session, NULL);
+	session->conf_path = NIH_MUST (nih_strdup (session, "/lives/here"));
+	TEST_LIST_NOT_EMPTY (sessions);
+
+	/* We simulate a user job being blocked by a system event, hence
+	 * the session is not associated with the event.
+	 */
+	event = event_new (NULL, "Christmas", NULL);
+	TEST_NE_P (event, NULL);
+	TEST_LIST_EMPTY (&event->blocking);
+
+	source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
+	source->session = session;
+	TEST_NE_P (source, NULL);
+
+	file = conf_file_new (source, "/tmp/foo/bar");
+	TEST_NE_P (file, NULL);
+
+	/* Create class with non-NULL session, simulating a user job */
+	class = file->job = job_class_new (NULL, "bar", session);
+	TEST_NE_P (class, NULL);
+
+	TEST_HASH_EMPTY (job_classes);
+	TEST_TRUE (job_class_consider (class));
+	TEST_HASH_NOT_EMPTY (job_classes);
+
+	job = job_new (class, "");
+	TEST_NE_P (job, NULL);
+	TEST_HASH_NOT_EMPTY (class->instances);
+
+	blocked = blocked_new (event, BLOCKED_JOB, job);
+	TEST_NE_P (blocked, NULL);
+
+	nih_list_add (&event->blocking, &blocked->entry);
+	job->blocker = event;
+
+	TEST_LIST_NOT_EMPTY (&event->blocking);
+
+	assert0 (state_to_string (&json_string, &len));
+	TEST_GT (len, 0);
+
+	/* XXX: We don't remove the source as these are not
+	 * recreated on re-exec, so we'll re-use the existing one.
+	 */
+	nih_list_remove (&class->entry);
+	nih_free (event);
+	nih_list_remove (&session->entry);
+
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_NOT_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	assert0 (state_from_string (json_string));
+
+	TEST_LIST_NOT_EMPTY (conf_sources);
+	TEST_LIST_NOT_EMPTY (events);
+
+	/* We don't expect any job_classes since the serialised one
+	 * related to a user session.
+	 */
+	TEST_HASH_EMPTY (job_classes);
+
+	/* However, the session itself will exist */
+	TEST_LIST_NOT_EMPTY (sessions);
+
+	new_session = (Session *)nih_list_remove (sessions->next);
+
+	nih_free (session);
+	nih_free (new_session);
+	event = (Event *)nih_list_remove (events->next);
+	nih_free (event);
+	nih_free (source);
+
+	TEST_LIST_EMPTY (sessions);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	/*******************************/
 	TEST_FEATURE ("event blocking a job");
 
 	TEST_LIST_EMPTY (sessions);
@@ -2608,6 +2939,141 @@
 	/*******************************/
 }
 
+/**
+ * test_upgrade:
+ * 
+ * Run tests that simulate an upgrade by attempting to deserialise an
+ * older version of the JSON data format than is currently used.
+ **/
+void
+test_upgrade (void)
+{
+	TestDataFile  *datafile;
+
+	TEST_GROUP ("upgrade tests");
+
+	for (datafile = test_data_files; datafile && datafile->filename; datafile++) {
+		nih_local char  *path = NULL;
+		nih_local char  *name = NULL;
+
+		nih_assert (datafile->func != NULL);
+
+		name = NIH_MUST (nih_sprintf (NULL, "with data file '%s'",
+					datafile->filename));
+		TEST_FEATURE (name);
+
+		path = NIH_MUST (nih_sprintf (NULL, "%s/%s",
+					TEST_DATA_DIR, datafile->filename));
+
+		datafile->func (datafile->conf_file, path);
+	}
+}
+
+/**
+ * test_upstart1_6_upgrade:
+ *
+ * @conf_file: name of ConfFile to create prior to running test,
+ * @path: full path to JSON data file to deserialise.
+ *
+ * Test for original Upstart 1.6 serialisation data format containing
+ * a blocked object that does not contain a 'session' element.
+ *
+ * Note that this test is NOT testing whether a JobClass with an
+ * associated Upstart session is handled correctly, it is merely
+ * testing that a JobClass with the NULL session encoded in the JSON
+ * is handled correctly.
+ **/
+void
+test_upstart1_6_upgrade (const char *conf_file, const char *path)
+{
+	nih_local char  *json_string = NULL;
+	Event           *event;
+	ConfSource      *source;
+	ConfFile        *file;
+	nih_local char  *conf_file_path = NULL;
+	struct stat      statbuf;
+	size_t           len;
+
+	nih_assert (conf_file);
+	nih_assert (path);
+
+	conf_init ();
+	session_init ();
+	event_init ();
+	control_init ();
+	job_class_init ();
+
+	TEST_LIST_EMPTY (sessions);
+	TEST_LIST_EMPTY (events);
+	TEST_LIST_EMPTY (conf_sources);
+	TEST_HASH_EMPTY (job_classes);
+
+	/* Check data file exists */
+	TEST_EQ (stat (path, &statbuf), 0);
+
+	json_string = nih_file_read (NULL, path, &len);
+	TEST_NE_P (json_string, NULL);
+
+	/* Create the ConfSource and ConfFile objects to simulate
+	 * Upstart reading /etc/init on startup. Required since we
+	 * don't currently serialise these objects.
+	 */
+	source = conf_source_new (NULL, "/tmp/foo", CONF_JOB_DIR);
+	TEST_NE_P (source, NULL);
+
+	conf_file_path = NIH_MUST (nih_sprintf (NULL, "%s/%s",
+				"/tmp/foo", conf_file));
+
+	file = conf_file_new (source, conf_file_path);
+	TEST_NE_P (file, NULL);
+
+	/* Recreate state from JSON data file */
+	assert0 (state_from_string (json_string));
+
+	TEST_LIST_NOT_EMPTY (conf_sources);
+	TEST_LIST_NOT_EMPTY (events);
+	TEST_HASH_NOT_EMPTY (job_classes);
+	TEST_LIST_EMPTY (sessions);
+
+	event = (Event *)nih_list_remove (events->next);
+	TEST_NE_P (event, NULL);
+	TEST_EQ_STR (event->name, "Christmas");
+
+	NIH_HASH_FOREACH (job_classes, iter) {
+		JobClass *class = (JobClass *)iter;
+
+		TEST_EQ_STR (class->name, "bar");
+		TEST_EQ_STR (class->path, "/com/ubuntu/Upstart/jobs/bar");
+		TEST_HASH_NOT_EMPTY (class->instances);
+
+		NIH_HASH_FOREACH (class->instances, iter2) {
+			Blocked        *blocked;
+			Job            *job = (Job *)iter2;
+			nih_local char *instance_path = NULL;
+
+			/* instance name */
+			TEST_EQ_STR (job->name, "");
+
+			instance_path = NIH_MUST (nih_sprintf (NULL, "%s/_", class->path));
+			TEST_EQ_STR (job->path, instance_path);
+
+			/* job is blocked by the event */
+			TEST_EQ (job->blocker, event);
+
+			/* First entry in list should be a Blocked
+			 * object pointing to the job.
+			 */
+			TEST_LIST_NOT_EMPTY (&event->blocking);
+			blocked = (Blocked *)(&event->blocking)->next;
+			TEST_EQ (blocked->type, BLOCKED_JOB);
+			TEST_EQ (blocked->job, job);
+		}
+	}
+
+	nih_free (event);
+	nih_free (conf_sources);
+}
+
 int
 main (int   argc,
       char *argv[])
@@ -2632,6 +3098,7 @@
 	test_log_serialise ();
 	test_job_serialise ();
 	test_job_class_serialise ();
+	test_upgrade ();
 
 	return 0;
 }

-- 
upstart-devel mailing list
[email protected]
Modify settings or unsubscribe at: 
https://lists.ubuntu.com/mailman/listinfo/upstart-devel

Reply via email to