portable: detect drop-in-only attachments

Commit edea370222 (portable: remove drop-in configs even if the main unit file does not exist)
taught detach to handle leftover .service.d directories
after the main unit symlink was removed.

portable_get_state_internal() still had the same blind
spot: it only considered regular unit entries, so portablectl
is-attached could report detached while a portable drop-in directory was still present.

Handle those drop-in-only entries like detach does, preserve
the existing unit-file based enabled checks when the
main unit file is still present, and add TEST-29 coverage.

Signed-off-by: dongshengyuan <dongshengyuan@uniontech.com>
(cherry picked from commit a977bf4d06)
(cherry picked from commit 60cba9ebf8)
(cherry picked from commit 05b1275c72)
This commit is contained in:
dongshengyuan
2026-07-04 17:05:33 +08:00
committed by Luca Boccassi
parent 567c60aad1
commit a6abe0a3e4
2 changed files with 84 additions and 39 deletions

View File

@@ -1883,6 +1883,42 @@ static int test_chroot_dropin(
return r;
}
static int portable_attached_dirent_name(
const struct dirent *de,
char **ret_unit_name,
bool *ret_dropin) {
_cleanup_free_ char *unit_name = NULL;
const char *dropin_suffix;
assert(de);
assert(ret_unit_name);
/* When a portable service is enabled with "portablectl --copy=symlink --enable --now attach",
* and is disabled with "portablectl --enable --now detach", which calls DisableUnitFilesWithFlags
* DBus method, the main unit file is removed, but its drop-ins are not. Hence, we need to list both
* main unit files and drop-in directories (without the main unit files). */
dropin_suffix = endswith(de->d_name, ".d");
if (dropin_suffix)
unit_name = strndup(de->d_name, dropin_suffix - de->d_name);
else
unit_name = strdup(de->d_name);
if (!unit_name)
return -ENOMEM;
if (!unit_name_is_valid(unit_name, UNIT_NAME_ANY))
return 0;
if (dropin_suffix ? !IN_SET(de->d_type, DT_LNK, DT_DIR) : !IN_SET(de->d_type, DT_LNK, DT_REG))
return 0;
*ret_unit_name = TAKE_PTR(unit_name);
if (ret_dropin)
*ret_dropin = dropin_suffix != NULL;
return 1;
}
int portable_detach(
RuntimeScope scope,
sd_bus *bus,
@@ -1919,31 +1955,17 @@ int portable_detach(
FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) {
_cleanup_free_ char *marker = NULL, *unit_name = NULL;
const char *dot;
/* When a portable service is enabled with "portablectl --copy=symlink --enable --now attach",
* and is disabled with "portablectl --enable --now detach", which calls DisableUnitFilesWithFlags
* DBus method, the main unit file is removed, but its drop-ins are not. Hence, here we need
* to list both main unit files and drop-in directories (without the main unit files). */
dot = endswith(de->d_name, ".d");
if (dot)
unit_name = strndup(de->d_name, dot - de->d_name);
else
unit_name = strdup(de->d_name);
if (!unit_name)
return -ENOMEM;
if (!unit_name_is_valid(unit_name, UNIT_NAME_ANY))
r = portable_attached_dirent_name(de, &unit_name, /* ret_dropin= */ NULL);
if (r < 0)
return r;
if (r == 0)
continue;
/* Filter out duplicates */
if (set_contains(unit_files, unit_name))
continue;
if (dot ? !IN_SET(de->d_type, DT_LNK, DT_DIR) : !IN_SET(de->d_type, DT_LNK, DT_REG))
continue;
r = test_chroot_dropin(d, where, unit_name, name_or_path, extension_image_paths, &marker);
if (r < 0)
return r;
@@ -2109,39 +2131,54 @@ static int portable_get_state_internal(
}
FOREACH_DIRENT(de, d, return log_debug_errno(errno, "Failed to enumerate '%s' directory: %m", where)) {
UnitFileState state;
_cleanup_free_ char *unit_name = NULL;
bool dropin;
if (!unit_name_is_valid(de->d_name, UNIT_NAME_ANY))
continue;
/* Filter out duplicates */
if (set_contains(unit_files, de->d_name))
continue;
if (!IN_SET(de->d_type, DT_LNK, DT_REG))
continue;
r = test_chroot_dropin(d, where, de->d_name, name_or_path, extension_image_paths, NULL);
r = portable_attached_dirent_name(de, &unit_name, &dropin);
if (r < 0)
return r;
if (r == 0)
continue;
r = unit_file_lookup_state(scope, &paths, de->d_name, &state);
if (r < 0)
return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", de->d_name);
if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME))
found_enabled = true;
/* Filter out duplicates */
if (set_contains(unit_files, unit_name))
continue;
r = unit_file_is_active(bus, de->d_name, error);
if (dropin) {
/* If the main unit file still exists, let the regular entry handle it so that
* enabled/running state is determined from the unit file as before. */
r = RET_NERRNO(faccessat(dirfd(d), unit_name, F_OK, AT_SYMLINK_NOFOLLOW));
if (r >= 0)
continue;
if (r != -ENOENT)
return log_debug_errno(r, "Failed to check if '%s/%s' exists: %m", where, unit_name);
}
r = test_chroot_dropin(d, where, unit_name, name_or_path, extension_image_paths, NULL);
if (r < 0)
return r;
if (r == 0)
continue;
if (!dropin) {
UnitFileState state;
r = unit_file_lookup_state(scope, &paths, unit_name, &state);
if (r < 0)
return log_debug_errno(r, "Failed to determine unit file state of '%s': %m", unit_name);
if (!IN_SET(state, UNIT_FILE_STATIC, UNIT_FILE_DISABLED, UNIT_FILE_LINKED, UNIT_FILE_LINKED_RUNTIME))
found_enabled = true;
}
r = unit_file_is_active(bus, unit_name, error);
if (r < 0)
return r;
if (r > 0)
found_running = true;
r = set_put_strdup(&unit_files, de->d_name);
r = set_ensure_consume(&unit_files, &string_hash_ops_free, TAKE_PTR(unit_name));
if (r < 0)
return log_debug_errno(r, "Failed to add unit name '%s' to set: %m", de->d_name);
return log_oom_debug();
}
*ret = found_running ? (!set_isempty(unit_files) && (flags & PORTABLE_RUNTIME) ? PORTABLE_RUNNING_RUNTIME : PORTABLE_RUNNING) :

View File

@@ -144,9 +144,17 @@ portablectl detach --now --runtime --extension /tmp/app0 /tmp/rootdir app0
# Provides coverage for https://github.com/systemd/systemd/issues/23481
portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
# attach and detach again to check if all drop-in configs are removed even if the main unit files are removed
# Attach and detach again to check if drop-in-only leftovers are still recognized and removed.
portablectl "${ARGS[@]}" attach --copy=symlink --now --runtime /tmp/rootdir minimal-app0
rm /run/systemd/system.attached/minimal-app0*.service
status="$(portablectl is-attached --runtime /tmp/rootdir)"
[[ "${status}" == "running-runtime" ]]
systemctl stop minimal-app0.service minimal-app0-foo.service
status="$(portablectl is-attached --runtime /tmp/rootdir)"
[[ "${status}" == "attached-runtime" ]]
portablectl detach --now --runtime --enable /tmp/rootdir minimal-app0
[[ ! -d /run/systemd/system.attached/minimal-app0.service.d ]]
[[ ! -d /run/systemd/system.attached/minimal-app0-foo.service.d ]]
# The wrong file should be ignored, given the right one has the xattr set
trap 'rm -rf /var/cache/wrongext' EXIT