core: introduce varlink io.systemd.Job interface (#42104)

Methods:

- `io.systemd.Job.List` — list all queued jobs or look up by `id`/`unit`
name, with streaming support. Uses context/runtime split: `JobContext`
(Unit, JobType) and `JobRuntime` (Id, State, Result,
ActivationDetails). Follows the same SELinux and parameter-conflict
patterns as `io.systemd.Unit.List`.
- `io.systemd.Job.Cancel` — cancel a specific job by ID, with SELinux
and polkit authorization.
- `io.systemd.Job.ClearAll` — cancel all pending jobs, with SELinux and
polkit authorization.
This commit is contained in:
Yu Watanabe
2026-05-20 17:22:18 +09:00
committed by GitHub
14 changed files with 598 additions and 118 deletions
+1
View File
@@ -71,6 +71,7 @@ libcore_sources = files(
'varlink-common.c',
'varlink-dynamic-user.c',
'varlink-execute.c',
'varlink-job.c',
'varlink-kill.c',
'varlink-manager.c',
'varlink-metrics.c',
+252
View File
@@ -0,0 +1,252 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include "sd-varlink.h"
#include "bus-polkit.h"
#include "job.h"
#include "json-util.h"
#include "locale-util.h"
#include "manager.h"
#include "selinux-access.h"
#include "strv.h"
#include "unit.h"
#include "varlink-job.h"
static int activation_details_build_json(sd_json_variant **ret, const char *name, void *userdata) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
_cleanup_strv_free_ char **pairs = NULL;
Job *j = ASSERT_PTR(userdata);
int r;
assert(ret);
r = activation_details_append_pair(j->activation_details, &pairs);
if (r < 0)
return log_debug_errno(r, "Failed to get activation details: %m");
if (r == 0) {
*ret = NULL;
return 0;
}
STRV_FOREACH_PAIR(key, value, pairs) {
r = sd_json_variant_set_field_string(&v, *key, *value);
if (r < 0)
return r;
}
*ret = TAKE_PTR(v);
return 0;
}
int job_build_json(sd_json_variant **ret, const char *name, void *userdata) {
Job *j = ASSERT_PTR(userdata);
/* "Unit" is omitted in StartTransient streaming notifications where the caller already knows the unit. */
return sd_json_buildo(
ASSERT_PTR(ret),
SD_JSON_BUILD_PAIR_INTEGER("Id", j->id),
JSON_BUILD_PAIR_STRING_NON_EMPTY("Unit", j->unit ? j->unit->id : NULL),
JSON_BUILD_PAIR_ENUM("JobType", job_type_to_string(j->type)),
JSON_BUILD_PAIR_ENUM("State", job_state_to_string(j->state)),
JSON_BUILD_PAIR_ENUM_NON_EMPTY("Result", job_result_to_string(j->result)),
JSON_BUILD_PAIR_CALLBACK_NON_NULL("ActivationDetails", activation_details_build_json, j));
}
static int varlink_error_no_such_job(sd_varlink *link, const char *name) {
return sd_varlink_errorbo(
ASSERT_PTR(link),
VARLINK_ERROR_JOB_NO_SUCH_JOB,
JSON_BUILD_PAIR_STRING_NON_EMPTY("parameter", name));
}
static int list_job_one(sd_varlink *link, Job *job) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
int r;
assert(link);
assert(job);
r = job_build_json(&v, /* name= */ NULL, job);
if (r < 0)
return r;
return sd_varlink_reply(link, v);
}
static int list_job_one_with_selinux_access_check(sd_varlink *link, Job *job) {
int r;
assert(link);
assert(job);
assert(job->unit);
r = mac_selinux_unit_access_check_varlink(job->unit, link, "status");
if (r < 0)
/* If mac_selinux_unit_access_check_varlink() returned an error,
* it means that SELinux enforce is on. It also does all the logging(). */
return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, NULL);
return list_job_one(link, job);
}
typedef struct JobLookupParameters {
uint32_t id;
const char *unit;
} JobLookupParameters;
static int lookup_job_by_parameters(
sd_varlink *link,
Manager *manager,
JobLookupParameters *p,
Job **ret) {
/* The function can return ret=NULL if no lookup parameters provided */
Job *job = NULL;
assert(link);
assert(manager);
assert(p);
assert(ret);
if (p->id > 0) {
job = manager_get_job(manager, p->id);
if (!job)
return varlink_error_no_such_job(link, "id");
}
if (p->unit) {
Unit *u = manager_get_unit(manager, p->unit);
if (!u || !u->job)
return varlink_error_no_such_job(link, "unit");
if (job && u->job != job) {
log_debug("Job lookup by parameters id=%u unit='%s' resulted in different jobs.", p->id, p->unit);
return varlink_error_no_such_job(link, /* name= */ NULL);
}
job = u->job;
}
*ret = job;
return !!job;
}
int vl_method_list_jobs(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
static const sd_json_dispatch_field dispatch_table[] = {
{ "id", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, offsetof(JobLookupParameters, id), 0 },
{ "unit", SD_JSON_VARIANT_STRING, json_dispatch_const_unit_name, offsetof(JobLookupParameters, unit), 0 },
{}
};
Manager *manager = ASSERT_PTR(userdata);
JobLookupParameters p = {};
Job *job;
int r;
assert(link);
assert(parameters);
r = sd_varlink_dispatch(link, parameters, dispatch_table, &p);
if (r != 0)
return r;
r = lookup_job_by_parameters(link, manager, &p, &job);
if (r < 0)
return r;
if (r > 0)
return list_job_one_with_selinux_access_check(link, job);
/* List all jobs */
if (!FLAGS_SET(flags, SD_VARLINK_METHOD_MORE))
return sd_varlink_error(link, SD_VARLINK_ERROR_EXPECTED_MORE, NULL);
r = sd_varlink_set_sentinel(link, VARLINK_ERROR_JOB_NO_SUCH_JOB);
if (r < 0)
return r;
HASHMAP_FOREACH(job, manager->jobs) {
r = mac_selinux_unit_access_check_varlink(job->unit, link, "status");
if (r < 0)
continue;
r = list_job_one(link, job);
if (r < 0)
return r;
}
return 0;
}
int vl_method_cancel_job(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
static const sd_json_dispatch_field dispatch_table[] = {
{ "id", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, 0, SD_JSON_MANDATORY },
{}
};
Manager *manager = ASSERT_PTR(userdata);
uint32_t id = 0;
int r;
assert(link);
assert(parameters);
r = sd_varlink_dispatch(link, parameters, dispatch_table, &id);
if (r != 0)
return r;
Job *j = manager_get_job(manager, id);
if (!j)
return varlink_error_no_such_job(link, "id");
r = mac_selinux_unit_access_check_varlink(j->unit, link, "stop");
if (r < 0)
return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, NULL);
r = varlink_verify_polkit_async(
link,
manager->system_bus,
"org.freedesktop.systemd1.manage-units",
(const char**) STRV_MAKE(
"unit", j->unit ? j->unit->id : NULL,
"verb", "cancel",
"polkit.message", N_("Authentication is required to cancel job for unit '$(unit)'."),
"polkit.gettext_domain", GETTEXT_PACKAGE),
&manager->polkit_registry);
if (r <= 0)
return r;
job_finish_and_invalidate(j, JOB_CANCELED, /* recursive= */ true, /* already= */ false);
return sd_varlink_reply(link, NULL);
}
int vl_method_clear_all_jobs(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata) {
Manager *manager = ASSERT_PTR(userdata);
int r;
assert(link);
assert(parameters);
r = sd_varlink_dispatch(link, parameters, /* dispatch_table= */ NULL, /* userdata= */ NULL);
if (r != 0)
return r;
r = mac_selinux_access_check_varlink(link, "reload");
if (r < 0)
return sd_varlink_error(link, SD_VARLINK_ERROR_PERMISSION_DENIED, NULL);
r = varlink_verify_polkit_async(
link,
manager->system_bus,
"org.freedesktop.systemd1.manage-units",
(const char**) STRV_MAKE(
"verb", "clear-jobs",
"polkit.message", N_("Authentication is required to clear all pending jobs."),
"polkit.gettext_domain", GETTEXT_PACKAGE),
&manager->polkit_registry);
if (r <= 0)
return r;
manager_clear_jobs(manager);
return sd_varlink_reply(link, NULL);
}
+12
View File
@@ -0,0 +1,12 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
#include "core-forward.h"
#define VARLINK_ERROR_JOB_NO_SUCH_JOB "io.systemd.Job.NoSuchJob"
int job_build_json(sd_json_variant **ret, const char *name, void *userdata);
int vl_method_list_jobs(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
int vl_method_cancel_job(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
int vl_method_clear_all_jobs(sd_varlink *link, sd_json_variant *parameters, sd_varlink_method_flags_t flags, void *userdata);
+1 -12
View File
@@ -28,6 +28,7 @@
#include "varlink-cgroup.h"
#include "varlink-common.h"
#include "varlink-execute.h"
#include "varlink-job.h"
#include "varlink-kill.h"
#include "varlink-mount.h"
#include "varlink-path.h"
@@ -618,18 +619,6 @@ void varlink_unit_send_change_signal(Unit *u) {
SD_JSON_BUILD_PAIR_CALLBACK("runtime", unit_runtime_build_json, u));
}
static int job_build_json(sd_json_variant **ret, const char *name, void *userdata) {
Job *j = ASSERT_PTR(userdata);
/* Note that "Result" is suppressed until the job reaches JOB_FINISHED. */
return sd_json_buildo(
ASSERT_PTR(ret),
SD_JSON_BUILD_PAIR_INTEGER("Id", j->id),
JSON_BUILD_PAIR_ENUM("JobType", job_type_to_string(j->type)),
JSON_BUILD_PAIR_ENUM("State", job_state_to_string(j->state)),
JSON_BUILD_PAIR_STRING_NON_EMPTY_UNDERSCORIFY("Result", job_result_to_string(j->result)));
}
void varlink_job_send_change_signal(Job *j) {
assert(j);
+6
View File
@@ -15,11 +15,13 @@
#include "unit.h"
#include "varlink.h"
#include "varlink-dynamic-user.h"
#include "varlink-io.systemd.Job.h"
#include "varlink-io.systemd.ManagedOOM.h"
#include "varlink-io.systemd.Manager.h"
#include "varlink-io.systemd.Unit.h"
#include "varlink-io.systemd.UserDatabase.h"
#include "varlink-io.systemd.service.h"
#include "varlink-job.h"
#include "varlink-manager.h"
#include "varlink-metrics.h"
#include "varlink-serialize.h"
@@ -411,6 +413,7 @@ int manager_setup_varlink_server(Manager *m) {
r = sd_varlink_server_add_interface_many(
s,
&vl_interface_io_systemd_Job,
&vl_interface_io_systemd_Manager,
&vl_interface_io_systemd_Unit,
&vl_interface_io_systemd_service);
@@ -419,6 +422,9 @@ int manager_setup_varlink_server(Manager *m) {
r = sd_varlink_server_bind_method_many(
s,
"io.systemd.Job.List", vl_method_list_jobs,
"io.systemd.Job.Cancel", vl_method_cancel_job,
"io.systemd.Job.ClearAll", vl_method_clear_all_jobs,
"io.systemd.Manager.Describe", vl_method_describe_manager,
"io.systemd.Manager.Reexecute", vl_method_reexecute_manager,
"io.systemd.Manager.Reload", vl_method_reload_manager,
+21
View File
@@ -796,6 +796,27 @@ int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_jso
return 0;
}
int json_dispatch_job_id(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
uint32_t *id = ASSERT_PTR(userdata);
uint32_t k;
int r;
if (sd_json_variant_is_null(variant)) {
*id = 0;
return 0;
}
r = sd_json_dispatch_uint32(name, variant, flags, &k);
if (r < 0)
return r;
if (k == 0)
return json_log(variant, flags, SYNTHETIC_ERRNO(EINVAL), "JSON field '%s' is not a valid job ID.", strna(name));
*id = k;
return 0;
}
int json_variant_compare(sd_json_variant *a, sd_json_variant *b) {
int r;
+2
View File
@@ -132,6 +132,7 @@ int json_dispatch_ifindex(const char *name, sd_json_variant *variant, sd_json_di
int json_dispatch_log_level(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_strv_environment(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_access_mode(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
int json_dispatch_job_id(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
static inline int json_variant_unbase64_iovec(sd_json_variant *v, struct iovec *ret) {
return sd_json_variant_unbase64(v, ret ? &ret->iov_base : NULL, ret ? &ret->iov_len : NULL);
@@ -267,6 +268,7 @@ enum {
#define JSON_BUILD_PAIR_PIDREF(name, p) SD_JSON_BUILD_PAIR(name, JSON_BUILD_PIDREF(p))
#define JSON_BUILD_PAIR_DEVNUM(name, d) SD_JSON_BUILD_PAIR(name, JSON_BUILD_DEVNUM(d))
#define JSON_BUILD_PAIR_ENUM(name, s) SD_JSON_BUILD_PAIR(name, JSON_BUILD_STRING_UNDERSCORIFY(s))
#define JSON_BUILD_PAIR_ENUM_NON_EMPTY(name, s) JSON_BUILD_PAIR_STRING_NON_EMPTY_UNDERSCORIFY(name, s)
#define JSON_BUILD_PAIR_YES_NO(name, b) SD_JSON_BUILD_PAIR(name, SD_JSON_BUILD_STRING(yes_no(b)))
#define JSON_BUILD_PAIR_CONDITION_UNSIGNED(condition, name, value) \
+63
View File
@@ -1632,6 +1632,69 @@ TEST(access_mode) {
&mm), ERANGE);
}
TEST(job_id) {
_cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL;
ASSERT_OK(sd_json_parse("{\"a\": 1, \"b\": 4294967295, \"c\": null}",
/* flags= */ 0,
&v,
/* reterr_line= */ NULL,
/* reterr_column= */ NULL));
struct {
uint32_t a, b, c;
} data = { 99, 99, 99 };
ASSERT_OK(sd_json_dispatch(
v,
(const sd_json_dispatch_field[]) {
{ "a", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, voffsetof(data, a), 0 },
{ "b", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, voffsetof(data, b), 0 },
{ "c", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, voffsetof(data, c), 0 },
{},
},
/* flags= */ 0,
&data));
ASSERT_EQ(data.a, UINT32_C(1));
ASSERT_EQ(data.b, UINT32_MAX);
ASSERT_EQ(data.c, UINT32_C(0));
/* Zero is not a valid job ID */
sd_json_variant_unrefp(&v);
ASSERT_OK(sd_json_parse("{\"a\": 0}",
/* flags= */ 0,
&v,
/* reterr_line= */ NULL,
/* reterr_column= */ NULL));
ASSERT_ERROR(sd_json_dispatch(
v,
(const sd_json_dispatch_field[]) {
{ "a", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, voffsetof(data, a), 0 },
{},
},
/* flags= */ 0,
&data), EINVAL);
/* Negative values are not valid */
sd_json_variant_unrefp(&v);
ASSERT_OK(sd_json_parse("{\"a\": -1}",
/* flags= */ 0,
&v,
/* reterr_line= */ NULL,
/* reterr_column= */ NULL));
ASSERT_ERROR(sd_json_dispatch(
v,
(const sd_json_dispatch_field[]) {
{ "a", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_job_id, voffsetof(data, a), 0 },
{},
},
/* flags= */ 0,
&data), EINVAL);
}
static void test_json_variant_compare_one(const char *a, const char *b, int expected) {
int r;
+47 -11
View File
@@ -39,22 +39,56 @@ SD_VARLINK_DEFINE_ENUM_TYPE(
SD_VARLINK_DEFINE_ENUM_VALUE(frozen),
SD_VARLINK_DEFINE_ENUM_VALUE(concurrency));
/* Field names match the D-Bus Job properties (Id, JobType, State) */
/* Field names match the D-Bus Job properties (Id, Unit, JobType, State) */
#define VARLINK_DEFINE_JOB_FIELDS(direction) \
SD_VARLINK_FIELD_COMMENT("The numeric job ID"), \
SD_VARLINK_DEFINE_##direction(Id, SD_VARLINK_INT, 0), \
SD_VARLINK_FIELD_COMMENT("The unit name this job operates on"), \
SD_VARLINK_DEFINE_##direction(Unit, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), \
SD_VARLINK_FIELD_COMMENT("The job type"), \
SD_VARLINK_DEFINE_##direction##_BY_TYPE(JobType, JobType, 0), \
SD_VARLINK_FIELD_COMMENT("Current job state. 'finished' indicates the job has completed; in that case Result is also set."), \
SD_VARLINK_DEFINE_##direction##_BY_TYPE(State, JobState, SD_VARLINK_NULLABLE), \
SD_VARLINK_FIELD_COMMENT("Job result. Only set once the job has reached the 'finished' state."), \
SD_VARLINK_DEFINE_##direction##_BY_TYPE(Result, JobResult, SD_VARLINK_NULLABLE), \
SD_VARLINK_FIELD_COMMENT("Activation details describing what triggered this job, as key-value pairs"), \
SD_VARLINK_DEFINE_##direction(ActivationDetails, SD_VARLINK_STRING, SD_VARLINK_NULLABLE|SD_VARLINK_MAP)
SD_VARLINK_DEFINE_STRUCT_TYPE(
Job,
SD_VARLINK_FIELD_COMMENT("The numeric job ID"),
SD_VARLINK_DEFINE_FIELD(Id, SD_VARLINK_INT, 0),
SD_VARLINK_FIELD_COMMENT("The job type"),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(JobType, JobType, 0),
SD_VARLINK_FIELD_COMMENT("Current job state. 'finished' indicates the job has completed; in that case Result is also set."),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(State, JobState, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("Job result. Only set once the job has reached the 'finished' state."),
SD_VARLINK_DEFINE_FIELD_BY_TYPE(Result, JobResult, SD_VARLINK_NULLABLE));
VARLINK_DEFINE_JOB_FIELDS(FIELD));
static SD_VARLINK_DEFINE_ERROR(
NoSuchJob,
SD_VARLINK_DEFINE_FIELD(parameter, SD_VARLINK_STRING, SD_VARLINK_NULLABLE));
static SD_VARLINK_DEFINE_METHOD_FULL(
List,
SD_VARLINK_SUPPORTS_MORE,
SD_VARLINK_FIELD_COMMENT("If non-null, filter by job ID"),
SD_VARLINK_DEFINE_INPUT(id, SD_VARLINK_INT, SD_VARLINK_NULLABLE),
SD_VARLINK_FIELD_COMMENT("If non-null, filter by unit name"),
SD_VARLINK_DEFINE_INPUT(unit, SD_VARLINK_STRING, SD_VARLINK_NULLABLE),
VARLINK_DEFINE_JOB_FIELDS(OUTPUT));
static SD_VARLINK_DEFINE_METHOD(
Cancel,
SD_VARLINK_FIELD_COMMENT("The job ID to cancel"),
SD_VARLINK_DEFINE_INPUT(id, SD_VARLINK_INT, 0));
static SD_VARLINK_DEFINE_METHOD(
ClearAll);
SD_VARLINK_DEFINE_INTERFACE(
io_systemd_Job,
"io.systemd.Job",
SD_VARLINK_INTERFACE_COMMENT("Job-related types for the systemd service manager."),
SD_VARLINK_INTERFACE_COMMENT("Job management interface for the systemd service manager."),
SD_VARLINK_SYMBOL_COMMENT("List queued jobs"),
&vl_method_List,
SD_VARLINK_SYMBOL_COMMENT("Cancel a specific job"),
&vl_method_Cancel,
SD_VARLINK_SYMBOL_COMMENT("Cancel all pending jobs"),
&vl_method_ClearAll,
SD_VARLINK_SYMBOL_COMMENT("Job type"),
&vl_type_JobType,
SD_VARLINK_SYMBOL_COMMENT("Job state"),
@@ -62,4 +96,6 @@ SD_VARLINK_DEFINE_INTERFACE(
SD_VARLINK_SYMBOL_COMMENT("Job result"),
&vl_type_JobResult,
SD_VARLINK_SYMBOL_COMMENT("A job object"),
&vl_type_Job);
&vl_type_Job,
SD_VARLINK_SYMBOL_COMMENT("The specified job does not exist"),
&vl_error_NoSuchJob);
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# test io.systemd.Job
varlinkctl introspect /run/systemd/io.systemd.Manager io.systemd.Job
# List with no jobs pending — should return empty with --more
varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Job.List '{}' --graceful=io.systemd.Job.NoSuchJob
# Without --more and no filter, must fail (streaming required)
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{}')
# Error cases: non-existent job ID, non-existent unit
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"id": 999999}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "non-existent.service"}')
# Invalid inputs
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"id": 0}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": ""}')
at_exit() {
systemctl stop varlink-test-job.service 2>/dev/null || true
rm -f /run/systemd/system/varlink-test-job.service
systemctl daemon-reload
}
trap at_exit EXIT
# Create a job by starting a slow service, then test List/Cancel
cat >/run/systemd/system/varlink-test-job.service <<UNIT
[Service]
Type=notify
ExecStart=sleep infinity
UNIT
systemctl daemon-reload
# Start asynchronously to create a job
systemctl start --no-block varlink-test-job.service
# The job should now be visible
job_id=$(varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}' | jq -r '.Id')
test "$job_id" -gt 0
# Verify job fields
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}' | jq -e '.Unit == "varlink-test-job.service"'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}' | jq -e '.JobType == "start"'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}' | jq -e '.State'
# Lookup by job ID should return the same job
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List "{\"id\": $job_id}" | jq -e ".Unit == \"varlink-test-job.service\""
# Lookup by both id and unit (matching) should succeed
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List "{\"id\": $job_id, \"unit\": \"varlink-test-job.service\"}" | jq -e ".Id == $job_id"
# Lookup by both id and unit (conflicting) should fail
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List "{\"id\": $((job_id + 1)), \"unit\": \"varlink-test-job.service\"}")
# The job should appear in the full listing
varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Job.List '{}' | grep "varlink-test-job.service"
# Cancel the job
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.Cancel "{\"id\": $job_id}"
# The job should no longer exist
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}')
# Cancel with non-existent job ID should fail
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.Cancel '{"id": 999999}')
# Test ClearAll: create another job, then clear all
systemctl start --no-block varlink-test-job.service
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}' | jq -e '.Id'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.ClearAll '{}'
# No jobs should remain for our test unit
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Job.List '{"unit": "varlink-test-job.service"}')
+16
View File
@@ -0,0 +1,16 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# test io.systemd.Metrics
varlinkctl info /run/systemd/report/io.systemd.Manager
varlinkctl list-methods /run/systemd/report/io.systemd.Manager
varlinkctl list-methods -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq .
varlinkctl introspect /run/systemd/report/io.systemd.Manager
varlinkctl introspect -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq .
varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.List '{}'
varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.Describe '{}'
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# test io.systemd.Network
varlinkctl info /run/systemd/netif/io.systemd.Network
varlinkctl introspect /run/systemd/netif/io.systemd.Network io.systemd.Network
varlinkctl call /run/systemd/netif/io.systemd.Network io.systemd.Network.Describe '{}'
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: LGPL-2.1-or-later
set -eux
set -o pipefail
# test io.systemd.Unit
varlinkctl info /run/systemd/io.systemd.Manager
varlinkctl introspect /run/systemd/io.systemd.Manager io.systemd.Unit
varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target"}'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 1}}'
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' |& grep "called without 'more' flag" >/dev/null)
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "init.scope", "pid": {"pid": 1}}'
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": ""}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "non-existent.service"}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": -1}}' )
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target", "pid": {"pid": 1}}')
set +o pipefail
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "non-existent.service", "properties": {"Markers": ["needs-restart"]}}' |& grep "io.systemd.Unit.NoSuchUnit"
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "systemd-journald.service", "properties": {"LoadState": "foobar"}}' |& grep "io.systemd.Unit.PropertyNotSupported"
set -o pipefail
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"cgroup": "/init.scope"}'
invocation_id="$(systemctl show -P InvocationID systemd-journald.service)"
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "{\"invocationID\": \"$invocation_id\"}"
# test for KillContext
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 0}}' | jq -e '.context.Kill'
# test for AutomountContext/Runtime
automount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "automount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$automount_id"
# Use jq to JSON-encode the unit name as it may contain backslash escapes (e.g. \x2d) that
# are not valid JSON escape sequences and would be rejected by varlinkctl's JSON parser.
automount_params=$(jq -cn --arg name "$automount_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.context.Automount'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.runtime.Automount'
# test for MountContext/Runtime
mount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "mount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$mount_id"
mount_params=$(jq -cn --arg name "$mount_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.context.Mount'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.runtime.Mount'
# test for PathContext/Runtime
path_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "path" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$path_id"
path_params=$(jq -cn --arg name "$path_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.context.Path'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.runtime.Path'
# test for ServiceContext/Runtime
service_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "service" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$service_id"
service_params=$(jq -cn --arg name "$service_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.context.Service'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.runtime.Service'
# test for ScopeContext/Runtime
scope_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "scope" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$scope_id"
scope_params=$(jq -cn --arg name "$scope_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.context.Scope'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.runtime.Scope'
# test for SocketContext/Runtime
socket_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "socket" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$socket_id"
socket_params=$(jq -cn --arg name "$socket_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.context.Socket'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.runtime.Socket'
# test for SwapContext/Runtime (swap units may not be present on all systems)
swap_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "swap" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
if test -n "$swap_id"; then
swap_params=$(jq -cn --arg name "$swap_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.context.Swap'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.runtime.Swap'
fi
# test for TimerContext/Runtime
timer_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "timer" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$timer_id"
timer_params=$(jq -cn --arg name "$timer_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.context.Timer'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.runtime.Timer'
# test io.systemd.Unit in user manager
testuser_uid=$(id -u testuser)
systemd-run --wait --pipe --user --machine testuser@ \
varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
+8 -95
View File
@@ -204,97 +204,14 @@ varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.Describe '{}'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.Reload '{}'
# This will disconnect and fail, as the manager reexec and drops connections
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Manager.Reexecute '{}' ||:
# test io.systemd.Network
varlinkctl info /run/systemd/netif/io.systemd.Network
varlinkctl introspect /run/systemd/netif/io.systemd.Network io.systemd.Network
varlinkctl call /run/systemd/netif/io.systemd.Network io.systemd.Network.Describe '{}'
# test io.systemd.Unit
varlinkctl info /run/systemd/io.systemd.Manager
varlinkctl introspect /run/systemd/io.systemd.Manager io.systemd.Unit
varlinkctl --more call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target"}'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 1}}'
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' |& grep "called without 'more' flag" >/dev/null)
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "init.scope", "pid": {"pid": 1}}'
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": ""}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "non-existent.service"}')
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": -1}}' )
(! varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"name": "multi-user.target", "pid": {"pid": 1}}')
set +o pipefail
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "non-existent.service", "properties": {"Markers": ["needs-restart"]}}' |& grep "io.systemd.Unit.NoSuchUnit"
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.SetProperties '{"runtime": true, "name": "systemd-journald.service", "properties": {"LoadState": "foobar"}}' |& grep "io.systemd.Unit.PropertyNotSupported"
set -o pipefail
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"cgroup": "/init.scope"}'
invocation_id="$(systemctl show -P InvocationID systemd-journald.service)"
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "{\"invocationID\": \"$invocation_id\"}"
# test for KillContext
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List '{"pid": {"pid": 0}}' | jq -e '.context.Kill'
# test for AutomountContext/Runtime
automount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "automount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$automount_id"
# Use jq to JSON-encode the unit name as it may contain backslash escapes (e.g. \x2d) that
# are not valid JSON escape sequences and would be rejected by varlinkctl's JSON parser.
automount_params=$(jq -cn --arg name "$automount_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.context.Automount'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$automount_params" | jq -e '.runtime.Automount'
# test for MountContext/Runtime
mount_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "mount" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$mount_id"
mount_params=$(jq -cn --arg name "$mount_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.context.Mount'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$mount_params" | jq -e '.runtime.Mount'
# test for PathContext/Runtime
path_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "path" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$path_id"
path_params=$(jq -cn --arg name "$path_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.context.Path'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$path_params" | jq -e '.runtime.Path'
# test for ServiceContext/Runtime
service_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "service" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$service_id"
service_params=$(jq -cn --arg name "$service_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.context.Service'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$service_params" | jq -e '.runtime.Service'
# test for ScopeContext/Runtime
scope_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "scope" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$scope_id"
scope_params=$(jq -cn --arg name "$scope_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.context.Scope'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$scope_params" | jq -e '.runtime.Scope'
# test for SocketContext/Runtime
socket_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "socket" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$socket_id"
socket_params=$(jq -cn --arg name "$socket_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.context.Socket'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$socket_params" | jq -e '.runtime.Socket'
# test for SwapContext/Runtime (swap units may not be present on all systems)
swap_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "swap" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
if test -n "$swap_id"; then
swap_params=$(jq -cn --arg name "$swap_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.context.Swap'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$swap_params" | jq -e '.runtime.Swap'
fi
# test for TimerContext/Runtime
timer_id=$(varlinkctl call --collect /run/systemd/io.systemd.Manager io.systemd.Unit.List '{}' | jq -r '.[] | select(.context.Type == "timer" and .runtime.LoadState == "loaded") .context.ID // empty' | tail -n 1)
test -n "$timer_id"
timer_params=$(jq -cn --arg name "$timer_id" '{name: $name}')
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.context.Timer'
varlinkctl call /run/systemd/io.systemd.Manager io.systemd.Unit.List "$timer_params" | jq -e '.runtime.Timer'
# test io.systemd.Metrics
varlinkctl info /run/systemd/report/io.systemd.Manager
varlinkctl list-methods /run/systemd/report/io.systemd.Manager
varlinkctl list-methods -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq .
varlinkctl introspect /run/systemd/report/io.systemd.Manager
varlinkctl introspect -j /run/systemd/report/io.systemd.Manager io.systemd.Metrics | jq .
varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.List {}
varlinkctl --more call /run/systemd/report/io.systemd.Manager io.systemd.Metrics.Describe {}
# Wait for the manager to finish re-exec before proceeding — the user manager
# tests below use systemd-run which requires a functional PID 1.
for _ in {1..10}; do
if systemctl is-system-running 2>/dev/null | grep -qE 'running|degraded'; then
break
fi
sleep 1
done
# test io.systemd.Manager in user manager
testuser_uid=$(id -u testuser)
@@ -305,10 +222,6 @@ systemd-run --wait --pipe --user --machine testuser@ \
systemd-run --wait --pipe --user --machine testuser@ \
varlinkctl call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Manager.Describe '{}'
# test io.systemd.Unit in user manager
systemd-run --wait --pipe --user --machine testuser@ \
varlinkctl --more call "/run/user/$testuser_uid/systemd/io.systemd.Manager" io.systemd.Unit.List '{}'
# test --upgrade (protocol upgrade)
# The basic --upgrade proxy test is covered by the "varlinkctl serve" tests below (which use
# serve+rev/gunzip as the server). The tests here exercise features that need the Python