Files
systemd/src/sysupdate/sysupdated.c
Mike Yuan 74cd56d31b notify-recv: several followups
Follow-up for 7f6af95dab

- Allocate internal buf on the stack, memdup() only at the end.
  This ensures we're able to handle OOM gracefully, i.e.
  return -EAGAIN on OOM while still emptying socket buffer.
- Do not treat empty notify message as error.
- Raise log level since all callers log loudly anyway.
2025-02-26 13:02:23 +01:00

2125 lines
67 KiB
C

/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-bus.h"
#include "sd-json.h"
#include "build-path.h"
#include "bus-common-errors.h"
#include "bus-error.h"
#include "bus-get-properties.h"
#include "bus-label.h"
#include "bus-log-control-api.h"
#include "bus-polkit.h"
#include "bus-util.h"
#include "common-signal.h"
#include "discover-image.h"
#include "dropin.h"
#include "env-util.h"
#include "escape.h"
#include "event-util.h"
#include "fd-util.h"
#include "fileio.h"
#include "hashmap.h"
#include "log.h"
#include "main-func.h"
#include "memfd-util.h"
#include "mkdir-label.h"
#include "notify-recv.h"
#include "os-util.h"
#include "process-util.h"
#include "service-util.h"
#include "signal-util.h"
#include "socket-util.h"
#include "string-table.h"
#include "sysupdate-util.h"
#define FEATURES_DROPIN_NAME "systemd-sysupdate-enabled"
typedef struct Manager {
sd_event *event;
sd_bus *bus;
Hashmap *targets;
uint64_t last_job_id;
Hashmap *jobs;
Hashmap *polkit_registry;
sd_event_source *notify_event;
RuntimeScope runtime_scope; /* For now only RUNTIME_SCOPE_SYSTEM */
} Manager;
/* Forward declare so that jobs can call it on exit */
static void manager_check_idle(Manager *m);
typedef enum TargetClass {
/* These should try to match ImageClass from src/basic/os-util.h */
TARGET_MACHINE = IMAGE_MACHINE,
TARGET_PORTABLE = IMAGE_PORTABLE,
TARGET_SYSEXT = IMAGE_SYSEXT,
TARGET_CONFEXT = IMAGE_CONFEXT,
_TARGET_CLASS_IS_IMAGE_CLASS_MAX,
/* sysupdate-specific classes */
TARGET_HOST = _TARGET_CLASS_IS_IMAGE_CLASS_MAX,
TARGET_COMPONENT,
_TARGET_CLASS_MAX,
_TARGET_CLASS_INVALID = -EINVAL,
} TargetClass;
/* Let's ensure when the number of classes is updated things are updated here too */
assert_cc((int) _IMAGE_CLASS_MAX == (int) _TARGET_CLASS_IS_IMAGE_CLASS_MAX);
typedef struct Target {
Manager *manager;
TargetClass class;
char *name;
char *path;
char *id;
ImageType image_type;
bool busy;
} Target;
typedef enum JobType {
JOB_LIST,
JOB_DESCRIBE,
JOB_CHECK_NEW,
JOB_UPDATE,
JOB_VACUUM,
JOB_DESCRIBE_FEATURE,
_JOB_TYPE_MAX,
_JOB_TYPE_INVALID = -EINVAL,
} JobType;
typedef struct Job Job;
typedef int (*JobReady)(sd_bus_message *msg, const Job *job);
typedef int (*JobComplete)(sd_bus_message *msg, const Job *job, sd_json_variant *response, sd_bus_error *error);
struct Job {
Manager *manager;
Target *target;
uint64_t id;
char *object_path;
JobType type;
bool offline;
char *version; /* Passed into sysupdate for JOB_DESCRIBE and JOB_UPDATE */
char *feature; /* Passed into sysupdate for JOB_DESCRIBE_FEATURE */
unsigned progress_percent;
sd_event_source *child;
int stdout_fd;
int status_errno;
unsigned n_cancelled;
sd_json_variant *json;
JobComplete complete_cb; /* Callback called on job exit */
sd_bus_message *dbus_msg;
JobReady detach_cb; /* Callback called when job has started. Detaches the job to run in the background */
};
static const char* const target_class_table[_TARGET_CLASS_MAX] = {
[TARGET_MACHINE] = "machine",
[TARGET_PORTABLE] = "portable",
[TARGET_SYSEXT] = "sysext",
[TARGET_CONFEXT] = "confext",
[TARGET_COMPONENT] = "component",
[TARGET_HOST] = "host",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(target_class, TargetClass);
static const char* const job_type_table[_JOB_TYPE_MAX] = {
[JOB_LIST] = "list",
[JOB_DESCRIBE] = "describe",
[JOB_CHECK_NEW] = "check-new",
[JOB_UPDATE] = "update",
[JOB_VACUUM] = "vacuum",
[JOB_DESCRIBE_FEATURE] = "describe-feature",
};
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_TO_STRING(job_type, JobType);
static Job *job_free(Job *j) {
if (!j)
return NULL;
if (j->manager)
assert_se(hashmap_remove(j->manager->jobs, &j->id) == j);
free(j->object_path);
free(j->version);
free(j->feature);
sd_json_variant_unref(j->json);
sd_bus_message_unref(j->dbus_msg);
sd_event_source_disable_unref(j->child);
safe_close(j->stdout_fd);
return mfree(j);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(Job*, job_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(job_hash_ops, uint64_t, uint64_hash_func, uint64_compare_func,
Job, job_free);
static int job_new(JobType type, Target *t, sd_bus_message *msg, JobComplete complete_cb, Job **ret) {
_cleanup_(job_freep) Job *j = NULL;
int r;
assert(t);
assert(ret);
j = new(Job, 1);
if (!j)
return -ENOMEM;
*j = (Job) {
.type = type,
.target = t,
.id = t->manager->last_job_id + 1,
.stdout_fd = -EBADF,
.complete_cb = complete_cb,
.dbus_msg = sd_bus_message_ref(msg),
};
if (asprintf(&j->object_path, "/org/freedesktop/sysupdate1/job/_%" PRIu64, j->id) < 0)
return -ENOMEM;
r = hashmap_ensure_put(&t->manager->jobs, &job_hash_ops, &j->id, j);
if (r < 0)
return r;
j->manager = t->manager;
t->manager->last_job_id = j->id;
*ret = TAKE_PTR(j);
return 0;
}
static int job_parse_child_output(int _fd, sd_json_variant **ret) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
/* Take ownership of the passed fd */
_cleanup_close_ int fd = _fd;
_cleanup_fclose_ FILE *f = NULL;
struct stat st;
int r;
assert(ret);
if (fstat(fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat stdout fd: %m");
assert(S_ISREG(st.st_mode));
if (st.st_size == 0) {
log_warning("No output from child job, ignoring");
return 0;
}
if (lseek(fd, SEEK_SET, 0) == (off_t) -1)
return log_debug_errno(errno, "Failed to seek to beginning of memfd: %m");
f = take_fdopen(&fd, "r");
if (!f)
return log_debug_errno(errno, "Failed to reopen memfd: %m");
r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL);
if (r < 0)
return log_debug_errno(r, "Failed to parse JSON: %m");
*ret = TAKE_PTR(v);
return 0;
}
static void job_on_ready(Job *j) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *msg = NULL;
int r;
assert(j);
/* Some jobs run in the background as we return the job ID to the dbus caller (i.e. for the Update
* method). However, the worker will perform some sanity-checks on startup which would be valuable
* as dbus errors. So, we wait for the worker to signal via READY=1 that it has completed its sanity
* checks and we should continue the job in the background. */
if (!j->detach_cb)
return;
log_debug("Got READY=1 from job %" PRIu64", detaching.", j->id);
assert(j->dbus_msg);
msg = TAKE_PTR(j->dbus_msg);
j->complete_cb = NULL;
r = j->detach_cb(msg, j);
if (r < 0)
log_warning_errno(r, "Failed to detach job %" PRIu64 ", ignoring: %m", j->id);
}
static void job_on_errno(Job *j, const char *buf) {
int r;
assert(j);
assert(buf);
r = parse_errno(buf);
if (r < 0) {
log_warning_errno(r, "Got invalid errno value from job %" PRIu64 ", ignoring: %m", j->id);
return;
}
j->status_errno = r;
log_debug_errno(r, "Got errno from job %" PRIu64 ": %i (%m)", j->id, r);
}
static void job_on_progress(Job *j, const char *buf) {
unsigned progress;
int r;
assert(j);
assert(buf);
r = safe_atou(buf, &progress);
if (r < 0 || progress > 100) {
log_warning("Got invalid percent value, ignoring.");
return;
}
j->progress_percent = progress;
(void) sd_bus_emit_properties_changed(j->manager->bus, j->object_path,
"org.freedesktop.sysupdate1.Job",
"Progress", NULL);
log_debug("Got percentage from job %" PRIu64 ": %u%%", j->id, j->progress_percent);
}
static void job_on_version(Job *j, const char *version) {
assert(j);
assert(version);
if (free_and_strdup_warn(&j->version, version) < 0)
return;
log_debug("Got version from job %" PRIu64 ": %s ", j->id, j->version);
}
static int job_on_exit(sd_event_source *s, const siginfo_t *si, void *userdata) {
Job *j = ASSERT_PTR(userdata);
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
Manager *manager = j->manager;
int r;
assert(j);
assert(s);
assert(si);
if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM)) {
assert(j->target->busy);
j->target->busy = false;
}
if (si->si_code != CLD_EXITED) {
assert(IN_SET(si->si_code, CLD_KILLED, CLD_DUMPED));
sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED,
"Job terminated abnormally with signal %s.",
signal_to_string(si->si_status));
} else if (si->si_status != EXIT_SUCCESS)
if (j->status_errno != 0)
sd_bus_error_set_errno(&error, j->status_errno);
else
sd_bus_error_setf(&error, SD_BUS_ERROR_FAILED,
"Job failed with exit code %i.", si->si_status);
else {
r = job_parse_child_output(TAKE_FD(j->stdout_fd), &json);
if (r < 0)
sd_bus_error_set_errnof(&error, r, "Failed to parse job worker output: %m");
}
/* Only send notification of exit if the job was actually detached */
if (j->detach_cb) {
r = sd_bus_emit_signal(
j->manager->bus,
"/org/freedesktop/sysupdate1",
"org.freedesktop.sysupdate1.Manager",
"JobRemoved",
"toi",
j->id,
j->object_path,
j->status_errno != 0 ? -j->status_errno : si->si_status);
if (r < 0)
log_warning_errno(r,
"Cannot emit JobRemoved message for job %" PRIu64 ", ignoring: %m",
j->id);
}
if (j->dbus_msg && j->complete_cb) {
if (sd_bus_error_is_set(&error)) {
log_warning("Job %" PRIu64 " failed with bus error, ignoring callback: %s",
j->id, error.message);
sd_bus_reply_method_error(j->dbus_msg, &error);
} else {
r = j->complete_cb(j->dbus_msg, j, json, &error);
if (r < 0) {
log_warning_errno(r,
"Error during execution of job callback for job %" PRIu64 ": %s",
j->id,
bus_error_message(&error, r));
sd_bus_reply_method_errno(j->dbus_msg, r, &error);
}
}
}
job_free(j);
if (manager)
manager_check_idle(manager);
return 0;
}
static inline const char* sysupdate_binary_path(void) {
return secure_getenv("SYSTEMD_SYSUPDATE_PATH") ?: SYSTEMD_SYSUPDATE_PATH;
}
static int target_get_argument(Target *t, char **ret) {
_cleanup_free_ char *target_arg = NULL;
assert(t);
assert(ret);
if (t->class != TARGET_HOST) {
if (t->class == TARGET_COMPONENT)
target_arg = strjoin("--component=", t->name);
else if (IN_SET(t->image_type, IMAGE_DIRECTORY, IMAGE_SUBVOLUME))
target_arg = strjoin("--root=", t->path);
else if (IN_SET(t->image_type, IMAGE_RAW, IMAGE_BLOCK))
target_arg = strjoin("--image=", t->path);
else
assert_not_reached();
if (!target_arg)
return -ENOMEM;
}
*ret = TAKE_PTR(target_arg);
return 0;
}
static int job_start(Job *j) {
_cleanup_close_ int stdout_fd = -EBADF;
_cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
int r;
assert(j);
if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM) && j->target->busy)
return log_notice_errno(SYNTHETIC_ERRNO(EBUSY), "Target %s busy, ignoring job.", j->target->name);
stdout_fd = memfd_new("sysupdate-stdout");
if (stdout_fd < 0)
return log_error_errno(stdout_fd, "Failed to create memfd: %m");
r = pidref_safe_fork_full("(sd-sysupdate)",
(int[]) { -EBADF, stdout_fd, STDERR_FILENO }, NULL, 0,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|
FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG, &pid);
if (r < 0)
return r; /* FORK_LOG means pidref_safe_fork_full will handle the logging */
if (r == 0) {
/* Child */
_cleanup_free_ char *target_arg = NULL;
const char *cmd[] = {
"systemd-sysupdate",
"--json=short",
NULL, /* maybe --verify=no */
NULL, /* maybe --component=, --root=, or --image= */
NULL, /* maybe --offline */
NULL, /* list, check-new, update, vacuum, features */
NULL, /* maybe version (for list, update), maybe feature (features) */
NULL
};
size_t k = 2;
if (setenv("NOTIFY_SOCKET", "/run/systemd/sysupdate/notify", /* overwrite= */ 1) < 0) {
log_error_errno(errno, "setenv() failed: %m");
_exit(EXIT_FAILURE);
}
if (getenv_bool("SYSTEMD_SYSUPDATE_NO_VERIFY") > 0)
cmd[k++] = "--verify=no"; /* For testing */
r = setenv_systemd_exec_pid(true);
if (r < 0)
log_warning_errno(r, "Failed to update $SYSTEMD_EXEC_PID, ignoring: %m");
r = target_get_argument(j->target, &target_arg);
if (r < 0) {
log_oom();
_exit(EXIT_FAILURE);
}
if (target_arg)
cmd[k++] = target_arg;
if (j->offline)
cmd[k++] = "--offline";
switch (j->type) {
case JOB_LIST:
cmd[k++] = "list";
break;
case JOB_DESCRIBE:
cmd[k++] = "list";
assert(!isempty(j->version));
cmd[k++] = j->version;
break;
case JOB_CHECK_NEW:
cmd[k++] = "check-new";
break;
case JOB_UPDATE:
cmd[k++] = "update";
cmd[k++] = empty_to_null(j->version);
break;
case JOB_VACUUM:
cmd[k++] = "vacuum";
break;
case JOB_DESCRIBE_FEATURE:
cmd[k++] = "features";
assert(!isempty(j->feature));
cmd[k++] = j->feature;
break;
default:
assert_not_reached();
}
if (DEBUG_LOGGING) {
_cleanup_free_ char *s = NULL;
s = quote_command_line((char**) cmd, SHELL_ESCAPE_EMPTY);
if (!s) {
log_oom();
_exit(EXIT_FAILURE);
}
log_debug("Spawning worker for job %" PRIu64 ": %s", j->id, s);
}
r = invoke_callout_binary(sysupdate_binary_path(), (char *const *) cmd);
log_error_errno(r, "Failed to execute systemd-sysupdate: %m");
_exit(EXIT_FAILURE);
}
log_info("Started job %" PRIu64 " with worker PID " PID_FMT,
j->id, pid.pid);
r = event_add_child_pidref(j->manager->event, &j->child, &pid, WEXITED, job_on_exit, j);
if (r < 0)
return log_error_errno(r, "Failed to add child process to event loop: %m");
r = sd_event_source_set_child_process_own(j->child, true);
if (r < 0)
return log_error_errno(r, "Event loop failed to take ownership of child process: %m");
TAKE_PIDREF(pid);
j->stdout_fd = TAKE_FD(stdout_fd);
if (IN_SET(j->type, JOB_UPDATE, JOB_VACUUM))
j->target->busy = true;
return 0;
}
static int job_cancel(Job *j) {
int r;
assert(j);
r = sd_event_source_send_child_signal(j->child, j->n_cancelled < 3 ? SIGTERM : SIGKILL,
NULL, 0);
if (r < 0)
return r;
j->n_cancelled++;
return 0;
}
static int job_method_cancel(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Job *j = ASSERT_PTR(userdata);
const char *action;
int r;
assert(msg);
switch (j->type) {
case JOB_LIST:
case JOB_DESCRIBE:
case JOB_CHECK_NEW:
action = "org.freedesktop.sysupdate1.check";
break;
case JOB_UPDATE:
if (j->version)
action = "org.freedesktop.sysupdate1.update-to-version";
else
action = "org.freedesktop.sysupdate1.update";
break;
case JOB_VACUUM:
action = "org.freedesktop.sysupdate1.vacuum";
break;
case JOB_DESCRIBE_FEATURE:
action = NULL;
break;
default:
assert_not_reached();
}
if (action) {
r = bus_verify_polkit_async(
msg,
action,
/* details= */ NULL,
&j->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
}
r = job_cancel(j);
if (r < 0)
return r;
return sd_bus_reply_method_return(msg, NULL);
}
static BUS_DEFINE_PROPERTY_GET_ENUM(job_property_get_type, job_type, JobType);
static int job_object_find(
sd_bus *bus,
const char *path,
const char *iface,
void *userdata,
void **ret,
sd_bus_error *error) {
Manager *m = ASSERT_PTR(userdata);
Job *j;
const char *p;
uint64_t id;
int r;
assert(bus);
assert(path);
assert(ret);
p = startswith(path, "/org/freedesktop/sysupdate1/job/_");
if (!p)
return 0;
r = safe_atou64(p, &id);
if (r < 0 || id == 0)
return 0;
j = hashmap_get(m->jobs, &id);
if (!j)
return 0;
*ret = j;
return 1;
}
static int job_node_enumerator(
sd_bus *bus,
const char *path,
void *userdata,
char ***nodes,
sd_bus_error *error) {
_cleanup_strv_free_ char **l = NULL;
Manager *m = ASSERT_PTR(userdata);
Job *j;
unsigned k = 0;
l = new0(char*, hashmap_size(m->jobs) + 1);
if (!l)
return -ENOMEM;
HASHMAP_FOREACH(j, m->jobs) {
l[k] = strdup(j->object_path);
if (!l[k])
return -ENOMEM;
k++;
}
*nodes = TAKE_PTR(l);
return 1;
}
static const sd_bus_vtable job_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_PROPERTY("Id", "t", NULL, offsetof(Job, id), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Type", "s", job_property_get_type, offsetof(Job, type), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Offline", "b", bus_property_get_bool, offsetof(Job, offline), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Progress", "u", bus_property_get_unsigned, offsetof(Job, progress_percent), SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_METHOD("Cancel", NULL, NULL, job_method_cancel, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
static const BusObjectImplementation job_object = {
"/org/freedesktop/sysupdate1/job",
"org.freedesktop.sysupdate1.Job",
.fallback_vtables = BUS_FALLBACK_VTABLES({job_vtable, job_object_find}),
.node_enumerator = job_node_enumerator,
};
static Target *target_free(Target *t) {
if (!t)
return NULL;
free(t->name);
free(t->path);
free(t->id);
return mfree(t);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(Target*, target_free);
DEFINE_HASH_OPS_WITH_VALUE_DESTRUCTOR(target_hash_ops, char, string_hash_func, string_compare_func,
Target, target_free);
static int target_new(Manager *m, TargetClass class, const char *name, const char *path, Target **ret) {
_cleanup_(target_freep) Target *t = NULL;
assert(m);
assert(ret);
t = new(Target, 1);
if (!t)
return -ENOMEM;
*t = (Target) {
.manager = m,
.class = class,
.image_type = _IMAGE_TYPE_INVALID,
};
t->name = strdup(name);
if (!t->name)
return -ENOMEM;
t->path = strdup(path);
if (!t->path)
return -ENOMEM;
if (class == TARGET_HOST)
t->id = strdup("host"); /* This is what appears in the object path */
else
t->id = strjoin(target_class_to_string(class), ":", name);
if (!t->id)
return -ENOMEM;
*ret = TAKE_PTR(t);
return 0;
}
static int sysupdate_run_simple(sd_json_variant **ret, Target *t, ...) {
_cleanup_close_pair_ int pipe[2] = EBADF_PAIR;
_cleanup_(pidref_done_sigkill_wait) PidRef pid = PIDREF_NULL;
_cleanup_fclose_ FILE *f = NULL;
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_free_ char *target_arg = NULL;
int r;
if (t) {
r = target_get_argument(t, &target_arg);
if (r < 0)
return r;
}
r = pipe2(pipe, O_CLOEXEC);
if (r < 0)
return -errno;
r = pidref_safe_fork_full("(sd-sysupdate)",
(int[]) { -EBADF, pipe[1], STDERR_FILENO },
NULL, 0,
FORK_RESET_SIGNALS|FORK_CLOSE_ALL_FDS|FORK_DEATHSIG_SIGTERM|
FORK_REARRANGE_STDIO|FORK_LOG|FORK_REOPEN_LOG,
&pid);
if (r < 0)
return r;
if (r == 0) {
/* Child */
va_list ap;
char *arg;
_cleanup_strv_free_ char **args = NULL;
if (strv_extend(&args, "systemd-sysupdate") < 0) {
log_oom();
_exit(EXIT_FAILURE);
}
if (strv_extend(&args, "--json=short") < 0) {
log_oom();
_exit(EXIT_FAILURE);
}
if (target_arg && strv_extend(&args, target_arg) < 0) {
log_oom();
_exit(EXIT_FAILURE);
}
va_start(ap, t);
while ((arg = va_arg(ap, char*))) {
r = strv_extend(&args, arg);
if (r < 0)
break;
}
va_end(ap);
if (r < 0) {
log_oom();
_exit(EXIT_FAILURE);
}
if (DEBUG_LOGGING) {
_cleanup_free_ char *s = NULL;
s = quote_command_line((char**) args, SHELL_ESCAPE_EMPTY);
if (!s) {
log_oom();
_exit(EXIT_FAILURE);
}
log_debug("Spawning sysupdate: %s", s);
}
r = invoke_callout_binary(sysupdate_binary_path(), args);
log_error_errno(r, "Failed to execute systemd-sysupdate: %m");
_exit(EXIT_FAILURE);
}
pipe[1] = safe_close(pipe[1]);
f = take_fdopen(&pipe[0], "r");
if (!f)
return -errno;
r = sd_json_parse_file(f, "stdout", 0, &v, NULL, NULL);
if (r < 0)
return log_debug_errno(r, "Failed to parse JSON: %m");
*ret = TAKE_PTR(v);
return 0;
}
static BUS_DEFINE_PROPERTY_GET_ENUM(target_property_get_class, target_class, TargetClass);
#define log_sysupdate_bad_json_full(lvl, r, verb, msg) \
log_full_errno((lvl), (r), "Invalid JSON response from 'systemd-sysupdate %s': %s", (verb), (msg))
#define log_sysupdate_bad_json(r, verb, msg) \
log_sysupdate_bad_json_full(LOG_ERR, (r), (verb), (msg))
#define log_sysupdate_bad_json_debug(r, verb, msg) \
log_sysupdate_bad_json_full(LOG_DEBUG, (r), (verb), (msg))
static int target_method_list_finish(
sd_bus_message *msg,
const Job *j,
sd_json_variant *json,
sd_bus_error *error) {
sd_json_variant *v;
_cleanup_strv_free_ char **versions = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
int r;
assert(json);
v = sd_json_variant_by_key(json, "all");
if (!v)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Missing key 'all'");
r = sd_json_variant_strv(v, &versions);
if (r < 0)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Key 'all' should be strv");
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_append_strv(reply, versions);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int target_method_list(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "t", &flags);
if (r < 0)
return r;
if ((flags & ~SD_SYSUPDATE_FLAGS_ALL) != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags specified");
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"offline", one_zero(FLAGS_SET(flags, SD_SYSUPDATE_OFFLINE)),
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.check",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = job_new(JOB_LIST, t, msg, target_method_list_finish, &j);
if (r < 0)
return r;
j->offline = FLAGS_SET(flags, SD_SYSUPDATE_OFFLINE);
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static int target_method_describe_finish(
sd_bus_message *msg,
const Job *j,
sd_json_variant *json,
sd_bus_error *error) {
_cleanup_free_ char *text = NULL;
int r;
/* NOTE: This is also reused by target_method_describe_feature */
assert(json);
r = sd_json_variant_format(json, 0, &text);
if (r < 0)
return r;
return sd_bus_reply_method_return(msg, "s", text);
}
static int target_method_describe(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
const char *version;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "st", &version, &flags);
if (r < 0)
return r;
if (isempty(version))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Version must be specified");
if ((flags & ~SD_SYSUPDATE_FLAGS_ALL) != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid flags specified");
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"version", version,
"offline", one_zero(FLAGS_SET(flags, SD_SYSUPDATE_OFFLINE)),
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.check",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = job_new(JOB_DESCRIBE, t, msg, target_method_describe_finish, &j);
if (r < 0)
return r;
j->version = strdup(version);
if (!j->version)
return log_oom();
j->offline = FLAGS_SET(flags, SD_SYSUPDATE_OFFLINE);
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static int target_method_check_new_finish(
sd_bus_message *msg,
const Job *j,
sd_json_variant *json,
sd_bus_error *error) {
const char *reply;
assert(json);
sd_json_variant *v = sd_json_variant_by_key(json, "available");
if (!v)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "check-new", "Missing key 'available'");
if (sd_json_variant_is_null(v))
reply = "";
else
reply = sd_json_variant_string(v);
if (!reply)
return -EINVAL;
return sd_bus_reply_method_return(msg, "s", reply);
}
static int target_method_check_new(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
int r;
assert(msg);
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"offline", "0",
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.check",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = job_new(JOB_CHECK_NEW, t, msg, target_method_check_new_finish, &j);
if (r < 0)
return r;
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static int target_method_update_finished_early(
sd_bus_message *msg,
const Job *j,
sd_json_variant *json,
sd_bus_error *error) {
/* Called when job finishes w/ a successful exit code, but before any work begins.
* This happens when there is no candidate (i.e. we're already up-to-date), or
* specified update is already installed. */
return sd_bus_error_setf(error, BUS_ERROR_NO_UPDATE_CANDIDATE,
"Job exited successfully with no work to do, assume already updated");
}
static int target_method_update_detach(sd_bus_message *msg, const Job *j) {
int r;
assert(msg);
assert(j);
r = sd_bus_reply_method_return(msg, "sto", j->version, j->id, j->object_path);
if (r < 0)
return bus_log_parse_error(r);
return 0;
}
static int target_method_update(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
const char *version, *action;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "st", &version, &flags);
if (r < 0)
return r;
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
if (isempty(version))
action = "org.freedesktop.sysupdate1.update";
else
action = "org.freedesktop.sysupdate1.update-to-version";
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"version", version,
NULL
};
r = bus_verify_polkit_async(
msg,
action,
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = job_new(JOB_UPDATE, t, msg, target_method_update_finished_early, &j);
if (r < 0)
return r;
j->detach_cb = target_method_update_detach;
j->version = strdup(version);
if (!j->version)
return -ENOMEM;
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j);
return 1;
}
static int target_method_vacuum_finish(
sd_bus_message *msg,
const Job *j,
sd_json_variant *json,
sd_bus_error *error) {
sd_json_variant *v;
uint64_t instances, disabled;
assert(json);
v = sd_json_variant_by_key(json, "removed");
if (!v)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Missing key 'removed'");
if (!sd_json_variant_is_unsigned(v))
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Key 'removed' should be an unsigned int");
instances = sd_json_variant_unsigned(v);
assert(instances <= UINT32_MAX);
v = sd_json_variant_by_key(json, "disabledTransfers");
if (!v)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Missing key 'disabledTransfers'");
if (!sd_json_variant_is_unsigned(v))
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "vacuum", "Key 'disabledTransfers' should be an unsigned int");
disabled = sd_json_variant_unsigned(v);
assert(disabled <= UINT32_MAX);
return sd_bus_reply_method_return(msg, "uu", (uint32_t) instances, (uint32_t) disabled);
}
static int target_method_vacuum(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
int r;
assert(msg);
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.vacuum",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
r = job_new(JOB_VACUUM, t, msg, target_method_vacuum_finish, &j);
if (r < 0)
return r;
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static int target_method_get_version(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
sd_json_variant *version_json;
int r;
r = sysupdate_run_simple(&v, t, "--offline", "list", NULL);
if (r < 0)
return log_error_errno(r, "Failed to run 'systemd-sysupdate list': %m");
version_json = sd_json_variant_by_key(v, "current");
if (!version_json)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Missing key 'current'");
if (sd_json_variant_is_null(version_json))
return sd_bus_reply_method_return(msg, "s", "");
if (!sd_json_variant_is_string(version_json))
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Key 'current' should be a string");
return sd_bus_reply_method_return(msg, "s", sd_json_variant_string(version_json));
}
static int target_get_appstream(Target *t, char ***ret) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
sd_json_variant *appstream_url_json;
int r;
r = sysupdate_run_simple(&v, t, "--offline", "list", NULL);
if (r < 0)
return log_error_errno(r, "Failed to run 'systemd-sysupdate list': %m");
appstream_url_json = sd_json_variant_by_key(v, "appstreamUrls");
if (!appstream_url_json)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Missing key 'appstreamUrls'");
r = sd_json_variant_strv(appstream_url_json, ret);
if (r < 0)
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Key 'appstreamUrls' should be strv");
return 0;
}
static int target_method_get_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
_cleanup_strv_free_ char **appstream_urls = NULL;
int r;
r = target_get_appstream(t, &appstream_urls);
if (r < 0)
return r;
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_append_strv(reply, appstream_urls);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int target_method_list_features(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **features = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Target *t = ASSERT_PTR(userdata);
sd_json_variant *v;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "t", &flags);
if (r < 0)
return r;
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
r = sysupdate_run_simple(&json, t, "features", NULL);
if (r < 0)
return r;
v = sd_json_variant_by_key(json, "features");
if (!v)
return -EINVAL;
r = sd_json_variant_strv(v, &features);
if (r < 0)
return r;
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_append_strv(reply, features);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int target_method_describe_feature(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
Target *t = ASSERT_PTR(userdata);
_cleanup_(job_freep) Job *j = NULL;
const char *feature;
uint64_t flags;
int r;
assert(msg);
r = sd_bus_message_read(msg, "st", &feature, &flags);
if (r < 0)
return r;
if (isempty(feature))
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Feature must be specified");
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
r = job_new(JOB_DESCRIBE_FEATURE, t, msg, target_method_describe_finish, &j);
if (r < 0)
return r;
j->feature = strdup(feature);
if (!j->feature)
return log_oom();
r = job_start(j);
if (r < 0)
return sd_bus_error_set_errnof(error, r, "Failed to start job: %m");
TAKE_PTR(j); /* Avoid job from being killed & freed */
return 1;
}
static bool feature_name_is_valid(const char *name) {
if (isempty(name))
return false;
if (!ascii_is_valid(name))
return false;
if (!filename_is_valid(strjoina(name, ".feature.d")))
return false;
return true;
}
static int target_method_set_feature_enabled(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_free_ char *feature_ext = NULL;
Target *t = ASSERT_PTR(userdata);
const char *feature;
uint64_t flags;
int32_t enabled;
int r;
assert(msg);
if (t->class != TARGET_HOST)
return sd_bus_reply_method_errorf(msg,
SD_BUS_ERROR_NOT_SUPPORTED,
"For now, features can only be managed on the host system.");
r = sd_bus_message_read(msg, "sit", &feature, &enabled, &flags);
if (r < 0)
return r;
if (!feature_name_is_valid(feature))
return sd_bus_reply_method_errorf(msg,
SD_BUS_ERROR_INVALID_ARGS,
"The specified feature is invalid");
if (flags != 0)
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Flags must be 0");
if (!endswith(feature, ".feature")) {
feature_ext = strjoin(feature, ".feature");
if (!feature_ext)
return -ENOMEM;
feature = feature_ext;
}
const char *details[] = {
"class", target_class_to_string(t->class),
"name", t->name,
"feature", feature,
"enabled", enabled >= 0 ? true_false(enabled) : "unset",
NULL
};
r = bus_verify_polkit_async(
msg,
"org.freedesktop.sysupdate1.manage-features",
details,
&t->manager->polkit_registry,
error);
if (r < 0)
return r;
if (r == 0)
return 1; /* Will call us back */
/* We assume that no sysadmin will name their config 50-systemd-sysupdate-enabled.conf */
if (enabled < 0) { /* Reset -> delete the drop-in file */
_cleanup_free_ char *path = NULL;
r = drop_in_file(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME, NULL, &path);
if (r < 0)
return r;
if (unlink(path) < 0)
return -errno;
} else { /* otherwise, create the drop-in with the right settings */
r = write_drop_in_format(SYSCONF_DIR "/sysupdate.d", feature, 50, FEATURES_DROPIN_NAME,
"# Generated via org.freedesktop.sysupdate1 D-Bus interface\n\n"
"[Feature]\n"
"Enabled=%s\n",
yes_no(enabled));
if (r < 0)
return r;
}
return sd_bus_reply_method_return(msg, NULL);
}
static int target_list_components(Target *t, char ***ret_components, bool *ret_have_default) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *json = NULL;
_cleanup_strv_free_ char **components = NULL;
sd_json_variant *v;
bool have_default;
int r;
r = sysupdate_run_simple(&json, t, "components", NULL);
if (r < 0)
return log_debug_errno(r, "Failed to run 'systemd-sysupdate components': %m");
v = sd_json_variant_by_key(json, "default");
if (!v)
return log_sysupdate_bad_json_debug(SYNTHETIC_ERRNO(EPROTO), "components", "Missing key 'default'");
have_default = sd_json_variant_boolean(v);
v = sd_json_variant_by_key(json, "components");
if (!v)
return log_sysupdate_bad_json_debug(SYNTHETIC_ERRNO(EPROTO), "components", "Missing key 'components'");
r = sd_json_variant_strv(v, &components);
if (r < 0)
return log_sysupdate_bad_json_debug(SYNTHETIC_ERRNO(EPROTO), "components", "Key 'components' should be a strv");
if (ret_components)
*ret_components = TAKE_PTR(components);
if (ret_have_default)
*ret_have_default = have_default;
return 0;
}
static int manager_ensure_targets(Manager *m);
static int target_object_find(
sd_bus *bus,
const char *path,
const char *iface,
void *userdata,
void **found,
sd_bus_error *error) {
Manager *m = ASSERT_PTR(userdata);
Target *t;
_cleanup_free_ char *e = NULL;
const char *p;
int r;
assert(bus);
assert(path);
assert(found);
p = startswith(path, "/org/freedesktop/sysupdate1/target/");
if (!p)
return 0;
e = bus_label_unescape(p);
if (!e)
return -ENOMEM;
r = manager_ensure_targets(m);
if (r < 0)
return r;
t = hashmap_get(m->targets, e);
if (!t)
return 0;
*found = t;
return 1;
}
static char *target_bus_path(Target *t) {
_cleanup_free_ char *e = NULL;
assert(t);
e = bus_label_escape(t->id);
if (!e)
return NULL;
return strjoin("/org/freedesktop/sysupdate1/target/", e);
}
static int target_node_enumerator(
sd_bus *bus,
const char *path,
void *userdata,
char ***nodes,
sd_bus_error *error) {
_cleanup_strv_free_ char **l = NULL;
Manager *m = ASSERT_PTR(userdata);
Target *t;
unsigned k = 0;
int r;
r = manager_ensure_targets(m);
if (r < 0)
return r;
l = new0(char*, hashmap_size(m->targets) + 1);
if (!l)
return -ENOMEM;
HASHMAP_FOREACH(t, m->targets) {
l[k] = target_bus_path(t);
if (!l[k])
return -ENOMEM;
k++;
}
*nodes = TAKE_PTR(l);
return 1;
}
static const sd_bus_vtable target_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_PROPERTY("Class", "s", target_property_get_class,
offsetof(Target, class), SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Name", "s", NULL, offsetof(Target, name),
SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_PROPERTY("Path", "s", NULL, offsetof(Target, path),
SD_BUS_VTABLE_PROPERTY_CONST),
SD_BUS_METHOD_WITH_ARGS("List",
SD_BUS_ARGS("t", flags),
SD_BUS_RESULT("as", versions),
target_method_list,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("Describe",
SD_BUS_ARGS("s", version, "t", flags),
SD_BUS_RESULT("s", json),
target_method_describe,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("CheckNew",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("s", new_version),
target_method_check_new,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("Update",
SD_BUS_ARGS("s", new_version, "t", flags),
SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
target_method_update,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("Vacuum",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("u", instances, "u", disabled_transfers),
target_method_vacuum,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("GetAppStream",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("as", appstream),
target_method_get_appstream,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("GetVersion",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("s", version),
target_method_get_version,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("ListFeatures",
SD_BUS_ARGS("t", flags),
SD_BUS_RESULT("as", features),
target_method_list_features,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("DescribeFeature",
SD_BUS_ARGS("s", feature, "t", flags),
SD_BUS_RESULT("s", json),
target_method_describe_feature,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("SetFeatureEnabled",
SD_BUS_ARGS("s", feature, "i", enabled, "t", flags),
SD_BUS_NO_RESULT,
target_method_set_feature_enabled,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END
};
static const BusObjectImplementation target_object = {
"/org/freedesktop/sysupdate1/target",
"org.freedesktop.sysupdate1.Target",
.fallback_vtables = BUS_FALLBACK_VTABLES({target_vtable, target_object_find}),
.node_enumerator = target_node_enumerator,
};
static Manager *manager_free(Manager *m) {
if (!m)
return NULL;
hashmap_free(m->targets);
hashmap_free(m->jobs);
m->bus = sd_bus_flush_close_unref(m->bus);
sd_event_source_unref(m->notify_event);
sd_event_unref(m->event);
return mfree(m);
}
DEFINE_TRIVIAL_CLEANUP_FUNC(Manager *, manager_free);
static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void *userdata) {
Manager *m = ASSERT_PTR(userdata);
int r;
assert(fd >= 0);
_cleanup_(pidref_done) PidRef sender_pidref = PIDREF_NULL;
_cleanup_free_ char *buf = NULL;
r = notify_recv(fd, &buf, /* ret_ucred= */ NULL, &sender_pidref);
if (r == -EAGAIN)
return 0;
if (r < 0)
return r;
Job *j;
HASHMAP_FOREACH(j, m->jobs) {
PidRef child_pidref = PIDREF_NULL;
r = event_source_get_child_pidref(j->child, &child_pidref);
if (r < 0)
return log_warning_errno(r, "Failed to get child pidref: %m");
if (pidref_equal(&sender_pidref, &child_pidref))
break;
}
if (!j) {
log_warning("Got notification datagram from unexpected peer, ignoring.");
return 0;
}
char *version = find_line_startswith(buf, "X_SYSUPDATE_VERSION=");
char *progress = find_line_startswith(buf, "X_SYSUPDATE_PROGRESS=");
char *errno_str = find_line_startswith(buf, "ERRNO=");
const char *ready = find_line_startswith(buf, "READY=1");
if (version)
job_on_version(j, truncate_nl(version));
if (progress)
job_on_progress(j, truncate_nl(progress));
if (errno_str)
job_on_errno(j, truncate_nl(errno_str));
/* Should come last, since this might actually detach the job */
if (ready)
job_on_ready(j);
return 0;
}
static int manager_new(Manager **ret) {
_cleanup_(manager_freep) Manager *m = NULL;
_cleanup_close_ int notify_fd = -EBADF;
static const union sockaddr_union sa = {
.un.sun_family = AF_UNIX,
.un.sun_path = "/run/systemd/sysupdate/notify",
};
int r;
assert(ret);
m = new(Manager, 1);
if (!m)
return -ENOMEM;
*m = (Manager) {
.runtime_scope = RUNTIME_SCOPE_SYSTEM,
};
r = sd_event_default(&m->event);
if (r < 0)
return r;
(void) sd_event_set_watchdog(m->event, true);
r = sd_event_set_signal_exit(m->event, true);
if (r < 0)
return r;
r = sd_event_add_signal(m->event, NULL, (SIGRTMIN+18) | SD_EVENT_SIGNAL_PROCMASK,
sigrtmin18_handler, NULL);
if (r < 0)
return r;
r = sd_event_add_memory_pressure(m->event, NULL, NULL, NULL);
if (r < 0)
log_debug_errno(r, "Failed allocate memory pressure event source, ignoring: %m");
r = sd_bus_default_system(&m->bus);
if (r < 0)
return r;
notify_fd = socket(AF_UNIX, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0);
if (notify_fd < 0)
return -errno;
(void) mkdir_parents_label(sa.un.sun_path, 0755);
(void) sockaddr_un_unlink(&sa.un);
if (bind(notify_fd, &sa.sa, SOCKADDR_UN_LEN(sa.un)) < 0)
return -errno;
r = setsockopt_int(notify_fd, SOL_SOCKET, SO_PASSCRED, true);
if (r < 0)
return r;
r = setsockopt_int(notify_fd, SOL_SOCKET, SO_PASSPIDFD, true);
if (r < 0)
log_debug_errno(r, "Failed to enable SO_PASSPIDFD, ignoring: %m");
r = sd_event_add_io(m->event, &m->notify_event, notify_fd, EPOLLIN, manager_on_notify, m);
if (r < 0)
return r;
(void) sd_event_source_set_description(m->notify_event, "notify-socket");
r = sd_event_source_set_io_fd_own(m->notify_event, true);
if (r < 0)
return r;
TAKE_FD(notify_fd);
*ret = TAKE_PTR(m);
return 0;
}
static int manager_enumerate_image_class(Manager *m, TargetClass class) {
_cleanup_hashmap_free_ Hashmap *images = NULL;
Image *image;
int r;
images = hashmap_new(&image_hash_ops);
if (!images)
return -ENOMEM;
r = image_discover(m->runtime_scope, (ImageClass) class, NULL, images);
if (r < 0)
return r;
HASHMAP_FOREACH(image, images) {
_cleanup_(target_freep) Target *t = NULL;
bool have = false;
if (IMAGE_IS_HOST(image))
continue; /* We already enroll the host ourselves */
r = target_new(m, class, image->name, image->path, &t);
if (r < 0)
return r;
t->image_type = image->type;
r = target_list_components(t, NULL, &have);
if (r < 0)
return r;
if (!have) {
log_debug("Skipping %s because it has no default component", image->path);
continue;
}
r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t);
if (r < 0)
return r;
TAKE_PTR(t);
}
return 0;
}
static int manager_enumerate_components(Manager *m) {
_cleanup_strv_free_ char **components = NULL;
bool have_default;
int r;
r = target_list_components(NULL, &components, &have_default);
if (r < 0)
return r;
if (have_default) {
_cleanup_(target_freep) Target *t = NULL;
r = target_new(m, TARGET_HOST, "host", "sysupdate.d", &t);
if (r < 0)
return r;
r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t);
if (r < 0)
return r;
TAKE_PTR(t);
}
STRV_FOREACH(component, components) {
_cleanup_free_ char *path = NULL;
_cleanup_(target_freep) Target *t = NULL;
path = strjoin("sysupdate.", *component, ".d");
if (!path)
return -ENOMEM;
r = target_new(m, TARGET_COMPONENT, *component, path, &t);
if (r < 0)
return r;
r = hashmap_ensure_put(&m->targets, &target_hash_ops, t->id, t);
if (r < 0)
return r;
TAKE_PTR(t);
}
return 0;
}
static int manager_enumerate_targets(Manager *m) {
static const TargetClass discoverable_classes[] = {
TARGET_MACHINE,
TARGET_PORTABLE,
TARGET_SYSEXT,
TARGET_CONFEXT,
};
int r;
assert(m);
FOREACH_ARRAY(class, discoverable_classes, ELEMENTSOF(discoverable_classes)) {
r = manager_enumerate_image_class(m, *class);
if (r < 0)
log_warning_errno(r, "Failed to enumerate %ss, ignoring: %m",
target_class_to_string(*class));
}
r = manager_enumerate_components(m);
if (r < 0)
log_warning_errno(r, "Failed to enumerate components, ignoring: %m");
return 0;
}
static int manager_ensure_targets(Manager *m) {
assert(m);
if (!hashmap_isempty(m->targets))
return 0;
return manager_enumerate_targets(m);
}
static int method_list_targets(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Manager *m = ASSERT_PTR(userdata);
Target *t;
int r;
assert(msg);
r = manager_ensure_targets(m);
if (r < 0)
return r;
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_open_container(reply, 'a', "(sso)");
if (r < 0)
return r;
HASHMAP_FOREACH(t, m->targets) {
_cleanup_free_ char *bus_path = NULL;
bus_path = target_bus_path(t);
if (!bus_path)
return -ENOMEM;
r = sd_bus_message_append(reply, "(sso)",
target_class_to_string(t->class),
t->name,
bus_path);
if (r < 0)
return r;
}
r = sd_bus_message_close_container(reply);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int method_list_jobs(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Manager *m = ASSERT_PTR(userdata);
Job *j;
int r;
assert(msg);
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_open_container(reply, 'a', "(tsuo)");
if (r < 0)
return r;
HASHMAP_FOREACH(j, m->jobs) {
r = sd_bus_message_append(reply, "(tsuo)",
j->id,
job_type_to_string(j->type),
j->progress_percent,
j->object_path);
if (r < 0)
return r;
}
r = sd_bus_message_close_container(reply);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static int method_list_appstream(sd_bus_message *msg, void *userdata, sd_bus_error *error) {
_cleanup_strv_free_ char **urls = NULL;
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
Manager *m = ASSERT_PTR(userdata);
Target *t;
int r;
assert(msg);
r = manager_ensure_targets(m);
if (r < 0)
return r;
HASHMAP_FOREACH(t, m->targets) {
char **target_appstream;
r = target_get_appstream(t, &target_appstream);
if (r < 0)
return r;
r = strv_extend_strv_consume(&urls, target_appstream, /* filter_duplicates = */ true);
if (r < 0)
return r;
}
r = sd_bus_message_new_method_return(msg, &reply);
if (r < 0)
return r;
r = sd_bus_message_append_strv(reply, urls);
if (r < 0)
return r;
return sd_bus_send(NULL, reply, NULL);
}
static const sd_bus_vtable manager_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD_WITH_ARGS("ListTargets",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("a(sso)", targets),
method_list_targets,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("ListJobs",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("a(tsuo)", jobs),
method_list_jobs,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_METHOD_WITH_ARGS("ListAppStream",
SD_BUS_NO_ARGS,
SD_BUS_RESULT("as", urls),
method_list_appstream,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_SIGNAL_WITH_ARGS("JobRemoved",
SD_BUS_ARGS("t", id, "o", path, "i", status),
0),
SD_BUS_VTABLE_END
};
static const BusObjectImplementation manager_object = {
"/org/freedesktop/sysupdate1",
"org.freedesktop.sysupdate1.Manager",
.vtables = BUS_VTABLES(manager_vtable),
.children = BUS_IMPLEMENTATIONS(&job_object, &target_object),
};
static int manager_add_bus_objects(Manager *m) {
int r;
assert(m);
r = bus_add_implementation(m->bus, &manager_object, m);
if (r < 0)
return r;
r = bus_log_control_api_register(m->bus);
if (r < 0)
return r;
r = sd_bus_request_name_async(m->bus, NULL, "org.freedesktop.sysupdate1", 0, NULL, NULL);
if (r < 0)
return log_error_errno(r, "Failed to request name: %m");
r = sd_bus_attach_event(m->bus, m->event, 0);
if (r < 0)
return log_error_errno(r, "Failed to attach bus to event loop: %m");
return 0;
}
static bool manager_is_idle(void *userdata) {
Manager *m = ASSERT_PTR(userdata);
return hashmap_isempty(m->jobs);
}
static void manager_check_idle(Manager *m) {
assert(m);
if (!hashmap_isempty(m->jobs))
return;
hashmap_clear(m->targets);
log_debug("Cleared target cache");
}
static int manager_run(Manager *m) {
assert(m);
return bus_event_loop_with_idle(m->event,
m->bus,
"org.freedesktop.sysupdate1",
DEFAULT_EXIT_USEC,
manager_is_idle,
m);
}
static int run(int argc, char *argv[]) {
_cleanup_(manager_freep) Manager *m = NULL;
int r;
log_setup();
r = service_parse_argv("systemd-sysupdated.service",
"System update management service.",
BUS_IMPLEMENTATIONS(&manager_object,
&log_control_object),
argc, argv);
if (r <= 0)
return r;
umask(0022);
/* SIGCHLD signal must be blocked for sd_event_add_child to work */
assert_se(sigprocmask_many(SIG_BLOCK, NULL, SIGCHLD) >= 0);
r = manager_new(&m);
if (r < 0)
return log_error_errno(r, "Failed to allocate manager object: %m");
r = manager_add_bus_objects(m);
if (r < 0)
return log_error_errno(r, "Failed to add bus objects: %m");
r = manager_run(m);
if (r < 0)
return log_error_errno(r, "Failed to run event loop: %m");
return 0;
}
DEFINE_MAIN_FUNCTION(run);