Skip to content

Instantly share code, notes, and snippets.

@lstoll
Last active August 28, 2015 15:29
Show Gist options
  • Save lstoll/2a88cbfa1f84fdcc1511 to your computer and use it in GitHub Desktop.
Save lstoll/2a88cbfa1f84fdcc1511 to your computer and use it in GitHub Desktop.
/* upstart
*
* control.c - D-Bus connections, objects and methods
*
* Copyright 2009-2011 Canonical Ltd.
* Author: Scott James Remnant <scott@netsplit.com>.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <dbus/dbus.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/string.h>
#include <nih/list.h>
#include <nih/io.h>
#include <nih/main.h>
#include <nih/logging.h>
#include <nih/error.h>
#include <nih/errors.h>
#include <nih-dbus/dbus_error.h>
#include <nih-dbus/dbus_connection.h>
#include <nih-dbus/dbus_message.h>
#include <nih-dbus/dbus_object.h>
#include "dbus/upstart.h"
#include "environ.h"
#include "session.h"
#include "job_class.h"
#include "job.h"
#include "blocked.h"
#include "conf.h"
#include "control.h"
#include "errors.h"
#include "state.h"
#include "event.h"
#include "events.h"
#include "paths.h"
#include "xdg.h"
#include "com.ubuntu.Upstart.h"
#ifdef ENABLE_CGROUPS
#include "cgroup.h"
#endif /* ENABLE_CGROUPS */
/* Prototypes for static functions */
static int control_server_connect (DBusServer *server, DBusConnection *conn);
static void control_disconnected (DBusConnection *conn);
static void control_register_all (DBusConnection *conn);
static void control_bus_flush (void);
static int control_get_origin_uid (NihDBusMessage *message, uid_t *uid)
__attribute__ ((warn_unused_result));
static int control_check_permission (NihDBusMessage *message)
__attribute__ ((warn_unused_result));
static void control_session_file_create (void);
static void control_session_file_remove (void);
/**
* use_session_bus:
*
* If TRUE, connect to the D-Bus session bus rather than the system bus.
*
* Used for testing to simulate (as far as possible) a system-like init
* when running as a non-priv user (but not as a Session Init).
**/
int use_session_bus = FALSE;
/**
* dbus_bus_type:
*
* Type of D-Bus bus to connect to.
**/
DBusBusType dbus_bus_type;
/**
* control_server_address:
*
* Address on which the control server may be reached.
**/
char *control_server_address = NULL;
/**
* control_server:
*
* D-Bus server listening for new direct connections.
**/
DBusServer *control_server = NULL;
/**
* control_bus_address:
*
* Address on which the control bus may be reached.
**/
char *control_bus_address = NULL;
/**
* control_bus:
*
* Open connection to a D-Bus bus. The connection may be opened with
* control_bus_open() and if lost will become NULL.
**/
DBusConnection *control_bus = NULL;
/**
* control_conns:
*
* Open control connections, including the connection to a D-Bus
* bus and any private client connections.
**/
NihList *control_conns = NULL;
/* External definitions */
extern int user_mode;
extern int disable_respawn;
extern char *session_file;
/**
* control_init:
*
* Initialise the control connections list.
**/
void
control_init (void)
{
if (! control_conns)
control_conns = NIH_MUST (nih_list_new (NULL));
if (! control_server_address) {
if (user_mode) {
NIH_MUST (nih_strcat_sprintf (&control_server_address, NULL,
"%s-session/%d/%d", DBUS_ADDRESS_UPSTART, getuid (), getpid ()));
control_session_file_create ();
} else {
control_server_address = NIH_MUST (nih_strdup (NULL, DBUS_ADDRESS_UPSTART));
}
}
}
/**
* control_cleanup:
*
* Perform cleanup operations.
**/
void
control_cleanup (void)
{
control_session_file_remove ();
}
/**
* control_server_open:
*
* Open a listening D-Bus server and store it in the control_server global.
* New connections are permitted from the root user, and handled
* automatically in the main loop.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_server_open (void)
{
DBusServer *server;
nih_assert (control_server == NULL);
control_init ();
server = nih_dbus_server (control_server_address,
control_server_connect,
control_disconnected);
if (! server)
return -1;
control_server = server;
return 0;
}
/**
* control_server_connect:
*
* Called when a new client connects to our server and is used to register
* objects on the new connection.
*
* Returns: always TRUE.
**/
static int
control_server_connect (DBusServer *server,
DBusConnection *conn)
{
NihListEntry *entry;
nih_assert (server != NULL);
nih_assert (server == control_server);
nih_assert (conn != NULL);
nih_info (_("Connection from private client"));
/* Register objects on the connection. */
control_register_all (conn);
/* Add the connection to the list */
entry = NIH_MUST (nih_list_entry_new (NULL));
entry->data = conn;
nih_list_add (control_conns, &entry->entry);
return TRUE;
}
/**
* control_server_close:
*
* Close the connection to the D-Bus system bus. Since the connection is
* shared inside libdbus, this really only drops our reference to it so
* it's possible to have method and signal handlers called even after calling
* this (normally to dispatch what's in the queue).
**/
void
control_server_close (void)
{
if (! control_server)
return;
dbus_server_disconnect (control_server);
dbus_server_unref (control_server);
control_server = NULL;
}
/**
* control_bus_open:
*
* Open a connection to the appropriate D-Bus bus and store it in the
* control_bus global. The connection is handled automatically
* in the main loop.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_bus_open (void)
{
DBusConnection *conn;
DBusError error;
NihListEntry *entry;
int ret;
nih_assert (control_bus == NULL);
dbus_error_init (&error);
control_init ();
dbus_bus_type = control_get_bus_type ();
/* Connect to the appropriate D-Bus bus and hook everything up into
* our own main loop automatically.
*/
if (user_mode && control_bus_address) {
conn = nih_dbus_connect (control_bus_address, control_disconnected);
if (! conn)
return -1;
if (! dbus_bus_register (conn, &error)) {
nih_dbus_error_raise (error.name, error.message);
dbus_error_free (&error);
return -1;
}
nih_debug ("Connected to notified D-Bus bus");
} else {
conn = nih_dbus_bus (use_session_bus ? DBUS_BUS_SESSION : DBUS_BUS_SYSTEM,
control_disconnected);
if (! conn)
return -1;
nih_debug ("Connected to D-Bus %s bus",
dbus_bus_type == DBUS_BUS_SESSION
? "session" : "system");
}
/* Register objects on the bus. */
control_register_all (conn);
/* Request our well-known name. We do this last so that once it
* appears on the bus, clients can assume we're ready to talk to
* them.
*/
ret = dbus_bus_request_name (conn, DBUS_SERVICE_UPSTART,
DBUS_NAME_FLAG_DO_NOT_QUEUE, &error);
if (ret < 0) {
/* Error while requesting the name */
nih_dbus_error_raise (error.name, error.message);
dbus_error_free (&error);
dbus_connection_unref (conn);
return -1;
} else if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
/* Failed to obtain the name (already taken usually) */
nih_error_raise (CONTROL_NAME_TAKEN,
_(CONTROL_NAME_TAKEN_STR));
dbus_connection_unref (conn);
return -1;
}
/* Add the connection to the list */
entry = NIH_MUST (nih_list_entry_new (NULL));
entry->data = conn;
nih_list_add (control_conns, &entry->entry);
control_bus = conn;
return 0;
}
/**
* control_bus_close:
*
* Close the connection to the D-Bus system bus. Since the connection is
* shared inside libdbus, this really only drops our reference to it so
* it's possible to have method and signal handlers called even after calling
* this (normally to dispatch what's in the queue).
**/
void
control_bus_close (void)
{
nih_assert (control_bus != NULL);
dbus_connection_unref (control_bus);
control_disconnected (control_bus);
}
/**
* control_disconnected:
*
* This function is called when the connection to the D-Bus system bus,
* or a client connection to our D-Bus server, is dropped and our reference
* is about to be lost. We clear the connection from our current list
* and drop the control_bus global if relevant.
**/
static void
control_disconnected (DBusConnection *conn)
{
nih_assert (conn != NULL);
if (conn == control_bus) {
DBusError error;
dbus_error_init (&error);
if (user_mode && control_bus_address) {
nih_warn (_("Disconnected from notified D-Bus bus"));
} else {
nih_warn (_("Disconnected from D-Bus %s bus"),
dbus_bus_type == DBUS_BUS_SESSION
? "session" : "system");
}
control_bus = NULL;
}
/* Remove from the connections list */
NIH_LIST_FOREACH_SAFE (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
if (entry->data == conn)
nih_free (entry);
}
}
/**
* control_register_all:
* @conn: connection to register objects for.
*
* Registers the manager object and objects for all jobs and instances on
* the given connection.
**/
static void
control_register_all (DBusConnection *conn)
{
nih_assert (conn != NULL);
job_class_init ();
/* Register the manager object, this is the primary point of contact
* for clients. We only check for success, otherwise we're happy
* to let this object be tied to the lifetime of the connection.
*/
NIH_MUST (nih_dbus_object_new (NULL, conn, DBUS_PATH_UPSTART,
control_interfaces, NULL));
/* Register objects for each currently registered job and its
* instances.
*/
NIH_HASH_FOREACH (job_classes, iter) {
JobClass *class = (JobClass *)iter;
job_class_register (class, conn, FALSE);
}
}
/**
* control_reload_configuration:
* @data: not used,
* @message: D-Bus connection and message received.
*
* Implements the ReloadConfiguration method of the com.ubuntu.Upstart
* interface.
*
* Called to request that Upstart reloads its configuration from disk,
* useful when inotify is not available or the user is generally paranoid.
*
* Notes: chroot sessions are permitted to make this call.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_reload_configuration (void *data,
NihDBusMessage *message)
{
nih_assert (message != NULL);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to reload configuration"));
return -1;
}
nih_info (_("Reloading configuration"));
/* This can only be called after deserialisation */
conf_reload ();
return 0;
}
/**
* control_get_job_by_name:
* @data: not used,
* @message: D-Bus connection and message received,
* @name: name of job to get,
* @job: pointer for object path reply.
*
* Implements the GetJobByName method of the com.ubuntu.Upstart
* interface.
*
* Called to obtain the path to a D-Bus object for the job named @name,
* which will be stored in @job. If no job class with that name exists,
* the com.ubuntu.Upstart.Error.UnknownJob D-Bus error will be raised.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_get_job_by_name (void *data,
NihDBusMessage *message,
const char *name,
char **job)
{
Session *session;
JobClass *class = NULL;
JobClass *global_class = NULL;
nih_assert (message != NULL);
nih_assert (name != NULL);
nih_assert (job != NULL);
job_class_init ();
/* Verify that the name is valid */
if (! strlen (name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Name may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Lookup the job */
class = (JobClass *)nih_hash_search (job_classes, name, NULL);
while (class && (class->session != session)) {
/* Found a match in the global session which may be used
* later if no matching user session job exists.
*/
if ((! class->session) && (session && ! session->chroot))
global_class = class;
class = (JobClass *)nih_hash_search (job_classes, name,
&class->entry);
}
/* If no job with the given name exists in the appropriate
* session, look in the global namespace (aka the NULL session).
*/
if (! class)
class = global_class;
if (! class) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.UnknownJob",
_("Unknown job: %s"), name);
return -1;
}
/* Copy the path */
*job = nih_strdup (message, class->path);
if (! *job)
nih_return_system_error (-1);
return 0;
}
/**
* control_get_all_jobs:
* @data: not used,
* @message: D-Bus connection and message received,
* @jobs: pointer for array of object paths reply.
*
* Implements the GetAllJobs method of the com.ubuntu.Upstart
* interface.
*
* Called to obtain the paths of all known jobs, which will be stored in
* @jobs. If no jobs are registered, @jobs will point to an empty array.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_get_all_jobs (void *data,
NihDBusMessage *message,
char ***jobs)
{
Session *session;
char **list;
size_t len;
nih_assert (message != NULL);
nih_assert (jobs != NULL);
job_class_init ();
len = 0;
list = nih_str_array_new (message);
if (! list)
nih_return_system_error (-1);
/* Get the relevant session */
session = session_from_dbus (NULL, message);
NIH_HASH_FOREACH (job_classes, iter) {
JobClass *class = (JobClass *)iter;
if ((class->session || (session && session->chroot))
&& (class->session != session))
continue;
if (! nih_str_array_add (&list, message, &len,
class->path)) {
nih_error_raise_system ();
nih_free (list);
return -1;
}
}
*jobs = list;
return 0;
}
int
control_emit_event (void *data,
NihDBusMessage *message,
const char *name,
char * const *env,
int wait)
{
return control_emit_event_with_file (data, message, name, env, wait, -1);
}
/**
* control_emit_event_with_file:
* @data: not used,
* @message: D-Bus connection and message received,
* @name: name of event to emit,
* @env: environment of environment,
* @wait: whether to wait for event completion before returning,
* @file: file descriptor.
*
* Implements the top half of the EmitEvent method of the com.ubuntu.Upstart
* interface, the bottom half may be found in event_finished().
*
* Called to emit an event with a given @name and @env, which will be
* added to the event queue and processed asynchronously. If @name or
* @env are not valid, the org.freedesktop.DBus.Error.InvalidArgs D-Bus
* error will be returned immediately. If the event fails, the
* com.ubuntu.Upstart.Error.EventFailed D-Bus error will be returned when
* the event finishes.
*
* When @wait is TRUE the method call will not return until the event
* has completed, which means that all jobs affected by the event have
* finished starting (running for tasks) or stopping; when @wait is FALSE,
* the method call returns once the event has been queued.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_emit_event_with_file (void *data,
NihDBusMessage *message,
const char *name,
char * const *env,
int wait,
int file)
{
Event *event;
Blocked *blocked;
nih_assert (message != NULL);
nih_assert (name != NULL);
nih_assert (env != NULL);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to emit an event"));
return -1;
}
/* Verify that the name is valid */
if (! strlen (name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Name may not be empty string"));
close (file);
return -1;
}
/* Verify that the environment is valid */
if (! environ_all_valid (env)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Env must be KEY=VALUE pairs"));
close (file);
return -1;
}
/* Make the event and block the message on it */
event = event_new (NULL, name, (char **)env);
if (! event) {
nih_error_raise_system ();
close (file);
return -1;
}
event->fd = file;
if (event->fd >= 0) {
long flags;
flags = fcntl (event->fd, F_GETFD);
flags &= ~FD_CLOEXEC;
fcntl (event->fd, F_SETFD, flags);
}
/* Obtain the session */
event->session = session_from_dbus (NULL, message);
if (wait) {
blocked = blocked_new (event, BLOCKED_EMIT_METHOD, message);
if (! blocked) {
nih_error_raise_system ();
nih_free (event);
close (file);
return -1;
}
nih_list_add (&event->blocking, &blocked->entry);
} else {
NIH_ZERO (control_emit_event_reply (message));
}
return 0;
}
/**
* control_get_version:
* @data: not used,
* @message: D-Bus connection and message received,
* @version: pointer for reply string.
*
* Implements the get method for the version property of the
* com.ubuntu.Upstart interface.
*
* Called to obtain the version of the init daemon, which will be stored
* as a string in @version.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_get_version (void * data,
NihDBusMessage *message,
char ** version)
{
nih_assert (message != NULL);
nih_assert (version != NULL);
*version = nih_strdup (message, package_string);
if (! *version)
nih_return_no_memory_error (-1);
return 0;
}
/**
* control_get_log_priority:
* @data: not used,
* @message: D-Bus connection and message received,
* @log_priority: pointer for reply string.
*
* Implements the get method for the log_priority property of the
* com.ubuntu.Upstart interface.
*
* Called to obtain the init daemon's current logging level, which will
* be stored as a string in @log_priority.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_get_log_priority (void * data,
NihDBusMessage *message,
char ** log_priority)
{
const char *priority;
nih_assert (message != NULL);
nih_assert (log_priority != NULL);
switch (nih_log_priority) {
case NIH_LOG_DEBUG:
priority = "debug";
break;
case NIH_LOG_INFO:
priority = "info";
break;
case NIH_LOG_MESSAGE:
priority = "message";
break;
case NIH_LOG_WARN:
priority = "warn";
break;
case NIH_LOG_ERROR:
priority = "error";
break;
case NIH_LOG_FATAL:
priority = "fatal";
break;
default:
nih_assert_not_reached ();
}
*log_priority = nih_strdup (message, priority);
if (! *log_priority)
nih_return_no_memory_error (-1);
return 0;
}
/**
* control_set_log_priority:
* @data: not used,
* @message: D-Bus connection and message received,
* @log_priority: string log priority to be set.
*
* Implements the get method for the log_priority property of the
* com.ubuntu.Upstart interface.
*
* Called to change the init daemon's current logging level to that given
* as a string in @log_priority. If the string is not recognised, the
* com.ubuntu.Upstart.Error.InvalidLogPriority error will be returned.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_set_log_priority (void * data,
NihDBusMessage *message,
const char * log_priority)
{
nih_assert (message != NULL);
nih_assert (log_priority != NULL);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to set log priority"));
return -1;
}
if (! strcmp (log_priority, "debug")) {
nih_log_set_priority (NIH_LOG_DEBUG);
} else if (! strcmp (log_priority, "info")) {
nih_log_set_priority (NIH_LOG_INFO);
} else if (! strcmp (log_priority, "message")) {
nih_log_set_priority (NIH_LOG_MESSAGE);
} else if (! strcmp (log_priority, "warn")) {
nih_log_set_priority (NIH_LOG_WARN);
} else if (! strcmp (log_priority, "error")) {
nih_log_set_priority (NIH_LOG_ERROR);
} else if (! strcmp (log_priority, "fatal")) {
nih_log_set_priority (NIH_LOG_FATAL);
} else {
nih_dbus_error_raise (DBUS_ERROR_INVALID_ARGS,
_("The log priority given was not recognised"));
return -1;
}
return 0;
}
/**
* control_get_bus_type:
*
* Determine D-Bus bus type to connect to.
*
* Returns: Type of D-Bus bus to connect to.
**/
DBusBusType
control_get_bus_type (void)
{
return (use_session_bus || user_mode)
? DBUS_BUS_SESSION
: DBUS_BUS_SYSTEM;
}
/**
* control_notify_disk_writeable:
* @data: not used,
* @message: D-Bus connection and message received,
*
* Implements the NotifyDiskWriteable method of the
* com.ubuntu.Upstart interface.
*
* Called to flush the job logs for all jobs that ended before the log
* disk became writeable.
*
* Notes: Session Inits are permitted to make this call. In the common
* case of starting a Session Init as a child of a Display Manager this
* is somewhat meaningless, but it does mean that if a Session Init were
* started from a system job, behaviour would be as expected.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_notify_disk_writeable (void *data,
NihDBusMessage *message)
{
int ret;
Session *session;
nih_assert (message != NULL);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to notify disk is writeable"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* "nop" when run from a chroot */
if (session && session->chroot)
return 0;
ret = log_clear_unflushed ();
if (ret < 0) {
nih_error_raise_system ();
return -1;
}
return 0;
}
/**
* control_notify_dbus_address:
* @data: not used,
* @message: D-Bus connection and message received,
* @address: Address of D-Bus to connect to.
*
* Implements the NotifyDBusAddress method of the
* com.ubuntu.Upstart interface.
*
* Called to allow the Session Init to connect to the D-Bus
* Session Bus when available.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_notify_dbus_address (void *data,
NihDBusMessage *message,
const char *address)
{
nih_assert (message);
nih_assert (address);
if (getpid () == 1) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("Not permissible to notify D-Bus address for PID 1"));
return -1;
}
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to notify D-Bus address"));
return -1;
}
/* Ignore as already connected */
if (control_bus)
return 0;
control_bus_address = nih_strdup (NULL, address);
if (! control_bus_address) {
nih_dbus_error_raise_printf (DBUS_ERROR_NO_MEMORY,
_("Out of Memory"));
return -1;
}
if (control_bus_open () < 0)
return -1;
return 0;
}
/**
* control_notify_cgroup_manager_address:
* @data: not used,
* @message: D-Bus connection and message received,
* @address: D-Bus address that cgroup manager is connected to.
*
* Implements the NotifyCGroupManagerAddress method of the
* com.ubuntu.Upstart interface.
*
* Called to allow the cgroup manager to be contacted,
* thus enabling the cgroup stanza.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_notify_cgroup_manager_address (void *data,
NihDBusMessage *message,
const char *address)
{
nih_assert (message);
nih_assert (address);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to notify cgroup manager address"));
return -1;
}
#ifdef ENABLE_CGROUPS
if (! cgroup_support_enabled ())
return 0;
/* Already called */
if (cgroup_manager_available ())
return 0;
if (! cgroup_manager_set_address (address)) {
nih_dbus_error_raise_printf (DBUS_ERROR_NO_MEMORY,
_("Out of Memory"));
return -1;
}
nih_debug ("set cgroup manager address");
if (! job_class_induct_jobs ()) {
nih_dbus_error_raise_printf (DBUS_ERROR_NO_MEMORY,
_("Out of Memory"));
return -1;
}
#else
nih_debug ("cgroup support not available");
#endif /* ENABLE_CGROUPS */
return 0;
}
/**
* control_bus_flush:
*
* Drain any remaining messages in the D-Bus queue.
**/
static void
control_bus_flush (void)
{
control_init ();
if (! control_bus)
return;
while (dbus_connection_dispatch (control_bus) == DBUS_DISPATCH_DATA_REMAINS)
;
}
/**
* control_prepare_reexec:
*
* Prepare for a re-exec by allowing the bus connection to be retained
* over re-exec and clearing all queued messages.
**/
void
control_prepare_reexec (void)
{
control_init ();
/* Necessary to disallow further commands but also to allow the
* new instance to open the control server.
*/
if (control_server)
control_server_close ();
control_bus_flush ();
}
/**
* control_conn_to_index:
*
* @connection: D-Bus connection.
*
* Convert a control (DBusConnection) connection to an index number
* the list of control connections.
*
* Returns: connection index, or -1 on error.
**/
int
control_conn_to_index (const DBusConnection *connection)
{
int conn_index = 0;
int found = FALSE;
nih_assert (connection);
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
if (connection == conn) {
found = TRUE;
break;
}
conn_index++;
}
if (! found)
return -1;
return conn_index;
}
/**
* control_conn_from_index:
*
* @conn_index: control connection index number.
*
* Lookup control connection based on index number.
*
* Returns: existing connection on success, or NULL if connection
* not found.
**/
DBusConnection *
control_conn_from_index (int conn_index)
{
int i = 0;
nih_assert (conn_index >= 0);
nih_assert (control_conns);
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
if (i == conn_index)
return conn;
i++;
}
return NULL;
}
/**
* control_bus_release_name:
*
* Unregister well-known D-Bus name.
*
* Returns: 0 on success, -1 on raised error.
**/
int
control_bus_release_name (void)
{
DBusError error;
int ret;
if (! control_bus)
return 0;
dbus_error_init (&error);
ret = dbus_bus_release_name (control_bus,
DBUS_SERVICE_UPSTART,
&error);
if (ret < 0) {
nih_dbus_error_raise (error.name, error.message);
dbus_error_free (&error);
return -1;
}
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;
size_t len;
nih_assert (message);
nih_assert (state);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to request state"));
return -1;
}
/* 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;
}
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;
}
/**
* control_restart:
*
* @data: not used,
* @message: D-Bus connection and message received.
*
* Implements the Restart method of the com.ubuntu.Upstart
* interface.
*
* Called to request that Upstart performs a stateful re-exec.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_restart (void *data,
NihDBusMessage *message)
{
Session *session;
nih_assert (message != NULL);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to request restart"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Chroot sessions must not be able to influence
* the outside system.
*
* Making this a NOP is safe since it is the Upstart outside the
* chroot which manages all chroot jobs.
*/
if (session && session->chroot) {
nih_warn (_("Ignoring restart request from chroot session"));
return 0;
}
nih_info (_("Restarting"));
stateful_reexec ();
return 0;
}
/**
* control_notify_event_emitted
*
* @event: Event.
*
* Re-emits an event over DBUS using the EventEmitted signal
**/
void
control_notify_event_emitted (Event *event)
{
nih_assert (event != NULL);
control_init ();
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (control_emit_event_emitted (conn, DBUS_PATH_UPSTART,
event->name, event->env));
}
}
/**
* control_notify_restarted
*
* DBUS signal sent when upstart has re-executed itself.
**/
void
control_notify_restarted (void)
{
control_init ();
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (control_emit_restarted (conn, DBUS_PATH_UPSTART));
}
}
/**
* control_set_env_list:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @vars: array of name[/value] pairs of environment variables to set,
* @replace: TRUE if @name should be overwritten if already set, else
* FALSE.
*
* Implements the SetEnvList method of the com.ubuntu.Upstart
* interface.
*
* Called to request Upstart store one or more name/value pairs.
*
* If @job_details is empty, change will be applied to all job
* environments, else only apply changes to specific job environment
* encoded within @job_details.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_set_env_list (void *data,
NihDBusMessage *message,
char * const *job_details,
char * const *vars,
int replace)
{
Session *session;
Job *job = NULL;
char *job_name = NULL;
char *instance = NULL;
char * const *var;
nih_assert (message);
nih_assert (job_details);
nih_assert (vars);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job environment"));
return -1;
}
if (getpid () == 1) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("Not permissible to modify PID 1 job environment"));
return -1;
}
if (job_details[0]) {
job_name = job_details[0];
/* this can be a null value */
instance = job_details[1];
}
/* Verify that job name is valid */
if (job_name && ! strlen (job_name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Job may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Chroot sessions must not be able to influence
* the outside system.
*/
if (session && session->chroot) {
nih_warn (_("Ignoring set env request from chroot session"));
return 0;
}
/* Lookup the job */
control_get_job (session, job, job_name, instance);
for (var = vars; var && *var; var++) {
nih_local char *envvar = NULL;
if (! *var) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Variable may not be empty string"));
return -1;
}
/* If variable does not contain a delimiter, add one to ensure
* it gets entered into the job environment table. Without the
* delimiter, the variable will be silently ignored unless it's
* already set in inits environment. But in that case there is
* no point in setting such a variable to its already existing
* value.
*/
if (! strchr (*var, '=')) {
envvar = NIH_MUST (nih_sprintf (NULL, "%s=", *var));
} else {
envvar = NIH_MUST (nih_strdup (NULL, *var));
}
if (job) {
/* Modify job-specific environment */
nih_assert (job->env);
NIH_MUST (environ_add (&job->env, job, NULL, replace, envvar));
} else if (job_class_environment_set (envvar, replace) < 0) {
nih_return_no_memory_error (-1);
}
}
return 0;
}
/**
* control_set_env:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @var: name[/value] pair of environment variable to set,
* @replace: TRUE if @name should be overwritten if already set, else
* FALSE.
*
* Implements the SetEnv method of the com.ubuntu.Upstart
* interface.
*
* Called to request Upstart store a particular name/value pair.
*
* If @job_details is empty, change will be applied to all job
* environments, else only apply changes to specific job environment
* encoded within @job_details.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_set_env (void *data,
NihDBusMessage *message,
char * const *job_details,
const char *var,
int replace)
{
nih_local char **vars = NULL;
if (! var) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Variable may not be empty string"));
return -1;
}
vars = NIH_MUST (nih_str_array_new (NULL));
NIH_MUST (nih_str_array_add (&vars, NULL, NULL, var));
return control_set_env_list (data, message, job_details, vars, replace);
}
/**
* control_unset_env_list:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @names: array of variables to clear from the job environment array.
*
* Implements the UnsetEnvList method of the com.ubuntu.Upstart
* interface.
*
* Called to request Upstart remove one or more variables from the job
* environment array.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_unset_env_list (void *data,
NihDBusMessage *message,
char * const *job_details,
char * const *names)
{
Session *session;
Job *job = NULL;
char *job_name = NULL;
char *instance = NULL;
char * const *name;
nih_assert (message);
nih_assert (job_details);
nih_assert (names);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job environment"));
return -1;
}
if (getpid () == 1) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("Not permissible to modify PID 1 job environment"));
return -1;
}
if (job_details[0]) {
job_name = job_details[0];
/* this can be a null value */
instance = job_details[1];
}
/* Verify that job name is valid */
if (job_name && ! strlen (job_name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Job may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Chroot sessions must not be able to influence
* the outside system.
*/
if (session && session->chroot) {
nih_warn (_("Ignoring unset env request from chroot session"));
return 0;
}
/* Lookup the job */
control_get_job (session, job, job_name, instance);
for (name = names; name && *name; name++) {
if (! *name) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Variable may not be empty string"));
return -1;
}
if (job) {
/* Modify job-specific environment */
nih_assert (job->env);
if (! environ_remove (&job->env, job, NULL, *name))
return -1;
} else if (job_class_environment_unset (*name) < 0) {
goto error;
}
}
return 0;
error:
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
"%s: %s",
_("No such variable"), *name);
return -1;
}
/**
* control_unset_env:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @name: variable to clear from the job environment array.
*
* Implements the UnsetEnv method of the com.ubuntu.Upstart
* interface.
*
* Called to request Upstart remove a particular variable from the job
* environment array.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_unset_env (void *data,
NihDBusMessage *message,
char * const *job_details,
const char *name)
{
nih_local char **names = NULL;
if (! name) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Variable may not be empty string"));
return -1;
}
names = NIH_MUST (nih_str_array_new (NULL));
NIH_MUST (nih_str_array_add (&names, NULL, NULL, name));
return control_unset_env_list (data, message, job_details, names);
}
/**
* control_get_env:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @name: name of environment variable to retrieve,
* @value: value of @name.
*
* Implements the GetEnv method of the com.ubuntu.Upstart
* interface.
*
* Called to obtain the value of a specified job environment variable.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_get_env (void *data,
NihDBusMessage *message,
char * const *job_details,
const char *name,
char **value)
{
Session *session;
const char *tmp;
Job *job = NULL;
char *job_name = NULL;
char *instance = NULL;
nih_assert (message != NULL);
nih_assert (job_details);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to query job environment"));
return -1;
}
if (! name || ! *name) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Variable may not be empty string"));
return -1;
}
if (job_details[0]) {
job_name = job_details[0];
/* this can be a null value */
instance = job_details[1];
}
/* Verify that job name is valid */
if (job_name && ! strlen (job_name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Job may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Chroot sessions must not be able to influence
* the outside system.
*/
if (session && session->chroot) {
nih_warn (_("Ignoring get env request from chroot session"));
return 0;
}
/* Lookup the job */
control_get_job (session, job, job_name, instance);
if (job) {
tmp = environ_get (job->env, name);
if (! tmp)
goto error;
*value = nih_strdup (message, tmp);
if (! *value)
nih_return_no_memory_error (-1);
return 0;
}
tmp = job_class_environment_get (name);
if (! tmp)
goto error;
*value = nih_strdup (message, tmp);
if (! *value)
nih_return_no_memory_error (-1);
return 0;
error:
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
"%s: %s",
_("No such variable"), name);
return -1;
}
/**
* control_list_env:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to,
* @env: pointer to array of all job environment variables.
*
* Implements the ListEnv method of the com.ubuntu.Upstart
* interface.
*
* Called to obtain an unsorted array of all environment variables
* that will be set in a jobs environment.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_list_env (void *data,
NihDBusMessage *message,
char * const *job_details,
char ***env)
{
Session *session;
Job *job = NULL;
char *job_name = NULL;
char *instance = NULL;
nih_assert (message);
nih_assert (job_details);
nih_assert (env);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to query job environment"));
return -1;
}
if (job_details[0]) {
job_name = job_details[0];
/* this can be a null value */
instance = job_details[1];
}
/* Verify that job name is valid */
if (job_name && ! strlen (job_name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Job may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Lookup the job */
control_get_job (session, job, job_name, instance);
if (job) {
*env = nih_str_array_copy (job, NULL, job->env);
if (! *env)
nih_return_no_memory_error (-1);
return 0;
}
*env = job_class_environment_get_all (message);
if (! *env)
nih_return_no_memory_error (-1);
return 0;
}
/**
* control_reset_env:
*
* @data: not used,
* @message: D-Bus connection and message received,
* @job_details: name and instance of job to apply operation to.
*
* Implements the ResetEnv method of the com.ubuntu.Upstart
* interface.
*
* Called to reset the environment all subsequent jobs will run in to
* the default minimal environment.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_reset_env (void *data,
NihDBusMessage *message,
char * const *job_details)
{
Session *session;
Job *job = NULL;
char *job_name = NULL;
char *instance = NULL;
nih_assert (message);
nih_assert (job_details);
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job environment"));
return -1;
}
if (getpid () == 1) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("Not permissible to modify PID 1 job environment"));
return -1;
}
if (job_details[0]) {
job_name = job_details[0];
/* this can be a null value */
instance = job_details[1];
}
/* Verify that job name is valid */
if (job_name && ! strlen (job_name)) {
nih_dbus_error_raise_printf (DBUS_ERROR_INVALID_ARGS,
_("Job may not be empty string"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
/* Chroot sessions must not be able to influence
* the outside system.
*/
if (session && session->chroot) {
nih_warn (_("Ignoring reset env request from chroot session"));
return 0;
}
/* Lookup the job */
control_get_job (session, job, job_name, instance);
if (job) {
size_t len;
if (job->env) {
nih_free (job->env);
job->env = NULL;
}
job->env = job_class_environment (job, job->class, &len);
if (! job->env)
nih_return_system_error (-1);
return 0;
}
job_class_environment_reset ();
return 0;
}
/**
* control_get_origin_uid:
* @message: D-Bus connection and message received,
* @uid: returned uid value.
*
* Returns TRUE: if @uid now contains uid corresponding to @message,
* else FALSE.
**/
static int
control_get_origin_uid (NihDBusMessage *message, uid_t *uid)
{
DBusError dbus_error;
unsigned long unix_user = 0;
const char *sender;
nih_assert (message);
nih_assert (uid);
dbus_error_init (&dbus_error);
if (! message->message || ! message->connection)
return FALSE;
sender = dbus_message_get_sender (message->message);
if (sender) {
unix_user = dbus_bus_get_unix_user (message->connection, sender,
&dbus_error);
if (unix_user == (unsigned long)-1) {
dbus_error_free (&dbus_error);
return FALSE;
}
} else {
if (! dbus_connection_get_unix_user (message->connection,
&unix_user)) {
return FALSE;
}
}
*uid = (uid_t)unix_user;
return TRUE;
}
/**
* control_check_permission:
*
* @message: D-Bus connection and message received.
*
* Determine if caller should be allowed to make a control request.
*
* Note that these permission checks rely on D-Bus to limit
* session bus access to the same user.
*
* Returns: TRUE if permission is granted, else FALSE.
**/
static int
control_check_permission (NihDBusMessage *message)
{
int ret;
uid_t uid;
pid_t pid;
uid_t origin_uid = 0;
nih_assert (message);
uid = getuid ();
pid = getpid ();
ret = control_get_origin_uid (message, &origin_uid);
/* Its possible that D-Bus might be unable to determine the user
* making the request. In this case, deny the request unless
* we're running as a Session Init or via the test harness.
*/
if ((ret && origin_uid == uid) || user_mode || (uid && pid != 1))
return TRUE;
return FALSE;
}
/**
* control_session_file_create:
*
* Create session file if possible.
*
* Errors are not fatal - the file is just not created.
**/
static void
control_session_file_create (void)
{
nih_local char *session_dir = NULL;
FILE *f;
int ret;
nih_assert (control_server_address);
session_dir = get_session_dir ();
if (! session_dir)
return;
NIH_MUST (nih_strcat_sprintf (&session_file, NULL, "%s/%d%s",
session_dir, (int)getpid (), SESSION_EXT));
f = fopen (session_file, "w");
if (! f) {
nih_error ("%s: %s", _("unable to create session file"), session_file);
return;
}
ret = fprintf (f, SESSION_ENV "=%s\n", control_server_address);
if (ret < 0)
nih_error ("%s: %s", _("unable to write session file"), session_file);
fclose (f);
}
/**
* control_session_file_remove:
*
* Delete session file.
*
* Errors are not fatal.
**/
static void
control_session_file_remove (void)
{
if (session_file)
(void)unlink (session_file);
}
/**
* control_session_end:
*
* @data: not used,
* @message: D-Bus connection and message received.
*
* Implements the EndSession method of the com.ubuntu.Upstart
* interface.
*
* Called to request that Upstart stop all jobs and exit. Only
* appropriate when running as a Session Init and user wishes to
* 'logout'.
*
* Returns: zero on success, negative value on raised error.
**/
int
control_end_session (void *data,
NihDBusMessage *message)
{
Session *session;
nih_assert (message);
/* Not supported at the system level */
if (getpid () == 1)
return 0;
if (! control_check_permission (message)) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to end session"));
return -1;
}
/* Get the relevant session */
session = session_from_dbus (NULL, message);
if (session && session->chroot) {
nih_warn (_("Ignoring session end request from chroot session"));
return 0;
}
quiesce (QUIESCE_REQUESTER_SESSION);
return 0;
}
/**
* control_serialise_bus_address:
*
* Convert control_bus_address into JSON representation.
*
* Returns: JSON string representing control_bus_address or NULL if
* control_bus_address not set or on error.
*
* Note: If NULL is returned, check the value of control_bus_address
* itself to determine if the error is real.
**/
json_object *
control_serialise_bus_address (void)
{
control_init ();
/* A NULL return represents a JSON null */
return control_bus_address
? json_object_new_string (control_bus_address)
: NULL;
}
/**
* control_deserialise_bus_address:
*
* @json: root of JSON-serialised state.
*
* Convert JSON representation of control_bus_address back into a native
* string.
*
* Returns: 0 on success, -1 on error.
**/
int
control_deserialise_bus_address (json_object *json)
{
const char *address;
nih_assert (json);
nih_assert (! control_bus_address);
control_init ();
/* control_bus_address was never set */
if (state_check_json_type (json, null))
return 0;
if (! state_check_json_type (json, string))
goto error;
address = json_object_get_string (json);
if (! address)
goto error;
control_bus_address = nih_strdup (NULL, address);
if (! control_bus_address)
goto error;
return 0;
error:
return -1;
}
/* upstart
*
* job.c - core state machine of tasks and services
*
* Copyright © 2010,2011 Canonical Ltd.
* Author: Scott James Remnant <scott@netsplit.com>.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2, as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <sys/types.h>
#include <errno.h>
#include <string.h>
#include <nih/macros.h>
#include <nih/alloc.h>
#include <nih/string.h>
#include <nih/list.h>
#include <nih/hash.h>
#include <nih/signal.h>
#include <nih/logging.h>
#include <nih-dbus/dbus_error.h>
#include <nih-dbus/dbus_message.h>
#include <nih-dbus/dbus_object.h>
#include <nih-dbus/dbus_util.h>
#include "dbus/upstart.h"
#include "events.h"
#include "environ.h"
#include "process.h"
#include "session.h"
#include "job_class.h"
#include "job.h"
#include "job_process.h"
#include "event.h"
#include "event_operator.h"
#include "blocked.h"
#include "control.h"
#include "parse_job.h"
#include "state.h"
#include "apparmor.h"
#ifdef ENABLE_CGROUPS
#include "cgroup.h"
#endif /* ENABLE_CGROUPS */
#include "com.ubuntu.Upstart.Job.h"
#include "com.ubuntu.Upstart.Instance.h"
/* Prototypes for static functions */
static const char *
job_goal_enum_to_str (JobGoal goal)
__attribute__ ((warn_unused_result));
static JobGoal job_goal_str_to_enum (const char *goal)
__attribute__ ((warn_unused_result));
static const char *
job_trace_state_enum_to_str (TraceState state)
__attribute__ ((warn_unused_result));
static TraceState
job_trace_state_str_to_enum (const char *state)
__attribute__ ((warn_unused_result));
static json_object *
job_serialise_kill_timer (NihTimer *timer)
__attribute__ ((warn_unused_result));
static NihTimer *
job_deserialise_kill_timer (json_object *json)
__attribute__ ((warn_unused_result));
static int
job_destroy (Job *job);
/**
* job_destroy:
*
* @job: Job.
*
* Called automatically when Job is being destroyed.
*
* Returns: 0 always.
**/
static int
job_destroy (Job *job)
{
int i;
nih_assert (job);
/* Free any associated NihIo's to avoid the handlers getting
* called potentially after the job has been freed.
*/
if (job->process_data) {
for (i = 0; i < PROCESS_LAST; i++) {
if (job->process_data[i]) {
nih_free (job->process_data[i]);
job->process_data[i] = NULL;
}
}
}
nih_list_destroy (&job->entry);
return 0;
}
/**
* job_new:
* @class: class of job,
* @name: name for new instance.
*
* Allocates and returns a new Job structure for the @class given,
* appending it to the list of instances for @class. The returned job
* will also be an nih_alloc() child of @class.
*
* @name is used to uniquely identify the instance and is normally
* generated by expanding the @class's instance member.
*
* Returns: newly allocated job structure or NULL if insufficient memory.
**/
Job *
job_new (JobClass *class,
const char *name)
{
Job *job;
int i;
nih_assert (class != NULL);
nih_assert (name != NULL);
control_init ();
job = nih_new (class, Job);
if (! job)
return NULL;
nih_list_init (&job->entry);
/* Ensure unset before destructor could possibly be called */
job->process_data = NULL;
nih_alloc_set_destructor (job, job_destroy);
job->name = nih_strdup (job, name);
if (! job->name)
goto error;
job->class = class;
if (job->class->session && job->class->session->chroot) {
/* JobClass already contains a valid D-Bus path prefix for the job */
job->path = nih_dbus_path (job, class->path, job->name, NULL);
} else {
job->path = nih_dbus_path (job, DBUS_PATH_UPSTART, "jobs",
class->name, job->name, NULL);
}
if (! job->path)
goto error;
job->goal = JOB_STOP;
job->state = JOB_WAITING;
job->env = NULL;
job->start_env = NULL;
job->stop_env = NULL;
job->stop_on = NULL;
if (class->stop_on) {
job->stop_on = event_operator_copy (job, class->stop_on);
if (! job->stop_on)
goto error;
}
job->fds = NULL;
job->num_fds = 0;
job->pid = nih_alloc (job, sizeof (pid_t) * PROCESS_LAST);
if (! job->pid)
goto error;
for (i = 0; i < PROCESS_LAST; i++)
job->pid[i] = 0;
/* Each job process needs its own log object to ensure sane
* behaviour: consider a post-start that starts and ends
* before the main process ends: it will be reaped (and its log
* flushed) before the main process has a chance to have its log
* drained.
*/
job->log = nih_alloc (job, sizeof (Log *) * PROCESS_LAST);
if (! job->log)
goto error;
for (i = 0; i < PROCESS_LAST; i++)
job->log[i] = NULL;
job->blocker = NULL;
nih_list_init (&job->blocking);
job->kill_timer = NULL;
job->kill_process = PROCESS_INVALID;
job->failed = FALSE;
job->failed_process = PROCESS_INVALID;
job->exit_status = 0;
job->respawn_time = 0;
job->respawn_count = 0;
job->trace_forks = 0;
job->trace_state = TRACE_NONE;
nih_hash_add (class->instances, &job->entry);
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
job_register (job, conn, TRUE);
}
/* Since some job processes can run in parallel, we must ensure
* that the asynchronous-spawning of such job processes is
* handled by providing a handler for each pid.
*
* Strictly, this is only necessary for certain combinations of
* job processes (such as PROCESS_MAIN and PROCESS_POST_START),
* however for consistency with other entities (such as Log), we
* create a slot for all job processes since there is minimal
* overhead (a single pointer) for those job processes tha
* cannot run in parallel with others.
*/
job->process_data = nih_alloc (job, sizeof (JobProcessData *) * PROCESS_LAST);
if (! job->process_data)
goto error;
for (i = 0; i < PROCESS_LAST; i++)
job->process_data[i] = NULL;
return job;
error:
nih_free (job);
return NULL;
}
/**
* job_register:
* @job: job to register,
* @conn: connection to register for,
* @signal: emit the InstanceAdded signal.
*
* Register the @job instance with the D-Bus connection @conn, using
* the path set when the job was created.
**/
void
job_register (Job *job,
DBusConnection *conn,
int signal)
{
nih_assert (job != NULL);
nih_assert (conn != NULL);
NIH_MUST (nih_dbus_object_new (job, conn, job->path,
job_interfaces, job));
nih_debug ("Registered instance %s", job->path);
if (signal)
NIH_ZERO (job_class_emit_instance_added (conn, job->class->path,
job->path));
}
/**
* job_change_goal:
* @job: job to change goal of,
* @goal: goal to change to.
*
* This function changes the current goal of a @job to the new @goal given,
* performing any necessary state changes or actions (such as killing
* the running process) to correctly enter the new goal.
*
* If the job is not in a rest state (WAITING or RUNNING), this has no
* other effect than changing the goal; since the job is waiting on some
* other event. The goal change will cause it to take action to head
* towards stopped.
*
* If the job is in the WAITING state and @goal is START, the job will
* begin to be started and will block in the STARTING state for an event
* to finish.
*
* If the job is in the RUNNING state and @goal is STOP, the job will
* begin to be stopped and will either block in the PRE-STOP state for
* the pre-stop script or the STOPPING state for an event to finish.
*
* Thus in all circumstances, @job is safe to use once this function
* returns. Though further calls to job_change_state may change that as
* noted.
**/
void
job_change_goal (Job *job,
JobGoal goal)
{
nih_assert (job != NULL);
if (job->goal == goal)
return;
nih_info (_("%s goal changed from %s to %s"), job_name (job),
job_goal_name (job->goal), job_goal_name (goal));
job->goal = goal;
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (job_emit_goal_changed (
conn, job->path,
job_goal_name (job->goal)));
}
/* Normally whatever process or event is associated with the state
* will finish naturally, so all we need do is change the goal and
* we'll change direction through the state machine at that point.
*
* The exceptions are the natural rest states of waiting and a
* running process; these need induction to get them moving.
*/
switch (goal) {
case JOB_START:
if (job->state == JOB_WAITING)
job_change_state (job, job_next_state (job));
break;
case JOB_STOP:
if (job->state == JOB_RUNNING)
job_change_state (job, job_next_state (job));
break;
case JOB_RESPAWN:
break;
default:
nih_assert_not_reached ();
}
}
/**
* job_change_state:
* @job: job to change state of,
* @state: state to change to.
*
* This function changes the current state of a @job to the new @state
* given, performing any actions to correctly enter the new state (such
* as spawning scripts or processes).
*
* The associated event is also queued by this function.
*
* Some state transitions are not be permitted and will result in an
* assertion failure. Also some state transitions may result in further
* transitions, so the state when this function returns may not be the
* state requested.
*
* WARNING: On return from this function, @job may no longer be valid
* since it will be freed once it becomes fully stopped.
**/
void
job_change_state (Job *job,
JobState state)
{
nih_assert (job != NULL);
while (job->state != state) {
JobState old_state;
int unused;
/* If we got blocked during async spawns, stop
* transitions.
*/
if (job->blocker)
return;
nih_info (_("%s state changed from %s to %s"), job_name (job),
job_state_name (job->state), job_state_name (state));
old_state = job->state;
job->state = state;
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (job_emit_state_changed (
conn, job->path,
job_state_name (job->state)));
}
/* Perform whatever action is necessary to enter the new
* state, such as executing a process or emitting an event.
*/
switch (job->state) {
case JOB_STARTING:
nih_assert (job->goal == JOB_START);
nih_assert ((old_state == JOB_WAITING)
|| (old_state == JOB_POST_STOP));
/* Throw away our old environment and use the newly
* set environment from now on; unless that's NULL
* in which case we just keep our old environment.
*/
if (job->start_env) {
if (job->env)
nih_unref (job->env, job);
job->env = job->start_env;
job->start_env = NULL;
}
/* Throw away the stop environment */
if (job->stop_env) {
nih_unref (job->stop_env, job);
job->stop_env = NULL;
}
/* Clear any old failed information */
job->failed = FALSE;
job->failed_process = PROCESS_INVALID;
job->exit_status = 0;
job->blocker = job_emit_event (job);
break;
case JOB_SECURITY_SPAWNING:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_STARTING);
if (job->class->process[PROCESS_SECURITY]
&& apparmor_available()) {
job_process_start (job, PROCESS_SECURITY);
}
state = job_next_state (job);
break;
case JOB_SECURITY:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_SECURITY_SPAWNING);
if (! (job->class->process[PROCESS_SECURITY]
&& apparmor_available())) {
state = job_next_state (job);
}
break;
case JOB_PRE_STARTING:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_SECURITY);
/* spawn pre-start asynchronously, child
* watcher asynchronously will change goal to
* stop if spawning fails.
*/
if (job->class->process[PROCESS_PRE_START]) {
job_process_start (job, PROCESS_PRE_START);
}
state = job_next_state (job);
break;
case JOB_PRE_START:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_PRE_STARTING);
/* if no pre-start process, go to next
* state. otherwise async child watcher will
* trigger us to go to the next state */
if (! job->class->process[PROCESS_PRE_START])
state = job_next_state (job);
break;
case JOB_SPAWNING:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_PRE_START);
if (job->class->process[PROCESS_MAIN]) {
job_process_start (job, PROCESS_MAIN);
}
state = job_next_state (job);
break;
case JOB_SPAWNED:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_SPAWNING);
if (! job->class->process[PROCESS_MAIN]) {
state = job_next_state (job);
}
break;
case JOB_POST_STARTING:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_SPAWNED);
if (job->class->process[PROCESS_POST_START]) {
job_process_start (job, PROCESS_POST_START);
}
state = job_next_state (job);
break;
case JOB_POST_START:
nih_assert (job->goal == JOB_START);
nih_assert (old_state == JOB_POST_STARTING);
if (! job->class->process[PROCESS_POST_START]) {
state = job_next_state (job);
}
break;
case JOB_RUNNING:
nih_assert (job->goal == JOB_START);
nih_assert ((old_state == JOB_POST_START)
|| (old_state == JOB_PRE_STOP));
if (old_state == JOB_PRE_STOP) {
/* Throw away the stop environment */
if (job->stop_env) {
nih_unref (job->stop_env, job);
job->stop_env = NULL;
}
/* Cancel the stop attempt */
job_finished (job, FALSE);
} else {
job_emit_event (job);
/* If we're not a task, our goal is to be
* running.
*/
if (! job->class->task)
job_finished (job, FALSE);
}
break;
case JOB_PRE_STOPPING:
nih_assert (job->goal == JOB_STOP);
nih_assert (old_state == JOB_RUNNING);
if (job->class->process[PROCESS_PRE_STOP]) {
job_process_start (job, PROCESS_PRE_STOP);
}
state = job_next_state (job);
break;
case JOB_PRE_STOP:
nih_assert (job->goal == JOB_STOP);
nih_assert (old_state == JOB_PRE_STOPPING);
if (! job->class->process[PROCESS_PRE_STOP]) {
state = job_next_state (job);
}
break;
case JOB_STOPPING:
nih_assert ((old_state == JOB_STARTING)
|| (old_state == JOB_PRE_STARTING)
|| (old_state == JOB_PRE_START)
|| (old_state == JOB_SECURITY)
|| (old_state == JOB_SPAWNED)
|| (old_state == JOB_POST_START)
|| (old_state == JOB_RUNNING)
|| (old_state == JOB_PRE_STOP));
job->blocker = job_emit_event (job);
break;
case JOB_KILLED:
nih_assert (old_state == JOB_STOPPING);
if (job->class->process[PROCESS_MAIN]
&& (job->pid[PROCESS_MAIN] > 0)) {
job_process_kill (job, PROCESS_MAIN);
} else {
state = job_next_state (job);
}
break;
case JOB_POST_STOPPING:
nih_assert (old_state == JOB_KILLED);
if (job->class->process[PROCESS_POST_STOP]) {
job_process_start (job, PROCESS_POST_STOP);
}
state = job_next_state (job);
break;
case JOB_POST_STOP:
nih_assert (old_state == JOB_POST_STOPPING);
if (! job->class->process[PROCESS_POST_STOP]) {
state = job_next_state (job);
}
break;
case JOB_WAITING:
nih_assert (job->goal == JOB_STOP);
nih_assert ((old_state == JOB_POST_STOP)
|| (old_state == JOB_STARTING));
job_emit_event (job);
job_finished (job, FALSE);
/* Remove the job from the list of instances and
* then allow a better class to replace us
* in the hash table if we have no other instances
* and there is one.
*/
nih_list_remove (&job->entry);
unused = job_class_reconsider (job->class);
/* If the class is due to be deleted, free it
* taking the job with it; otherwise free the
* job.
*/
if (job->class->deleted && unused) {
nih_debug ("Destroyed unused job %s",
job->class->name);
nih_free (job->class);
} else {
nih_debug ("Destroyed inactive instance %s",
job_name (job));
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (job_class_emit_instance_removed (
conn,
job->class->path,
job->path));
}
/* Destroy the instance */
nih_free (job);
}
return;
}
}
}
/**
* job_next_state:
* @job: job undergoing state change.
*
* The next state a job needs to change into is not always obvious as it
* depends both on the current state and the ultimate goal of the job, ie.
* whether we're moving towards stop or start.
*
* This function contains the logic to decide the next state the job should
* be in based on the current state and goal.
*
* It is up to the caller to ensure the goal is set appropriately before
* calling this function, for example setting it to JOB_STOP if something
* failed. It is also up to the caller to actually set the new state as
* this simply returns the suggested one.
*
* Returns: suggested state to change to.
**/
JobState
job_next_state (Job *job)
{
nih_assert (job != NULL);
switch (job->state) {
case JOB_WAITING:
switch (job->goal) {
case JOB_STOP:
nih_assert_not_reached ();
case JOB_START:
return JOB_STARTING;
default:
nih_assert_not_reached ();
}
case JOB_STARTING:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_SECURITY_SPAWNING;
default:
nih_assert_not_reached ();
}
case JOB_SECURITY_SPAWNING:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_SECURITY;
default:
nih_assert_not_reached ();
}
case JOB_SECURITY:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_PRE_STARTING;
default:
nih_assert_not_reached ();
}
case JOB_PRE_STARTING:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_PRE_START;
default:
nih_assert_not_reached ();
}
case JOB_PRE_START:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_SPAWNING;
default:
nih_assert_not_reached ();
}
case JOB_SPAWNING:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_SPAWNED;
default:
nih_assert_not_reached ();
}
case JOB_SPAWNED:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_POST_STARTING;
default:
nih_assert_not_reached ();
}
case JOB_POST_STARTING:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_POST_START;
default:
nih_assert_not_reached ();
}
case JOB_POST_START:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_RUNNING;
case JOB_RESPAWN:
job_change_goal (job, JOB_START);
return JOB_STOPPING;
default:
nih_assert_not_reached ();
}
case JOB_RUNNING:
switch (job->goal) {
case JOB_STOP:
if (job->class->process[PROCESS_MAIN]
&& (job->pid[PROCESS_MAIN] > 0)) {
return JOB_PRE_STOPPING;
} else {
return JOB_STOPPING;
}
case JOB_START:
return JOB_STOPPING;
default:
nih_assert_not_reached ();
}
case JOB_PRE_STOPPING:
switch (job->goal) {
case JOB_STOP:
return JOB_PRE_STOP;
case JOB_START:
return JOB_PRE_STOP;
default:
nih_assert_not_reached ();
}
case JOB_PRE_STOP:
switch (job->goal) {
case JOB_STOP:
return JOB_STOPPING;
case JOB_START:
return JOB_RUNNING;
case JOB_RESPAWN:
job_change_goal (job, JOB_START);
return JOB_STOPPING;
default:
nih_assert_not_reached ();
}
case JOB_STOPPING:
switch (job->goal) {
case JOB_STOP:
return JOB_KILLED;
case JOB_START:
return JOB_KILLED;
default:
nih_assert_not_reached ();
}
case JOB_KILLED:
switch (job->goal) {
case JOB_STOP:
return JOB_POST_STOPPING;
case JOB_START:
return JOB_POST_STOPPING;
default:
nih_assert_not_reached ();
}
case JOB_POST_STOPPING:
switch (job->goal) {
case JOB_STOP:
return JOB_POST_STOP;
case JOB_START:
return JOB_POST_STOP;
default:
nih_assert_not_reached ();
}
case JOB_POST_STOP:
switch (job->goal) {
case JOB_STOP:
return JOB_WAITING;
case JOB_START:
return JOB_STARTING;
default:
nih_assert_not_reached ();
}
default:
nih_assert_not_reached ();
}
}
/**
* job_failed:
* @job: job that has failed,
* @process: process that failed,
* @status: status of @process at failure.
*
* Mark @job as having failed, unless it already has been marked so, storing
* @process and @status so that they may show up as arguments and environment
* to the stop and stopped events generated for the job.
*
* Additionally this marks the start and stop events as failed as well; this
* is reported to the emitter of the event, and will also cause a failed event
* to be generated after the event completes.
*
* @process may be -1 to indicate a failure to respawn, and @exit_status
* may be -1 to indicate a spawn failure.
**/
void
job_failed (Job *job,
ProcessType process,
int status)
{
nih_assert (job != NULL);
if (job->failed)
return;
job->failed = TRUE;
job->failed_process = process;
job->exit_status = status;
NIH_LIST_FOREACH (control_conns, iter) {
NihListEntry *entry = (NihListEntry *)iter;
DBusConnection *conn = (DBusConnection *)entry->data;
NIH_ZERO (job_emit_failed (conn, job->path, status));
}
job_finished (job, TRUE);
}
/**
* job_finished:
* @job: job that is blocking,
* @failed: mark events as failed.
*
* This function unblocks any events blocking on @job; it is called when the
* job reaches a rest state (waiting for all, running for services), when a
* new command is received or when the job fails.
*
* If @failed is TRUE then the events that are blocking will be marked as
* failed.
**/
void
job_finished (Job *job,
int failed)
{
nih_assert (job != NULL);
NIH_LIST_FOREACH_SAFE (&job->blocking, iter) {
Blocked *blocked = (Blocked *)iter;
switch (blocked->type) {
case BLOCKED_EVENT:
if (failed)
blocked->event->failed = TRUE;
event_unblock (blocked->event);
break;
case BLOCKED_JOB_START_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed to start")));
} else {
NIH_ZERO (job_class_start_reply (
blocked->message,
job->path));
}
break;
case BLOCKED_JOB_STOP_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed while stopping")));
} else {
NIH_ZERO (job_class_stop_reply (
blocked->message));
}
break;
case BLOCKED_JOB_RESTART_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed to restart")));
} else {
NIH_ZERO (job_class_restart_reply (
blocked->message,
job->path));
}
break;
case BLOCKED_INSTANCE_START_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed to start")));
} else {
NIH_ZERO (job_start_reply (blocked->message));
}
break;
case BLOCKED_INSTANCE_STOP_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed while stopping")));
} else {
NIH_ZERO (job_stop_reply (blocked->message));
}
break;
case BLOCKED_INSTANCE_RESTART_METHOD:
if (failed) {
NIH_ZERO (nih_dbus_message_error (
blocked->message,
DBUS_INTERFACE_UPSTART ".Error.JobFailed",
_("Job failed to restart")));
} else {
NIH_ZERO (job_restart_reply (blocked->message));
}
break;
default:
nih_assert_not_reached ();
}
nih_free (blocked);
}
}
/**
* job_emit_event:
* @job: job generating the event.
*
* Called from a state change because it believes an event should be
* emitted. Constructs the event with the right arguments and environment
* and adds it to the pending queue.
*
* The starting and stopping events will record the job as blocking on
* the event, and will change the job's state when they finish.
*
* The stopping and stopped events have an extra argument that is "ok" if
* the job terminated successfully, or "failed" if it terminated with an
* error. If failed, a further argument indicates which process it was
* that caused the failure and either an EXIT_STATUS or EXIT_SIGNAL
* environment variable detailing it.
*
* Returns: new Event in the queue.
**/
Event *
job_emit_event (Job *job)
{
Event *event;
const char *name;
int block = FALSE, stop = FALSE;
nih_local char **env = NULL;
char **e;
size_t len;
nih_assert (job != NULL);
switch (job->state) {
case JOB_STARTING:
name = JOB_STARTING_EVENT;
block = TRUE;
break;
case JOB_RUNNING:
name = JOB_STARTED_EVENT;
break;
case JOB_STOPPING:
name = JOB_STOPPING_EVENT;
block = TRUE;
stop = TRUE;
break;
case JOB_WAITING:
name = JOB_STOPPED_EVENT;
stop = TRUE;
break;
default:
nih_assert_not_reached ();
}
len = 0;
env = NIH_MUST (nih_str_array_new (NULL));
/* Add the job and instance name */
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"JOB=%s", job->class->name));
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"INSTANCE=%s", job->name));
/* Stop events include a "failed" argument if a process failed,
* otherwise stop events have an "ok" argument.
*/
if (stop && job->failed) {
NIH_MUST (environ_add (&env, NULL, &len, TRUE,
"RESULT=failed"));
/* Include information about the process that failed, and
* the signal/exit information. If it was the spawn itself
* that failed, we don't include signal/exit information and
* if it was a respawn failure, we use the special "respawn"
* argument instead of the process name,
*/
if ((job->failed_process != PROCESS_INVALID)
&& (job->exit_status != -1)) {
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"PROCESS=%s",
process_name (job->failed_process)));
/* If the job was terminated by a signal, that
* will be stored in the higher byte and we
* set EXIT_SIGNAL instead of EXIT_STATUS.
*/
if (job->exit_status & ~0xff) {
const char *sig;
sig = nih_signal_to_name (job->exit_status >> 8);
if (sig) {
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"EXIT_SIGNAL=%s", sig));
} else {
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"EXIT_SIGNAL=%d", job->exit_status >> 8));
}
} else {
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"EXIT_STATUS=%d", job->exit_status));
}
} else if (job->failed_process != PROCESS_INVALID) {
NIH_MUST (environ_set (&env, NULL, &len, TRUE,
"PROCESS=%s",
process_name (job->failed_process)));
} else {
NIH_MUST (environ_add (&env, NULL, &len, TRUE,
"PROCESS=respawn"));
}
} else if (stop) {
NIH_MUST (environ_add (&env, NULL, &len, TRUE, "RESULT=ok"));
}
/* Add any exported variables from the job environment */
for (e = job->class->export; e && *e; e++) {
char * const *str;
str = environ_lookup (job->env, *e, strlen (*e));
if (str)
NIH_MUST (environ_add (&env, NULL, &len, FALSE, *str));
}
event = NIH_MUST (event_new (NULL, name, env));
event->session = job->class->session;
if (block) {
Blocked *blocked;
blocked = NIH_MUST (blocked_new (event, BLOCKED_JOB, job));
nih_list_add (&event->blocking, &blocked->entry);
}
return event;
}
/**
* job_name:
* @job: job to return name of.
*
* Returns a string used in messages that contains the job name; this
* always begins with the name from the class, and then if set,
* has the name of the instance appended in brackets.
*
* Returns: internal copy of the string.
**/
const char *
job_name (Job *job)
{
static char *name = NULL;
nih_assert (job != NULL);
if (name)
nih_discard (name);
if (*job->name) {
name = NIH_MUST (nih_sprintf (NULL, "%s (%s)",
job->class->name, job->name));
} else {
name = NIH_MUST (nih_strdup (NULL, job->class->name));
}
return name;
}
/**
* job_goal_name:
* @goal: goal to convert.
*
* Converts an enumerated job goal into the string used for the status
* and for logging purposes.
*
* Returns: static string or NULL if goal not known.
**/
const char *
job_goal_name (JobGoal goal)
{
switch (goal) {
case JOB_STOP:
return N_("stop");
case JOB_START:
return N_("start");
case JOB_RESPAWN:
return N_("respawn");
default:
return NULL;
}
}
/**
* job_goal_from_name:
* @goal: goal to convert.
*
* Converts a job goal string into the enumeration.
*
* Returns: enumerated goal or -1 if not known.
**/
JobGoal
job_goal_from_name (const char *goal)
{
nih_assert (goal != NULL);
if (! strcmp (goal, "stop")) {
return JOB_STOP;
} else if (! strcmp (goal, "start")) {
return JOB_START;
} else if (! strcmp (goal, "respawn")) {
return JOB_RESPAWN;
} else {
return -1;
}
}
/**
* job_state_name:
* @state: state to convert.
*
* Converts an enumerated job state into the string used for the status
* and for logging purposes.
*
* Returns: static string or NULL if state not known.
**/
const char *
job_state_name (JobState state)
{
switch (state) {
case JOB_WAITING:
return N_("waiting");
case JOB_STARTING:
return N_("starting");
case JOB_SECURITY_SPAWNING:
return N_("security-spawning");
case JOB_SECURITY:
return N_("security");
case JOB_PRE_STARTING:
return N_("pre-starting");
case JOB_PRE_START:
return N_("pre-start");
case JOB_SPAWNING:
return N_("spawning");
case JOB_SPAWNED:
return N_("spawned");
case JOB_POST_STARTING:
return N_("post-starting");
case JOB_POST_START:
return N_("post-start");
case JOB_RUNNING:
return N_("running");
case JOB_PRE_STOPPING:
return N_("pre-stopping");
case JOB_PRE_STOP:
return N_("pre-stop");
case JOB_STOPPING:
return N_("stopping");
case JOB_KILLED:
return N_("killed");
case JOB_POST_STOPPING:
return N_("post-stopping");
case JOB_POST_STOP:
return N_("post-stop");
default:
return NULL;
}
}
/**
* job_state_from_name:
* @state: state to convert.
*
* Converts a job state string into the enumeration.
*
* Returns: enumerated state or -1 if not known.
**/
JobState
job_state_from_name (const char *state)
{
nih_assert (state != NULL);
if (! strcmp (state, "waiting")) {
return JOB_WAITING;
} else if (! strcmp (state, "starting")) {
return JOB_STARTING;
} else if (! strcmp (state, "security-spawning")) {
return JOB_SECURITY_SPAWNING;
} else if (! strcmp (state, "security")) {
return JOB_SECURITY;
} else if (! strcmp (state, "pre-starting")) {
return JOB_PRE_STARTING;
} else if (! strcmp (state, "pre-start")) {
return JOB_PRE_START;
} else if (! strcmp (state, "spawning")) {
return JOB_SPAWNING;
} else if (! strcmp (state, "spawned")) {
return JOB_SPAWNED;
} else if (! strcmp (state, "post-starting")) {
return JOB_POST_STARTING;
} else if (! strcmp (state, "post-start")) {
return JOB_POST_START;
} else if (! strcmp (state, "running")) {
return JOB_RUNNING;
} else if (! strcmp (state, "pre-stopping")) {
return JOB_PRE_STOPPING;
} else if (! strcmp (state, "pre-stop")) {
return JOB_PRE_STOP;
} else if (! strcmp (state, "stopping")) {
return JOB_STOPPING;
} else if (! strcmp (state, "killed")) {
return JOB_KILLED;
} else if (! strcmp (state, "post-stopping")) {
return JOB_POST_STOPPING;
} else if (! strcmp (state, "post-stop")) {
return JOB_POST_STOP;
} else {
return -1;
}
}
/**
* job_start:
* @job: job to be started,
* @message: D-Bus connection and message received,
* @wait: whether to wait for command to finish before returning.
*
* Implements the top half of the Start method of the
* com.ubuntu.Upstart.Instance interface, the bottom half may be found in
* job_finished().
*
* Called on a stopping instance @job to cause it to be restarted. If the
* instance goal is already start, the com.ubuntu.Upstart.Error.AlreadyStarted
* D_Bus error will be returned immediately. If the instance fails to
* start again, the com.ubuntu.Upstart.Error.JobFailed D-Bus error will
* be returned when the problem occurs.
*
* When @wait is TRUE the method call will not return until the job has
* finished starting (running for tasks); when @wait is FALSE, the method
* call returns once the command has been processed and the goal changed.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_start (Job *job,
NihDBusMessage *message,
int wait)
{
Session *session;
Blocked *blocked = NULL;
nih_assert (job != NULL);
nih_assert (message != NULL);
/* Don't permit out-of-session modification */
session = session_from_dbus (NULL, message);
if (session != job->class->session) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job: %s"),
job_name (job));
return -1;
}
if (job->goal == JOB_START) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.AlreadyStarted",
_("Job is already running: %s"),
job_name (job));
return -1;
}
#ifdef ENABLE_CGROUPS
/* Job has specified a cgroup stanza but since the cgroup
* manager has not yet been contacted, the job cannot be started.
*/
if (job_class_cgroups (job->class) && ! cgroup_manager_available ()) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.CGroupManagerNotAvailable",
_("Job cannot be started as cgroup manager not available: %s"),
job_name (job));
return -1;
}
#endif /* ENABLE_CGROUPS */
if (wait) {
blocked = blocked_new (job, BLOCKED_INSTANCE_START_METHOD,
message);
if (! blocked)
nih_return_system_error (-1);
}
if (job->start_env)
nih_unref (job->start_env, job);
job->start_env = NULL;
job_finished (job, FALSE);
if (blocked)
nih_list_add (&job->blocking, &blocked->entry);
job_change_goal (job, JOB_START);
if (! wait)
NIH_ZERO (job_start_reply (message));
return 0;
}
/**
* job_stop:
* @job: job to be stopped,
* @message: D-Bus connection and message received,
* @wait: whether to wait for command to finish before returning.
*
* Implements the top half of the Stop method of the
* com.ubuntu.Upstart.Instance interface, the bottom half may be found in
* job_finished().
*
* Called on a running instance @job to cause it to be stopped. If the
* instance goal is already stop, the com.ubuntu.Upstart.Error.AlreadyStopped
* D_Bus error will be returned immediately. If the instance fails while
* stopping, the com.ubuntu.Upstart.Error.JobFailed D-Bus error will
* be returned when the problem occurs.
*
* When @wait is TRUE the method call will not return until the job has
* finished stopping; when @wait is FALSE, the method call returns once
* the command has been processed and the goal changed.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_stop (Job *job,
NihDBusMessage *message,
int wait)
{
Session *session;
Blocked *blocked = NULL;
nih_assert (job != NULL);
nih_assert (message != NULL);
/* Don't permit out-of-session modification */
session = session_from_dbus (NULL, message);
if (session != job->class->session) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job: %s"),
job_name (job));
return -1;
}
if (job->goal == JOB_STOP) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.AlreadyStopped",
_("Job has already been stopped: %s"),
job_name (job));
return -1;
}
if (wait) {
blocked = blocked_new (job, BLOCKED_INSTANCE_STOP_METHOD,
message);
if (! blocked)
nih_return_system_error (-1);
}
if (job->stop_env)
nih_unref (job->stop_env, job);
job->stop_env = NULL;
job_finished (job, FALSE);
if (blocked)
nih_list_add (&job->blocking, &blocked->entry);
job_change_goal (job, JOB_STOP);
if (! wait)
NIH_ZERO (job_stop_reply (message));
return 0;
}
/**
* job_restart:
* @job: job to be restarted,
* @message: D-Bus connection and message received,
* @wait: whether to wait for command to finish before returning.
*
* Implements the top half of the Restart method of the
* com.ubuntu.Upstart.Instance interface, the bottom half may be found in
* job_finished().
*
* Called on a running instance @job to cause it to be restarted. If the
* instance goal is already stop, the com.ubuntu.Upstart.Error.AlreadyStopped
* D-Bus error will be returned immediately. If the instance fails to
* restart, the com.ubuntu.Upstart.Error.JobFailed D-Bus error will
* be returned when the problem occurs.
*
* When @wait is TRUE the method call will not return until the job has
* finished starting again (running for tasks); when @wait is FALSE, the
* method call returns once the command has been processed and the goal
* changed.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_restart (Job *job,
NihDBusMessage *message,
int wait)
{
Session *session;
Blocked *blocked = NULL;
nih_assert (job != NULL);
nih_assert (message != NULL);
/* Don't permit out-of-session modification */
session = session_from_dbus (NULL, message);
if (session != job->class->session) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job: %s"),
job_name (job));
return -1;
}
if (job->goal == JOB_STOP) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.AlreadyStopped",
_("Job has already been stopped: %s"),
job_name (job));
return -1;
}
if (wait) {
blocked = blocked_new (job, BLOCKED_INSTANCE_RESTART_METHOD,
message);
if (! blocked)
nih_return_system_error (-1);
}
if (job->start_env)
nih_unref (job->start_env, job);
job->start_env = NULL;
if (job->stop_env)
nih_unref (job->stop_env, job);
job->stop_env = NULL;
job_finished (job, FALSE);
if (blocked)
nih_list_add (&job->blocking, &blocked->entry);
job_change_goal (job, JOB_STOP);
job_change_goal (job, JOB_START);
if (! wait)
NIH_ZERO (job_restart_reply (message));
return 0;
}
/**
* job_reload:
* @job: job to reload,
* @message: D-Bus connection and message received,
*
* Implements the Reload method of the com.ubuntu.Upstart.Instance
* interface.
*
* Called on a running instance @job to reload.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_reload (Job *job,
NihDBusMessage *message)
{
Session *session;
nih_assert (job != NULL);
nih_assert (message != NULL);
/* Don't permit out-of-session modification */
session = session_from_dbus (NULL, message);
if (session != job->class->session) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.PermissionDenied",
_("You do not have permission to modify job: %s"),
job_name (job));
return -1;
}
if (job->pid[PROCESS_MAIN] <= 0) {
nih_dbus_error_raise_printf (
DBUS_INTERFACE_UPSTART ".Error.NotRunning",
_("Job is not running: %s"),
job_name (job));
return -1;
}
if (kill (job->pid[PROCESS_MAIN], job->class->reload_signal) < 0)
nih_return_system_error (-1);
NIH_ZERO (job_reload_reply (message));
return 0;
}
/**
* job_get_name:
* @job: job to obtain name from,
* @message: D-Bus connection and message received,
* @name: pointer for reply string.
*
* Implements the get method for the name property of the
* com.ubuntu.Upstart.Instance interface.
*
* Called to obtain the instance name of the given @job, which will be stored
* in @name.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_get_name (Job *job,
NihDBusMessage *message,
char **name)
{
nih_assert (job != NULL);
nih_assert (message != NULL);
nih_assert (name != NULL);
*name = job->name;
nih_ref (*name, message);
return 0;
}
/**
* job_get_goal:
* @job: job to obtain goal from,
* @message: D-Bus connection and message received,
* @goal: pointer for reply string.
*
* Implements the get method for the goal property of the
* com.ubuntu.Upstart.Instance interface.
*
* Called to obtain the goal of the given @job as a string, which will be
* stored in @goal.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_get_goal (Job *job,
NihDBusMessage *message,
char **goal)
{
nih_assert (job != NULL);
nih_assert (message != NULL);
nih_assert (goal != NULL);
*goal = nih_strdup (message, job_goal_name (job->goal));
if (! *goal)
nih_return_no_memory_error (-1);
return 0;
}
/**
* job_get_state:
* @job: job to obtain state from,
* @message: D-Bus connection and message received,
* @state: pointer for reply string.
*
* Implements the get method for the state property of the
* com.ubuntu.Upstart.Instance interface.
*
* Called to obtain the state of the given @job as a string, which will be
* stored in @state.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_get_state (Job *job,
NihDBusMessage *message,
char **state)
{
nih_assert (job != NULL);
nih_assert (message != NULL);
nih_assert (state != NULL);
*state = nih_strdup (message, job_state_name (job->state));
if (! *state)
nih_return_no_memory_error (-1);
return 0;
}
/**
* job_get_processes:
* @job: job to obtain state from,
* @message: D-Bus connection and message received,
* @processes: pointer for reply array.
*
* Implements the get method for the processes property of the
* com.ubuntu.Upstart.Instance interface.
*
* Called to obtain the current set of processes for the given @job as an
* array of process names and pids, which will be stored in @processes.
*
* Returns: zero on success, negative value on raised error.
**/
int
job_get_processes (Job * job,
NihDBusMessage * message,
JobProcessesElement ***processes)
{
size_t num_processes;
nih_assert (job != NULL);
nih_assert (message != NULL);
nih_assert (processes != NULL);
*processes = nih_alloc (message, sizeof (JobProcessesElement *) * 1);
if (! *processes)
nih_return_no_memory_error (-1);
num_processes = 0;
(*processes)[num_processes] = NULL;
for (int i = 0; i < PROCESS_LAST; i++) {
JobProcessesElement * process;
JobProcessesElement **tmp;
if (job->pid[i] <= 0)
continue;
process = nih_new (*processes, JobProcessesElement);
if (! process) {
nih_error_raise_no_memory ();
nih_free (*processes);
return -1;
}
process->item0 = nih_strdup (process, process_name (i));
if (! process->item0) {
nih_error_raise_no_memory ();
nih_free (*processes);
return -1;
}
process->item1 = job->pid[i];
tmp = nih_realloc (*processes, message,
(sizeof (JobProcessesElement *)
* (num_processes + 2)));
if (! tmp) {
nih_error_raise_no_memory ();
nih_free (*processes);
return -1;
}
*processes = tmp;
(*processes)[num_processes++] = process;
(*processes)[num_processes] = NULL;
}
return 0;
}
/**
* job_serialise:
* @job: job serialise.
*
* Convert @job into a JSON representation for serialisation.
* Caller must free returned value using json_object_put().
*
* Note that the 'class' element is not encoded - it is assumed the
* caller will encode the returned JSON Job object as a child of the
* parent JSON-encoded JobClass object so a reference is not required.
*
* Returns: JSON-serialised Job object, or NULL on error.
**/
json_object *
job_serialise (const Job *job)
{
json_object *json;
json_object *json_pid;
json_object *json_fds;
json_object *json_logs;
json_object *json_handler_data;
nih_assert (job);
json = json_object_new_object ();
if (! json)
return NULL;
if (! state_set_json_string_var_from_obj (json, job, name))
goto error;
if (! state_set_json_string_var_from_obj (json, job, path))
goto error;
if (! state_set_json_enum_var (json,
job_goal_enum_to_str,
"goal", job->goal))
goto error;
if (! state_set_json_enum_var (json,
job_state_enum_to_str,
"state", job->state))
goto error;
if (! state_set_json_str_array_from_obj (json, job, env))
goto error;
if (! state_set_json_str_array_from_obj (json, job, start_env))
goto error;
if (! state_set_json_str_array_from_obj (json, job, stop_env))
goto error;
if (job->stop_on) {
json_object *json_stop_on;
json_stop_on = event_operator_serialise_all (job->stop_on);
if (! json_stop_on)
goto error;
json_object_object_add (json, "stop_on", json_stop_on);
}
json_fds = state_serialise_int_array (int, job->fds, job->num_fds);
if (! json_fds)
goto error;
json_object_object_add (json, "fds", json_fds);
json_pid = state_serialise_int_array (pid_t, job->pid,
PROCESS_LAST);
if (! json_pid)
goto error;
json_object_object_add (json, "pid", json_pid);
/* Encode the blocking event as an index number which represents
* the event's position in the JSON events array.
*/
if (job->blocker) {
int event_index;
event_index = event_to_index (job->blocker);
if (event_index < 0)
goto error;
/* For consistency, it would be preferable to encode the
* event name, but the index is actually better since it is
* simple and unambiguous - encoding the name would also require
* us to encode the env to make the event unique.
*/
if (! state_set_json_int_var (json, "blocker", event_index))
goto error;
}
if (! NIH_LIST_EMPTY (&job->blocking)) {
json_object *json_blocking;
json_blocking = state_serialise_blocking (&job->blocking);
if (! json_blocking)
goto error;
json_object_object_add (json, "blocking", json_blocking);
}
/* conditionally encode kill timer */
if (job->kill_timer) {
json_object *kill_timer;
kill_timer = job_serialise_kill_timer (job->kill_timer);
if (! kill_timer)
goto error;
json_object_object_add (json, "kill_timer", kill_timer);
}
if (! state_set_json_enum_var (json,
process_type_enum_to_str,
"kill_process", job->kill_process))
goto error;
if (! state_set_json_int_var_from_obj (json, job, failed))
goto error;
if (! state_set_json_enum_var (json,
process_type_enum_to_str,
"failed_process", job->failed_process))
goto error;
if (! state_set_json_int_var_from_obj (json, job, exit_status))
goto error;
if (! state_set_json_int_var_from_obj (json, job, respawn_time))
goto error;
if (! state_set_json_int_var_from_obj (json, job, respawn_count))
goto error;
if (! state_set_json_int_var_from_obj (json, job, trace_forks))
goto error;
if (! state_set_json_enum_var (json,
job_trace_state_enum_to_str,
"trace_state", job->trace_state))
goto error;
json_logs = json_object_new_array ();
if (! json_logs)
return json;
for (int process = 0; process < PROCESS_LAST; process++) {
json_object *json_log;
json_log = log_serialise (job->log[process]);
if (! json_log)
goto error;
if (json_object_array_add (json_logs, json_log) < 0)
goto error;
}
json_object_object_add (json, "log", json_logs);
json_handler_data = json_object_new_array ();
if (! json_handler_data)
return json;
for (int process = 0; process < PROCESS_LAST; process++) {
json_object *json_data = NULL;
/* Only bother serialising if the process data hasn't
* been handled yet.
*/
if (job->process_data[process] && job->process_data[process]->valid) {
json_data = job_process_data_serialise (job,
job->process_data[process]);
if (! json_data)
goto error;
}
if (json_object_array_add (json_handler_data, json_data) < 0)
goto error;
}
json_object_object_add (json, "process_data", json_handler_data);
return json;
error:
json_object_put (json);
return NULL;
}
/**
* job_serialise_all:
*
* Convert existing Job objects to JSON representation.
*
* Returns: JSON object containing array of Job objects, or NULL on error.
**/
json_object *
job_serialise_all (const NihHash *jobs)
{
json_object *json;
nih_assert (jobs);
json = json_object_new_array ();
if (! json)
return NULL;
NIH_HASH_FOREACH (jobs, iter) {
json_object *json_job;
Job *job = (Job *)iter;
json_job = job_serialise (job);
if (! json_job)
goto error;
json_object_array_add (json, json_job);
}
return json;
error:
json_object_put (json);
return NULL;
}
/**
* job_deserialise:
* @parent: job class for JSON-encoded jobs,
* @json: JSON-serialised Job object to deserialise.
*
* XXX: All events must have been deserialised prior to this function
* XXX: being called.
*
* Returns: Job object, or NULL on error.
**/
Job *
job_deserialise (JobClass *parent, json_object *json)
{
nih_local char *name = NULL;
Job *job = NULL;
json_object *json_kill_timer;
json_object *json_fds;
json_object *json_pid;
json_object *json_logs;
json_object *json_process_data;
json_object *json_stop_on = NULL;
size_t len;
int ret;
nih_assert (parent);
nih_assert (json);
if (! state_check_json_type (json, object))
return NULL;
if (! state_get_json_string_var_strict (json, "name", NULL, name))
goto error;
job = NIH_MUST (job_new (parent, name));
if (! job)
return NULL;
if (! state_get_json_string_var_to_obj_strict (json, job, path))
goto error;
if (! state_get_json_enum_var (json,
job_goal_str_to_enum,
"goal", job->goal))
goto error;
if (! state_get_json_enum_var (json,
job_state_str_to_enum,
"state", job->state))
goto error;
if (! state_get_json_env_array_to_obj (json, job, env))
goto error;
if (! state_get_json_env_array_to_obj (json, job, start_env))
goto error;
if (! state_get_json_env_array_to_obj (json, job, stop_env))
goto error;
if (json_object_object_get_ex (json, "stop_on", &json_stop_on)) {
if (state_check_json_type (json_stop_on, array)) {
job->stop_on = event_operator_deserialise_all (job, json_stop_on);
if (! job->stop_on)
goto error;
} else {
nih_local char *stop_on = NULL;
/* Old format (string)
*
* Note that we re-search for the JSON key here
* (json, rather than json_stop_on) to allow
* the use of the convenience macro. This is
* of course slower, but its a legacy scenario.
*/
if (! state_get_json_string_var_strict (json, "stop_on", NULL, stop_on))
goto error;
if (*stop_on) {
nih_local JobClass *tmp = NULL;
tmp = NIH_MUST (job_class_new (NULL, "tmp", NULL));
tmp->stop_on = parse_on_simple (tmp, "stop", stop_on);
if (! tmp->stop_on) {
NihError *err;
err = nih_error_get ();
nih_error ("%s %s: %s",
_("BUG"),
_("instance 'stop on' parse error"),
err->message);
nih_free (err);
goto error;
}
nih_free (job->stop_on);
job->stop_on = event_operator_copy (job, tmp->stop_on);
if (! job->stop_on)
goto error;
}
}
}
/* fds and num_fds handled by caller */
/* pid handled by caller */
/* blocking is handled by state_deserialise_blocking() */
if (json_object_object_get_ex (json, "blocker", NULL)) {
int event_index = -1;
if (! state_get_json_int_var (json, "blocker", event_index))
goto error;
job->blocker = event_from_index (event_index);
if (! job->blocker)
goto error;
}
if (! state_get_json_enum_var (json,
process_type_str_to_enum,
"kill_process", job->kill_process))
goto error;
/* Check to see if a kill timer exists first since we do not
* want to end up creating a real but empty timer.
*/
if (json_object_object_get_ex (json, "kill_timer", &json_kill_timer)) {
/* Found a partial kill timer, so create a new one and
* adjust its due time. By the time the main loop gets
* called, the due time will probably be in the past
* such that the job will be stopped.
*
* To be completely fair we should:
*
* - encode the time at the point of serialisation in a
* JSON 'meta' header.
* - query the time post-deserialisation and calculate
* the delta (being the time to perform the stateful
* re-exec).
* - add that time to all jobs with active kill timers
* to give their processes the full amount of time to
* end.
*/
nih_local NihTimer *kill_timer = job_deserialise_kill_timer (json_kill_timer);
if (! kill_timer)
goto error;
nih_assert (job->kill_process);
job_process_set_kill_timer (job, job->kill_process,
kill_timer->timeout);
job_process_adj_kill_timer (job, kill_timer->due);
}
if (! state_get_json_int_var_to_obj (json, job, failed))
goto error;
if (! state_get_json_enum_var (json,
process_type_str_to_enum,
"failed_process", job->failed_process))
goto error;
if (! state_get_json_int_var_to_obj (json, job, exit_status))
goto error;
if (! state_get_json_int_var_to_obj (json, job, respawn_time))
goto error;
if (! state_get_json_int_var_to_obj (json, job, respawn_count))
goto error;
if (! json_object_object_get_ex (json, "fds", &json_fds))
goto error;
ret = state_deserialise_int_array (job, json_fds,
int, &job->fds, &job->num_fds);
if (ret < 0)
goto error;
if (! json_object_object_get_ex (json, "pid", &json_pid))
goto error;
ret = state_deserialise_int_array (job, json_pid,
pid_t, &job->pid, &len);
if (ret < 0)
goto error;
/* If we are missing one, we're probably importing from a
* previous version that didn't include PROCESS_SECURITY.
* Simply add the missing one.
*/
if (len == PROCESS_LAST - 1) {
job->pid = nih_realloc (job->pid, job, sizeof (pid_t) * PROCESS_LAST);
if (! job->pid)
goto error;
job->pid[PROCESS_LAST - 1] = 0;
} else if (len != PROCESS_LAST) {
goto error;
}
if (! state_get_json_int_var_to_obj (json, job, trace_forks))
goto error;
if (! state_get_json_enum_var (json,
job_trace_state_str_to_enum,
"trace_state", job->trace_state))
goto error;
if (! json_object_object_get_ex (json, "log", &json_logs))
goto error;
if (! state_check_json_type (json_logs, array))
goto error;
for (int process = 0; process < PROCESS_LAST; process++) {
json_object *json_log;
json_log = json_object_array_get_idx (json_logs, process);
if (json_log) {
/* NULL if there was no log configured, or we failed to
* deserialise it; either way, this should be non-fatal.
*/
job->log[process] = log_deserialise (job->log, json_log);
} else {
/* If we are missing one, we're probably importing from a
* previous version that didn't include PROCESS_SECURITY.
* Simply ignore the missing one.
*/
if (process == PROCESS_LAST - 1) {
job->log[process] = NULL;
} else {
goto error;
}
}
}
if (json_object_object_get_ex (json, "process_data", &json_process_data)) {
if (! state_check_json_type (json_process_data, array))
goto error;
for (int process = 0; process < PROCESS_LAST; process++) {
json_object *json_data = NULL;
json_data = json_object_array_get_idx (json_process_data, process);
if (json_data) {
/* NULL if there was no process_data for this job process, or we failed to
* deserialise it; either way, this should be non-fatal.
*/
job->process_data[process] =
job_process_data_deserialise (job->process_data, job, json_data);
if (! job->process_data[process])
goto error;
/* Recreate watch */
if (job->process_data[process]->valid) {
job_register_child_handler (job->process_data[process],
job->process_data[process]->job_process_fd,
job->process_data[process]);
}
} else {
job->process_data[process] = NULL;
}
}
}
return job;
error:
nih_free (job);
return NULL;
}
/**
* job_deserialise_all:
*
* @parent: job class for JSON-encoded jobs,
* @json: root of JSON-serialised state.
*
* Convert JSON representation of jobs back into Job objects associated
* with @parent.
*
* Returns: 0 on success, -1 on error.
**/
int
job_deserialise_all (JobClass *parent, json_object *json)
{
json_object *json_jobs;
Job *job;
nih_assert (parent);
nih_assert (json);
if (! json_object_object_get_ex (json, "jobs", &json_jobs))
goto error;
if (! state_check_json_type (json_jobs, array))
goto error;
for (int i = 0; i < json_object_array_length (json_jobs); i++) {
json_object *json_job;
json_job = json_object_array_get_idx (json_jobs, i);
if (! json_job)
goto error;
if (! state_check_json_type (json_job, object))
goto error;
job = job_deserialise (parent, json_job);
if (! job)
goto error;
}
return 0;
error:
return -1;
}
/**
* job_goal_enum_to_str:
*
* @goal: JobGoal.
*
* Convert JobGoal to a string representation.
*
* Returns: string representation of @goal, or NULL if not known.
**/
static const char *
job_goal_enum_to_str (JobGoal goal)
{
state_enum_to_str (JOB_STOP, goal);
state_enum_to_str (JOB_START, goal);
state_enum_to_str (JOB_RESPAWN, goal);
return NULL;
}
/**
* job_goal_str_to_enum:
*
* @goal: string JobGoal value.
*
* Convert @goal back into enum value.
*
* Returns: JobGoal representation of @goal, or -1 if not known.
**/
static JobGoal
job_goal_str_to_enum (const char *goal)
{
state_str_to_enum (JOB_STOP, goal);
state_str_to_enum (JOB_START, goal);
state_str_to_enum (JOB_RESPAWN, goal);
return -1;
}
/**
* job_state_enum_to_str:
*
* @state: JobState.
*
* Convert JobState to a string representation.
*
* Returns: string representation of @state, or NULL if not known.
**/
const char *
job_state_enum_to_str (JobState state)
{
state_enum_to_str (JOB_WAITING, state);
state_enum_to_str (JOB_STARTING, state);
state_enum_to_str (JOB_SECURITY_SPAWNING, state);
state_enum_to_str (JOB_SECURITY, state);
state_enum_to_str (JOB_PRE_STARTING, state);
state_enum_to_str (JOB_PRE_START, state);
state_enum_to_str (JOB_SPAWNING, state);
state_enum_to_str (JOB_SPAWNED, state);
state_enum_to_str (JOB_POST_STARTING, state);
state_enum_to_str (JOB_POST_START, state);
state_enum_to_str (JOB_RUNNING, state);
state_enum_to_str (JOB_PRE_STOPPING, state);
state_enum_to_str (JOB_PRE_STOP, state);
state_enum_to_str (JOB_STOPPING, state);
state_enum_to_str (JOB_KILLED, state);
state_enum_to_str (JOB_POST_STOPPING, state);
state_enum_to_str (JOB_POST_STOP, state);
return NULL;
}
/**
* job_state_str_to_enum:
*
* @state: string JobState value.
*
* Convert @state back into enum value.
*
* Returns: JobState representation of @state, or -1 if not known.
**/
JobState
job_state_str_to_enum (const char *state)
{
state_str_to_enum (JOB_WAITING, state);
state_str_to_enum (JOB_STARTING, state);
state_str_to_enum (JOB_SECURITY_SPAWNING, state);
state_str_to_enum (JOB_SECURITY, state);
state_str_to_enum (JOB_PRE_STARTING, state);
state_str_to_enum (JOB_PRE_START, state);
state_str_to_enum (JOB_SPAWNING, state);
state_str_to_enum (JOB_SPAWNED, state);
state_str_to_enum (JOB_POST_STARTING, state);
state_str_to_enum (JOB_POST_START, state);
state_str_to_enum (JOB_RUNNING, state);
state_str_to_enum (JOB_PRE_STOPPING, state);
state_str_to_enum (JOB_PRE_STOP, state);
state_str_to_enum (JOB_STOPPING, state);
state_str_to_enum (JOB_KILLED, state);
state_str_to_enum (JOB_POST_STOPPING, state);
state_str_to_enum (JOB_POST_STOP, state);
return -1;
}
/**
* job_state_enum_to_str:
*
* @state: TraceState.
*
* Convert TraceState to a string representation.
*
* Returns: string representation of @state, or NULL if not known.
**/
static const char *
job_trace_state_enum_to_str (TraceState state)
{
state_enum_to_str (TRACE_NONE, state);
state_enum_to_str (TRACE_NEW, state);
state_enum_to_str (TRACE_NEW_CHILD, state);
state_enum_to_str (TRACE_NORMAL, state);
return NULL;
}
/**
* job_trace_state_str_to_enum:
*
* @state: string TraceState value.
*
* Convert @state back into enum value.
*
* Returns: TraceState representation of @state, or -1 if not known.
**/
static TraceState
job_trace_state_str_to_enum (const char *state)
{
state_str_to_enum (TRACE_NONE, state);
state_str_to_enum (TRACE_NEW, state);
state_str_to_enum (TRACE_NEW_CHILD, state);
state_str_to_enum (TRACE_NORMAL, state);
return -1;
}
/**
* job_serialise_kill_timer:
*
* @timer: NihTimer to serialise.
*
* Serialise @timer into JSON.
*
* Returns: JSON-serialised NihTimer object, or NULL on error.
**/
static json_object *
job_serialise_kill_timer (NihTimer *timer)
{
json_object *json;
nih_assert (timer);
json = json_object_new_object ();
if (! json)
return NULL;
if (! state_set_json_int_var_from_obj (json, timer, timeout))
goto error;
if (! state_set_json_int_var_from_obj (json, timer, due))
goto error;
return json;
error:
json_object_put (json);
return NULL;
}
/**
* job_deserialise_kill_timer:
*
* @json: JSON representation of NihTimer.
*
* Deserialise @json back into an NihTimer.
*
* Returns: NihTimer on NULL on error.
**/
static NihTimer *
job_deserialise_kill_timer (json_object *json)
{
NihTimer *timer;
nih_assert (json);
timer = nih_new (NULL, NihTimer);
if (! timer)
return NULL;
memset (timer, '\0', sizeof (NihTimer));
if (! state_get_json_int_var_to_obj (json, timer, due))
goto error;
if (! state_get_json_int_var_to_obj (json, timer, timeout))
goto error;
return timer;
error:
nih_free (timer);
return NULL;
}
/**
* job_find:
*
* @session: session of job class,
* @job_class: name of job class,
* @job_name: name of job instance.
*
* Lookup job based on parent class name and
* job instance name.
*
* Returns: existing Job on success, or NULL if job class or
* job are not found in @session.
**/
Job *
job_find (const Session *session,
JobClass *class,
const char *job_class,
const char *job_name)
{
Job *job;
nih_assert (class || job_class);
nih_assert (job_classes);
if (! job_name)
goto error;
if (! class)
class = job_class_get_registered (job_class, session);
if (! class)
goto error;
job = (Job *)nih_hash_lookup (class->instances, job_name);
if (! job)
goto error;
return job;
error:
return NULL;
}
/**
* job_needs_cgroups:
*
* @job: job.
*
* Determine if specified job requires cgroups.
*
* Returns: TRUE if @job needs atleast 1 cgroup, else FALSE.
**/
int
job_needs_cgroups (const Job *job)
{
nih_assert (job);
#ifdef ENABLE_CGROUPS
return job_class_cgroups (job->class);
#else
/* No cgroup support */
return FALSE;
#endif /* ENABLE_CGROUPS */
}
/**
* job_child_error_handler:
*
* @job: job,
* @process: process that failed to start.
*
* JobProcessErrorHandler that deals with errors resulting from
* a failure to start a job process.
**/
void
job_child_error_handler (Job *job, ProcessType process)
{
nih_assert (job);
nih_assert (process > PROCESS_INVALID);
nih_assert (process < PROCESS_LAST);
job->pid[process] = 0;
switch (process) {
case PROCESS_SECURITY:
job_failed (job, PROCESS_SECURITY, -1);
job_change_goal (job, JOB_STOP);
break;
case PROCESS_PRE_START:
job_failed (job, PROCESS_PRE_START, -1);
job_change_goal (job, JOB_STOP);
job_change_state (job, job_next_state (job));
break;
case PROCESS_MAIN:
job_failed (job, PROCESS_MAIN, -1);
job_change_goal (job, JOB_STOP);
job_change_state (job, job_next_state (job));
break;
case PROCESS_POST_START:
job_change_state (job, job_next_state (job));
break;
case PROCESS_PRE_STOP:
job_change_state (job, job_next_state (job));
break;
case PROCESS_POST_STOP:
job_failed (job, PROCESS_POST_STOP, -1);
job_change_goal (job, JOB_STOP);
job_change_state (job, job_next_state (job));
break;
default:
nih_assert_not_reached ();
}
}
#ifdef ENABLE_CGROUPS
/**
* job_last_process:
*
* @job: job,
* @process: process.
*
* Returns: TRUE if the last defined process for @job is @process,
* else FALSE.
**/
int
job_last_process (const Job *job, ProcessType process)
{
ProcessType i;
ProcessType last = PROCESS_INVALID;
nih_assert (job);
nih_assert (process >= PROCESS_MAIN);
nih_assert (process < PROCESS_LAST);
for (i = 0; i < PROCESS_LAST; i++) {
if (job->class->process[i])
last = i;
}
return last == process ? TRUE : FALSE;
}
#endif /* ENABLE_CGROUPS */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment