diff --git a/src/core/varlink-unit.c b/src/core/varlink-unit.c index 85139f068d..cb67dfa70f 100644 --- a/src/core/varlink-unit.c +++ b/src/core/varlink-unit.c @@ -702,6 +702,18 @@ static void transient_exec_command_item_done(TransientExecCommandItem *i) { static JSON_DISPATCH_ENUM_DEFINE(dispatch_service_type, ServiceType, service_type_from_string); static JSON_DISPATCH_ENUM_DEFINE(dispatch_job_mode, JobMode, job_mode_from_string); +typedef struct TransientWorkingDirectory { + const char *path; + bool home; + bool missing_ok; +} TransientWorkingDirectory; + +typedef struct TransientExecContextParameters { + bool present; + bool working_directory_set; + TransientWorkingDirectory working_directory; +} TransientExecContextParameters; + typedef struct TransientServiceParameters { bool present; ServiceType type; @@ -753,7 +765,9 @@ static int dispatch_transient_exec_command(const char *name, sd_json_variant *va typedef struct StartTransientContextParameters { const char *id; const char *description; + TransientExecContextParameters exec; TransientServiceParameters service; + const char *bad_exec_field; /* Set by inner Exec dispatcher to the unknown sub-property name */ } StartTransientContextParameters; static void start_transient_context_parameters_done(StartTransientContextParameters *p) { @@ -766,12 +780,49 @@ typedef struct StartTransientParameters { JobMode mode; int notify_job_changes; int notify_unit_changes; - const char *unsupported_property; /* For error reporting on unknown context fields */ + char *unsupported_property; /* For error reporting on unknown context fields */ } StartTransientParameters; static void start_transient_parameters_done(StartTransientParameters *p) { assert(p); start_transient_context_parameters_done(&p->context); + free(p->unsupported_property); +} + +static int dispatch_const_string_empty_as_null(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + const char **s = ASSERT_PTR(userdata); + int r; + + r = sd_json_dispatch_const_string(name, variant, flags, s); + if (r >= 0 && isempty(*s)) + *s = NULL; + return r; +} + +static int dispatch_transient_working_directory(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + /* No equivalent D-Bus properties, so use varlink camelCase */ + static const sd_json_dispatch_field dispatch[] = { + { "path", SD_JSON_VARIANT_STRING, dispatch_const_string_empty_as_null, offsetof(TransientWorkingDirectory, path), 0 }, + { "home", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(TransientWorkingDirectory, home), 0 }, + { "missingOK", SD_JSON_VARIANT_BOOLEAN, sd_json_dispatch_stdbool, offsetof(TransientWorkingDirectory, missing_ok), 0 }, + {} + }; + + TransientExecContextParameters *p = ASSERT_PTR(userdata); + p->working_directory_set = true; + return sd_json_dispatch(variant, dispatch, flags, &p->working_directory); +} + +static int dispatch_transient_exec_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { + /* Key names compatible with D-Bus property names */ + static const sd_json_dispatch_field exec_dispatch[] = { + { "WorkingDirectory", SD_JSON_VARIANT_OBJECT, dispatch_transient_working_directory, 0, 0 }, + {} + }; + + StartTransientContextParameters *p = ASSERT_PTR(userdata); + p->exec.present = true; + return sd_json_dispatch_full(variant, exec_dispatch, /* bad= */ NULL, /* flags= */ 0, &p->exec, &p->bad_exec_field); } static int dispatch_transient_service(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { @@ -789,9 +840,10 @@ static int dispatch_transient_service(const char *name, sd_json_variant *variant static int dispatch_transient_context(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) { static const sd_json_dispatch_field context_dispatch[] = { - { "ID", SD_JSON_VARIANT_STRING, json_dispatch_const_unit_name, offsetof(StartTransientContextParameters, id), SD_JSON_MANDATORY }, - { "Description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(StartTransientContextParameters, description), 0 }, - { "Service", SD_JSON_VARIANT_OBJECT, dispatch_transient_service, 0, 0 }, + { "ID", SD_JSON_VARIANT_STRING, json_dispatch_const_unit_name, offsetof(StartTransientContextParameters, id), SD_JSON_MANDATORY }, + { "Description", SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(StartTransientContextParameters, description), 0 }, + { "Exec", SD_JSON_VARIANT_OBJECT, dispatch_transient_exec_context, 0, 0 }, + { "Service", SD_JSON_VARIANT_OBJECT, dispatch_transient_service, 0, 0 }, {} }; @@ -802,10 +854,18 @@ static int dispatch_transient_context(const char *name, sd_json_variant *variant /* Don't propagate the caller's flags (in particular SD_JSON_MANDATORY from the outer 'context' * field) into the nested dispatch, otherwise every inner field becomes mandatory. */ r = sd_json_dispatch_full(variant, context_dispatch, /* bad= */ NULL, /* flags= */ 0, &p->context, &bad_field); - if (r == -EADDRNOTAVAIL && !isempty(bad_field)) + if (r == -EADDRNOTAVAIL && !isempty(bad_field)) { /* A UnitContext field that exists in the schema but is not settable at creation time: stash - * the name so the caller can map this to io.systemd.Unit.PropertyNotSupported. */ - p->unsupported_property = bad_field; + * the name so the caller can map this to io.systemd.Unit.PropertyNotSupported. If the + * unknown field lives inside the nested Exec object, compose a dotted name to identify the + * actual sub-property. */ + if (streq(bad_field, "Exec") && !isempty(p->context.bad_exec_field)) + p->unsupported_property = strjoin("Exec.", p->context.bad_exec_field); + else + p->unsupported_property = strdup(bad_field); + if (!p->unsupported_property) + return -ENOMEM; + } return r; } @@ -825,6 +885,49 @@ static int transient_unit_apply_properties(Unit *u, StartTransientContextParamet return 0; } +static int transient_exec_context_apply_properties(Unit *u, ExecContext *c, TransientExecContextParameters *p) { + int r; + + assert(u); + assert(c); + assert(p); + + if (p->working_directory_set) { + TransientWorkingDirectory *wd = &p->working_directory; + _cleanup_free_ char *simplified = NULL; + + if (wd->home && wd->path) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "WorkingDirectory: 'home' and 'path' are mutually exclusive"); + if (!wd->home && !wd->path) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "WorkingDirectory: must specify either 'home' or 'path'"); + + if (!wd->home) { + if (!path_is_absolute(wd->path)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "WorkingDirectory: expects an absolute path"); + r = path_simplify_alloc(wd->path, &simplified); + if (r < 0) + return r; + if (!path_is_normalized(simplified)) + return log_debug_errno(SYNTHETIC_ERRNO(EINVAL), + "WorkingDirectory: expects a normalized path"); + } + + free_and_replace(c->working_directory, simplified); + c->working_directory_home = wd->home; + c->working_directory_missing_ok = wd->missing_ok; + + unit_write_settingf(u, UNIT_RUNTIME|UNIT_PRIVATE|UNIT_ESCAPE_SPECIFIERS, "WorkingDirectory", + "WorkingDirectory=%s%s", + c->working_directory_missing_ok ? "-" : "", + c->working_directory_home ? "~" : strempty(c->working_directory)); + } + + return 0; +} + static int transient_service_apply_properties(Service *s, TransientServiceParameters *sp) { Unit *u = UNIT(ASSERT_PTR(s)); int r; @@ -978,6 +1081,15 @@ int vl_method_start_transient_unit(sd_varlink *link, sd_json_variant *parameters if (r < 0) return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + /* Apply exec-specific properties from context.Exec */ + ExecContext *c = unit_get_exec_context(u); + if (c) { + r = transient_exec_context_apply_properties(u, c, &p.context.exec); + if (r < 0) + return sd_varlink_error(link, VARLINK_ERROR_UNIT_BAD_SETTING, NULL); + } else if (p.context.exec.present) + return sd_varlink_error(link, VARLINK_ERROR_UNIT_TYPE_NOT_SUPPORTED, NULL); + /* Apply service-specific properties from context.Service */ Service *s = SERVICE(u); if (s) { diff --git a/src/shared/varlink-io.systemd.Unit.c b/src/shared/varlink-io.systemd.Unit.c index e62fe40a62..dada1d5813 100644 --- a/src/shared/varlink-io.systemd.Unit.c +++ b/src/shared/varlink-io.systemd.Unit.c @@ -421,10 +421,12 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( /* ExecContext */ static SD_VARLINK_DEFINE_STRUCT_TYPE( WorkingDirectory, - SD_VARLINK_FIELD_COMMENT("The path to the working directory"), - SD_VARLINK_DEFINE_FIELD(path, SD_VARLINK_STRING, 0), + SD_VARLINK_FIELD_COMMENT("The path to the working directory. Mutually exclusive with 'home'"), + SD_VARLINK_DEFINE_FIELD(path, SD_VARLINK_STRING, SD_VARLINK_NULLABLE), + SD_VARLINK_FIELD_COMMENT("If true, use the configured user's home directory as the working directory. Mutually exclusive with 'path'"), + SD_VARLINK_DEFINE_FIELD(home, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE), SD_VARLINK_FIELD_COMMENT("Whether the path to the working directory is allowed to not exist"), - SD_VARLINK_DEFINE_FIELD(missingOK, SD_VARLINK_BOOL, 0)); + SD_VARLINK_DEFINE_FIELD(missingOK, SD_VARLINK_BOOL, SD_VARLINK_NULLABLE)); static SD_VARLINK_DEFINE_STRUCT_TYPE( PartitionMountOptions, @@ -1182,8 +1184,9 @@ static SD_VARLINK_DEFINE_STRUCT_TYPE( SD_VARLINK_DEFINE_FIELD(parameter, SD_VARLINK_STRING, SD_VARLINK_NULLABLE)); /* UnitContext is used both as input to StartTransient (subset settable at creation time: ID, - * Description, Service) and as output from List/StartTransient (full unit configuration). Fields that - * are not settable at creation time are rejected with PropertyNotSupported when supplied as input. */ + * Description, Service, and the Exec subset {WorkingDirectory}) and as output from + * List/StartTransient (full unit configuration). Fields that are not settable at creation time are + * rejected with PropertyNotSupported when supplied as input. */ static SD_VARLINK_DEFINE_STRUCT_TYPE( UnitContext, SD_VARLINK_FIELD_COMMENT("The unit type"), diff --git a/test/units/TEST-26-SYSTEMCTL.sh b/test/units/TEST-26-SYSTEMCTL.sh index ed030031d2..ec3f88b3a9 100755 --- a/test/units/TEST-26-SYSTEMCTL.sh +++ b/test/units/TEST-26-SYSTEMCTL.sh @@ -621,6 +621,27 @@ result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ echo "$result" | jq -e '.context.Service.ExecStart[0].arguments == ["/bin/true"]' timeout 30 bash -c 'until systemctl is-active varlink-transient-noargs.service; do sleep 0.5; done' +# Exec.WorkingDirectory +defer_transient_cleanup varlink-transient-exec.service +result=$(varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exec.service","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}') +echo "$result" | jq -e '.context.Exec.WorkingDirectory.path == "/tmp"' +timeout 30 bash -c 'until systemctl is-active varlink-transient-exec.service; do sleep 0.5; done' +systemctl show -P WorkingDirectory varlink-transient-exec.service | grep '^/tmp$' >/dev/null + +# WorkingDirectory with missingOK=true (path does not exist but unit still starts) +defer_transient_cleanup varlink-transient-wd-missing.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-wd-missing.service","Exec":{"WorkingDirectory":{"path":"/nonexistent/path","missingOK":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' +timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-missing.service; do sleep 0.5; done' + +# WorkingDirectory with home=true, missingOK omitted (defaults to false) +defer_transient_cleanup varlink-transient-wd-home.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-wd-home.service","Exec":{"WorkingDirectory":{"home":true}},"Service":{"Type":"oneshot","RemainAfterExit":true,"ExecStart":[{"path":"/bin/true"}]}}}' +timeout 30 bash -c 'until systemctl is-active varlink-transient-wd-home.service; do sleep 0.5; done' +systemctl show -P WorkingDirectory varlink-transient-wd-home.service | grep '^~$' >/dev/null + # Error cases: verify specific varlink error types set +o pipefail varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ @@ -634,6 +655,17 @@ varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ defer_transient_cleanup varlink-transient-badpath.service varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ '{"context":{"ID":"varlink-transient-badpath.service","Service":{"Type":"simple","ExecStart":[{"path":""}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +# Relative WorkingDirectory path is rejected +defer_transient_cleanup varlink-transient-bad-wd.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-bad-wd.service","Exec":{"WorkingDirectory":{"path":"relative/path","missingOK":false}},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.BadUnitSetting" +# Exec on a unit type without an exec context (.target) is rejected +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-exec.slice","Exec":{"WorkingDirectory":{"path":"/tmp","missingOK":false}}}}' |& grep "io.systemd.Unit.UnitTypeNotSupported" +# Unknown field in Exec is rejected as PropertyNotSupported +defer_transient_cleanup varlink-transient-unknown-exec.service +varlinkctl call "$MANAGER_SOCKET" io.systemd.Unit.StartTransient \ + '{"context":{"ID":"varlink-transient-unknown-exec.service","Exec":{"RootDirectory":"/tmp"},"Service":{"Type":"oneshot","ExecStart":[{"path":"/bin/true"}]}}}' |& grep "io.systemd.Unit.PropertyNotSupported" set -o pipefail transient_cleanup