We nowadays support unprivileged invocation of systemd-nspawn + systemd-vmspawn, but there was no support for discovering suitable disk images (i.e. no per-user counterpart of /var/lib/machines). Add this now, and hook it up everywhere. Instead of hardcoding machined's, importd's, portabled's, sysupdated's image discovery to RUNTIME_SCOPE_SYSTEM I introduced a field that make the scope variable, even if this field is always initialized to RUNTIME_SCOPE_SYSTEM for now. I think these four services should eventually be updated to support a per-user concept too, this is preparation for that, even though it doesn't outright add support for this. This is for the largest part not user visible, except for in nspawn, vmspawn and the dissect tool. For the latter I added a pair of --user/--system switches to select the discovery scope.
2146 lines
68 KiB
C
2146 lines
68 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 "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) {
|
|
char buf[NOTIFY_BUFFER_MAX+1];
|
|
struct iovec iovec = {
|
|
.iov_base = buf,
|
|
.iov_len = sizeof(buf)-1,
|
|
};
|
|
CMSG_BUFFER_TYPE(CMSG_SPACE(sizeof(struct ucred))) control;
|
|
struct msghdr msghdr = {
|
|
.msg_iov = &iovec,
|
|
.msg_iovlen = 1,
|
|
.msg_control = &control,
|
|
.msg_controllen = sizeof(control),
|
|
};
|
|
struct ucred *ucred;
|
|
Manager *m = ASSERT_PTR(userdata);
|
|
Job *j;
|
|
ssize_t n;
|
|
char *version, *progress, *errno_str, *ready;
|
|
|
|
n = recvmsg_safe(fd, &msghdr, MSG_DONTWAIT|MSG_CMSG_CLOEXEC);
|
|
if (ERRNO_IS_NEG_TRANSIENT(n))
|
|
return 0;
|
|
if (n == -ECHRNG) {
|
|
log_warning_errno(n, "Got message with truncated control data (unexpected fds sent?), ignoring.");
|
|
return 0;
|
|
}
|
|
if (n == -EXFULL) {
|
|
log_warning_errno(n, "Got message with truncated payload data, ignoring.");
|
|
return 0;
|
|
}
|
|
if (n < 0)
|
|
return (int) n;
|
|
|
|
cmsg_close_all(&msghdr);
|
|
|
|
ucred = CMSG_FIND_DATA(&msghdr, SOL_SOCKET, SCM_CREDENTIALS, struct ucred);
|
|
if (!ucred || ucred->pid <= 0) {
|
|
log_warning("Got notification datagram lacking credential information, ignoring.");
|
|
return 0;
|
|
}
|
|
|
|
HASHMAP_FOREACH(j, m->jobs) {
|
|
pid_t pid;
|
|
assert_se(sd_event_source_get_child_pid(j->child, &pid) >= 0);
|
|
|
|
if (ucred->pid == pid)
|
|
break;
|
|
}
|
|
|
|
if (!j) {
|
|
log_warning("Got notification datagram from unexpected peer, ignoring.");
|
|
return 0;
|
|
}
|
|
|
|
buf[n] = 0;
|
|
|
|
version = find_line_startswith(buf, "X_SYSUPDATE_VERSION=");
|
|
progress = find_line_startswith(buf, "X_SYSUPDATE_PROGRESS=");
|
|
errno_str = find_line_startswith(buf, "ERRNO=");
|
|
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 = 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);
|