We have two validators, and neither really makes sense. Let's unify and clean things up.
2179 lines
69 KiB
C
2179 lines
69 KiB
C
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||
|
||
#include <stdlib.h>
|
||
#include <sys/stat.h>
|
||
#include <unistd.h>
|
||
|
||
#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-object.h"
|
||
#include "bus-polkit.h"
|
||
#include "bus-util.h"
|
||
#include "common-signal.h"
|
||
#include "constants.h"
|
||
#include "discover-image.h"
|
||
#include "dropin.h"
|
||
#include "env-util.h"
|
||
#include "escape.h"
|
||
#include "event-util.h"
|
||
#include "fd-util.h"
|
||
#include "format-util.h"
|
||
#include "hashmap.h"
|
||
#include "log.h"
|
||
#include "main-func.h"
|
||
#include "memfd-util.h"
|
||
#include "notify-recv.h"
|
||
#include "os-util.h"
|
||
#include "parse-util.h"
|
||
#include "pidref.h"
|
||
#include "process-util.h"
|
||
#include "runtime-scope.h"
|
||
#include "service-util.h"
|
||
#include "signal-util.h"
|
||
#include "string-table.h"
|
||
#include "strv.h"
|
||
#include "sysupdate-target.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;
|
||
|
||
char *notify_socket_path;
|
||
|
||
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 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_ACQUIRE,
|
||
JOB_INSTALL,
|
||
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, JOB_ACQUIRE and JOB_INSTALL */
|
||
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 job_type_table[_JOB_TYPE_MAX] = {
|
||
[JOB_LIST] = "list",
|
||
[JOB_DESCRIBE] = "describe",
|
||
[JOB_CHECK_NEW] = "check-new",
|
||
[JOB_ACQUIRE] = "acquire",
|
||
[JOB_INSTALL] = "install",
|
||
[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_PRIVATE_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;
|
||
}
|
||
|
||
/* Is Job in the set of jobs which require Target.busy to be set so they run exclusively? */
|
||
static bool job_requires_busy(Job *j) {
|
||
return IN_SET(j->type, JOB_ACQUIRE, JOB_INSTALL, JOB_VACUUM);
|
||
}
|
||
|
||
static int job_parse_child_output(int _fd, sd_json_variant **ret) {
|
||
_cleanup_close_ int fd = ASSERT_FD(_fd); /* Take ownership of the passed fd */
|
||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = 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;
|
||
}
|
||
|
||
r = sd_json_parse_fd(
|
||
/* path= */ "stdout",
|
||
TAKE_FD(fd),
|
||
SD_JSON_PARSE_DONATE_FD|SD_JSON_PARSE_SEEK0,
|
||
&v,
|
||
/* reterr_line= */ NULL,
|
||
/* reterr_column= */ NULL);
|
||
if (r < 0)
|
||
return log_debug_errno(r, "Failed to parse child output as 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 (job_requires_busy(j)) {
|
||
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 (job_requires_busy(j) && 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, acquire, update, vacuum, features */
|
||
NULL, /* maybe version (for list, acquire, update), maybe feature (features) */
|
||
NULL
|
||
};
|
||
size_t k = 2;
|
||
|
||
if (setenv("NOTIFY_SOCKET", j->manager->notify_socket_path, /* 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 || j->type == JOB_INSTALL) /* install is implemented as `update --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_ACQUIRE:
|
||
cmd[k++] = "acquire";
|
||
cmd[k++] = empty_to_null(j->version);
|
||
break;
|
||
|
||
case JOB_INSTALL:
|
||
cmd[k++] = "update"; /* install is implemented as `update --offline` */
|
||
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");
|
||
pidref_done(&pid); /* disarm sigkill_wait */
|
||
|
||
j->stdout_fd = TAKE_FD(stdout_fd);
|
||
|
||
if (job_requires_busy(j))
|
||
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.cancel-check";
|
||
break;
|
||
|
||
case JOB_ACQUIRE:
|
||
case JOB_INSTALL:
|
||
if (j->version)
|
||
action = "org.freedesktop.sysupdate1.cancel-update-to-version";
|
||
else
|
||
action = "org.freedesktop.sysupdate1.cancel-update";
|
||
break;
|
||
|
||
case JOB_VACUUM:
|
||
action = "org.freedesktop.sysupdate1.cancel-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_PRIVATE_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_(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(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]);
|
||
|
||
r = sd_json_parse_fd(
|
||
"stdout",
|
||
TAKE_FD(pipe[0]),
|
||
SD_JSON_PARSE_DONATE_FD,
|
||
&v,
|
||
/* reterr_line= */ NULL,
|
||
/* reterr_column= */ 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_message_send(reply);
|
||
}
|
||
|
||
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 (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS))
|
||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version");
|
||
|
||
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_acquire_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 acquired. */
|
||
return sd_bus_error_setf(error, BUS_ERROR_NO_UPDATE_CANDIDATE,
|
||
"Job exited successfully with no work to do, assume already acquired");
|
||
}
|
||
|
||
static int target_method_acquire_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_acquire(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");
|
||
|
||
/* We don’t have a separate polkit action for acquire/install as they are both effectively (part of)
|
||
* an update anyway. */
|
||
if (isempty(version))
|
||
action = "org.freedesktop.sysupdate1.update";
|
||
else {
|
||
if (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS))
|
||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version");
|
||
|
||
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_ACQUIRE, t, msg, target_method_acquire_finished_early, &j);
|
||
if (r < 0)
|
||
return r;
|
||
j->detach_cb = target_method_acquire_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_install_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 installed");
|
||
}
|
||
|
||
static int target_method_install_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_install(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");
|
||
|
||
/* We don’t have a separate polkit action for acquire/install as they are both effectively (part of)
|
||
* an update anyway. */
|
||
if (isempty(version))
|
||
action = "org.freedesktop.sysupdate1.update";
|
||
else {
|
||
if (!version_is_valid(version, VERSION_ALLOW_UNDERSCORE|VERSION_ALLOW_PLUS))
|
||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid version");
|
||
|
||
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_INSTALL, t, msg, target_method_install_finished_early, &j);
|
||
if (r < 0)
|
||
return r;
|
||
j->detach_cb = target_method_install_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)
|
||
version_json = sd_json_variant_by_key(v, "current+pending");
|
||
if (!version_json)
|
||
return log_sysupdate_bad_json(SYNTHETIC_ERRNO(EPROTO), "list", "Missing key 'current' or 'current+pending'");
|
||
|
||
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_message_send(reply);
|
||
}
|
||
|
||
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, "--offline", "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_message_send(reply);
|
||
}
|
||
|
||
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 (!feature_name_valid(feature))
|
||
return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Invalid feature name");
|
||
|
||
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 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_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("Acquire",
|
||
SD_BUS_ARGS("s", new_version, "t", flags),
|
||
SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
|
||
target_method_acquire,
|
||
SD_BUS_VTABLE_UNPRIVILEGED),
|
||
|
||
SD_BUS_METHOD_WITH_ARGS("Install",
|
||
SD_BUS_ARGS("s", new_version, "t", flags),
|
||
SD_BUS_RESULT("s", new_version, "t", job_id, "o", job_path),
|
||
target_method_install,
|
||
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);
|
||
free(m->notify_socket_path);
|
||
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;
|
||
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 to allocate memory pressure event source, ignoring: %m");
|
||
|
||
r = sd_bus_default_system(&m->bus);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
r = notify_socket_prepare(
|
||
m->event,
|
||
SD_EVENT_PRIORITY_NORMAL - 1, /* Make this processed before worker exit. */
|
||
manager_on_notify,
|
||
m,
|
||
&m->notify_socket_path);
|
||
if (r < 0)
|
||
return r;
|
||
|
||
*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;
|
||
|
||
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 */
|
||
|
||
if (image->type == IMAGE_MSTACK)
|
||
continue; /* systemd-sysupdate doesn't support mstack images yet */
|
||
|
||
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_message_send(reply);
|
||
}
|
||
|
||
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_message_send(reply);
|
||
}
|
||
|
||
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_message_send(reply);
|
||
}
|
||
|
||
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),
|
||
/* runtime_scope= */ NULL,
|
||
argc, argv);
|
||
if (r <= 0)
|
||
return r;
|
||
|
||
umask(0022);
|
||
|
||
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);
|