494 lines
22 KiB
Diff
494 lines
22 KiB
Diff
From e31e2b84cf5afff73b793dd335600265dca1ca7e Mon Sep 17 00:00:00 2001
|
|
From: Luca Boccassi <luca.boccassi@microsoft.com>
|
|
Date: Tue, 25 Jan 2022 15:49:22 +0000
|
|
Subject: [PATCH] portable: add flag to return extension-releases in
|
|
GetImageMetadataWithExtensions
|
|
|
|
Return the name of each extension and the associated extension-release
|
|
file, and pretty-print them in 'portablectl inspect', if a new flag
|
|
is passed.
|
|
|
|
$ portablectl inspect --extension app2 --extension app0 minimal app0 app1
|
|
(Matching unit files with prefixes 'app0', 'app1'.)
|
|
Image:
|
|
/run/portables/minimal.raw
|
|
Portable Service:
|
|
n/a
|
|
Operating System:
|
|
Debian GNU/Linux 10 (buster)
|
|
Extension:
|
|
/run/portables/app2.raw
|
|
Extension Scope:
|
|
n/a
|
|
Extension Compatibility Level:
|
|
n/a
|
|
Portable Service:
|
|
n/a
|
|
Portable Prefixes:
|
|
n/a
|
|
Operating System:
|
|
n/a (debian 10)
|
|
Extension:
|
|
/run/portables/app0.raw
|
|
Extension Scope:
|
|
n/a
|
|
Extension Compatibility Level:
|
|
n/a
|
|
Portable Service:
|
|
n/a
|
|
Portable Prefixes:
|
|
n/a
|
|
Operating System:
|
|
n/a (debian 10)
|
|
Unit files:
|
|
app0.service
|
|
|
|
(cherry picked from commit e3f7ed944ae750a40685c52349f3cc850db0876e)
|
|
(cherry picked from commit a87fdd2af22128bce621508315ed5126a8d11f45)
|
|
|
|
Conflict:NA
|
|
Reference:https://github.com/systemd/systemd/commit/e31e2b84cf5afff73b793dd335600265dca1ca7e
|
|
---
|
|
man/org.freedesktop.portable1.xml | 10 ++-
|
|
src/portable/portable.c | 99 ++++++++++++++++++++----------
|
|
src/portable/portable.h | 15 ++---
|
|
src/portable/portablectl.c | 78 ++++++++++++++++++++++-
|
|
src/portable/portabled-image-bus.c | 38 ++++++++++--
|
|
test/units/testsuite-29.sh | 3 +
|
|
6 files changed, 199 insertions(+), 44 deletions(-)
|
|
|
|
diff --git a/man/org.freedesktop.portable1.xml b/man/org.freedesktop.portable1.xml
|
|
index 53c960206e..053f2a5434 100644
|
|
--- a/man/org.freedesktop.portable1.xml
|
|
+++ b/man/org.freedesktop.portable1.xml
|
|
@@ -187,7 +187,15 @@ node /org/freedesktop/portable1 {
|
|
This method is a superset of <function>GetImageMetadata()</function> with the addition of
|
|
a list of extensions as input parameter, which were overlaid on top of the main
|
|
image via <function>AttachImageWithExtensions()</function>.
|
|
- The <varname>flag</varname> parameter is currently unused and reserved for future purposes.</para>
|
|
+ The <varname>flag</varname> parameter can be used to request that, before the units, the path of
|
|
+ each extension and an array of bytes with the content of the respective extension-release file
|
|
+ are sent. One such structure will be sent for each extension named in the input arguments. The
|
|
+ flag value to enable this functionality is defined as follows:</para>
|
|
+
|
|
+ <programlisting>
|
|
+#define PORTABLE_INSPECT_EXTENSION_RELEASES (UINT64_C(1) << 1)
|
|
+ </programlisting>
|
|
+
|
|
|
|
<para><function>GetImageState()</function> retrieves the image state as one of the following
|
|
strings:
|
|
diff --git a/src/portable/portable.c b/src/portable/portable.c
|
|
index 8c5e5b6821..23fe6bf926 100644
|
|
--- a/src/portable/portable.c
|
|
+++ b/src/portable/portable.c
|
|
@@ -533,13 +533,14 @@ static int extract_image_and_extensions(
|
|
bool validate_sysext,
|
|
Image **ret_image,
|
|
OrderedHashmap **ret_extension_images,
|
|
+ OrderedHashmap **ret_extension_releases,
|
|
PortableMetadata **ret_os_release,
|
|
Hashmap **ret_unit_files,
|
|
sd_bus_error *error) {
|
|
|
|
_cleanup_free_ char *id = NULL, *version_id = NULL, *sysext_level = NULL;
|
|
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
|
|
- _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
|
|
+ _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
|
|
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
|
|
_cleanup_(image_unrefp) Image *image = NULL;
|
|
Image *ext;
|
|
@@ -561,6 +562,12 @@ static int extract_image_and_extensions(
|
|
if (!extension_images)
|
|
return -ENOMEM;
|
|
|
|
+ if (ret_extension_releases) {
|
|
+ extension_releases = ordered_hashmap_new(&portable_metadata_hash_ops);
|
|
+ if (!extension_releases)
|
|
+ return -ENOMEM;
|
|
+ }
|
|
+
|
|
STRV_FOREACH(p, extension_image_paths) {
|
|
_cleanup_(image_unrefp) Image *new = NULL;
|
|
|
|
@@ -600,6 +607,7 @@ static int extract_image_and_extensions(
|
|
_cleanup_(portable_metadata_unrefp) PortableMetadata *extension_release_meta = NULL;
|
|
_cleanup_hashmap_free_ Hashmap *extra_unit_files = NULL;
|
|
_cleanup_strv_free_ char **extension_release = NULL;
|
|
+ _cleanup_close_ int extension_release_fd = -1;
|
|
_cleanup_fclose_ FILE *f = NULL;
|
|
|
|
r = portable_extract_by_path(ext->path, /* path_is_extension= */ true, matches, &extension_release_meta, &extra_unit_files, error);
|
|
@@ -610,10 +618,15 @@ static int extract_image_and_extensions(
|
|
if (r < 0)
|
|
return r;
|
|
|
|
- if (!validate_sysext)
|
|
+ if (!validate_sysext && !ret_extension_releases)
|
|
continue;
|
|
|
|
- r = take_fdopen_unlocked(&extension_release_meta->fd, "r", &f);
|
|
+ /* We need to keep the fd valid, to return the PortableMetadata to the caller. */
|
|
+ extension_release_fd = fd_reopen(extension_release_meta->fd, O_CLOEXEC);
|
|
+ if (extension_release_fd < 0)
|
|
+ return extension_release_fd;
|
|
+
|
|
+ r = take_fdopen_unlocked(&extension_release_fd, "r", &f);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
@@ -621,15 +634,28 @@ static int extract_image_and_extensions(
|
|
if (r < 0)
|
|
return r;
|
|
|
|
- r = extension_release_validate(ext->path, id, version_id, sysext_level, extension_release);
|
|
- if (r == 0)
|
|
- return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
|
|
- if (r < 0)
|
|
- return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path);
|
|
+ if (validate_sysext) {
|
|
+ r = extension_release_validate(ext->path, id, version_id, sysext_level, extension_release);
|
|
+ if (r == 0)
|
|
+ return sd_bus_error_set_errnof(error, SYNTHETIC_ERRNO(ESTALE), "Image %s extension-release metadata does not match the root's", ext->path);
|
|
+ if (r < 0)
|
|
+ return sd_bus_error_set_errnof(error, r, "Failed to compare image %s extension-release metadata with the root's os-release: %m", ext->path);
|
|
+ }
|
|
+
|
|
+ if (ret_extension_releases) {
|
|
+ r = ordered_hashmap_put(extension_releases, ext->name, extension_release_meta);
|
|
+ if (r < 0)
|
|
+ return r;
|
|
+ TAKE_PTR(extension_release_meta);
|
|
+ }
|
|
}
|
|
|
|
- *ret_image = TAKE_PTR(image);
|
|
- *ret_extension_images = TAKE_PTR(extension_images);
|
|
+ if (ret_image)
|
|
+ *ret_image = TAKE_PTR(image);
|
|
+ if (ret_extension_images)
|
|
+ *ret_extension_images = TAKE_PTR(extension_images);
|
|
+ if (ret_extension_releases)
|
|
+ *ret_extension_releases = TAKE_PTR(extension_releases);
|
|
if (ret_os_release)
|
|
*ret_os_release = TAKE_PTR(os_release);
|
|
if (ret_unit_files)
|
|
@@ -643,24 +669,29 @@ int portable_extract(
|
|
char **matches,
|
|
char **extension_image_paths,
|
|
PortableMetadata **ret_os_release,
|
|
+ OrderedHashmap **ret_extension_releases,
|
|
Hashmap **ret_unit_files,
|
|
sd_bus_error *error) {
|
|
|
|
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
|
|
- _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL;
|
|
+ _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_images = NULL, *extension_releases = NULL;
|
|
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
|
|
_cleanup_(image_unrefp) Image *image = NULL;
|
|
int r;
|
|
|
|
- r = extract_image_and_extensions(name_or_path,
|
|
- matches,
|
|
- extension_image_paths,
|
|
- /* validate_sysext= */ false,
|
|
- &image,
|
|
- &extension_images,
|
|
- &os_release,
|
|
- &unit_files,
|
|
- error);
|
|
+ assert(name_or_path);
|
|
+
|
|
+ r = extract_image_and_extensions(
|
|
+ name_or_path,
|
|
+ matches,
|
|
+ extension_image_paths,
|
|
+ /* validate_sysext= */ false,
|
|
+ &image,
|
|
+ &extension_images,
|
|
+ &extension_releases,
|
|
+ &os_release,
|
|
+ &unit_files,
|
|
+ error);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
@@ -677,8 +708,12 @@ int portable_extract(
|
|
isempty(extensions) ? "" : extensions);
|
|
}
|
|
|
|
- *ret_os_release = TAKE_PTR(os_release);
|
|
- *ret_unit_files = TAKE_PTR(unit_files);
|
|
+ if (ret_os_release)
|
|
+ *ret_os_release = TAKE_PTR(os_release);
|
|
+ if (ret_extension_releases)
|
|
+ *ret_extension_releases = TAKE_PTR(extension_releases);
|
|
+ if (ret_unit_files)
|
|
+ *ret_unit_files = TAKE_PTR(unit_files);
|
|
|
|
return 0;
|
|
}
|
|
@@ -1225,15 +1260,17 @@ int portable_attach(
|
|
PortableMetadata *item;
|
|
int r;
|
|
|
|
- r = extract_image_and_extensions(name_or_path,
|
|
- matches,
|
|
- extension_image_paths,
|
|
- /* validate_sysext= */ true,
|
|
- &image,
|
|
- &extension_images,
|
|
- /* os_release= */ NULL,
|
|
- &unit_files,
|
|
- error);
|
|
+ r = extract_image_and_extensions(
|
|
+ name_or_path,
|
|
+ matches,
|
|
+ extension_image_paths,
|
|
+ /* validate_sysext= */ true,
|
|
+ &image,
|
|
+ &extension_images,
|
|
+ /* extension_releases= */ NULL,
|
|
+ /* os_release= */ NULL,
|
|
+ &unit_files,
|
|
+ error);
|
|
if (r < 0)
|
|
return r;
|
|
|
|
diff --git a/src/portable/portable.h b/src/portable/portable.h
|
|
index 94144287ae..ce55f050a2 100644
|
|
--- a/src/portable/portable.h
|
|
+++ b/src/portable/portable.h
|
|
@@ -20,13 +20,14 @@ typedef struct PortableMetadata {
|
|
#define PORTABLE_METADATA_IS_UNIT(m) (!IN_SET((m)->name[0], 0, '/'))
|
|
|
|
typedef enum PortableFlags {
|
|
- PORTABLE_RUNTIME = 1 << 0, /* Public API via DBUS, do not change */
|
|
- PORTABLE_PREFER_COPY = 1 << 1,
|
|
- PORTABLE_PREFER_SYMLINK = 1 << 2,
|
|
- PORTABLE_REATTACH = 1 << 3,
|
|
- _PORTABLE_MASK_PUBLIC = PORTABLE_RUNTIME,
|
|
+ PORTABLE_RUNTIME = 1 << 0,
|
|
+ PORTABLE_INSPECT_EXTENSION_RELEASES = 1 << 1, /* Public API via DBUS, do not change */
|
|
+ PORTABLE_PREFER_COPY = 1 << 2,
|
|
+ PORTABLE_PREFER_SYMLINK = 1 << 3,
|
|
+ PORTABLE_REATTACH = 1 << 4,
|
|
+ _PORTABLE_MASK_PUBLIC = PORTABLE_RUNTIME | PORTABLE_INSPECT_EXTENSION_RELEASES,
|
|
_PORTABLE_TYPE_MAX,
|
|
- _PORTABLE_TYPE_INVALID = -EINVAL,
|
|
+ _PORTABLE_TYPE_INVALID = -EINVAL,
|
|
} PortableFlags;
|
|
|
|
/* This enum is anonymous, since we usually store it in an 'int', as we overload it with negative errno
|
|
@@ -64,7 +65,7 @@ DEFINE_TRIVIAL_CLEANUP_FUNC(PortableMetadata*, portable_metadata_unref);
|
|
|
|
int portable_metadata_hashmap_to_sorted_array(Hashmap *unit_files, PortableMetadata ***ret);
|
|
|
|
-int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, Hashmap **ret_unit_files, sd_bus_error *error);
|
|
+int portable_extract(const char *image, char **matches, char **extension_image_paths, PortableMetadata **ret_os_release, OrderedHashmap **ret_extension_releases, Hashmap **ret_unit_files, sd_bus_error *error);
|
|
|
|
int portable_attach(sd_bus *bus, const char *name_or_path, char **matches, const char *profile, char **extension_images, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
|
|
int portable_detach(sd_bus *bus, const char *name_or_path, char **extension_image_paths, PortableFlags flags, PortableChange **changes, size_t *n_changes, sd_bus_error *error);
|
|
diff --git a/src/portable/portablectl.c b/src/portable/portablectl.c
|
|
index 2d8079ad97..af5e78c998 100644
|
|
--- a/src/portable/portablectl.c
|
|
+++ b/src/portable/portablectl.c
|
|
@@ -259,8 +259,8 @@ static int maybe_reload(sd_bus **bus) {
|
|
static int get_image_metadata(sd_bus *bus, const char *image, char **matches, sd_bus_message **reply) {
|
|
_cleanup_(sd_bus_message_unrefp) sd_bus_message *m = NULL;
|
|
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
|
+ PortableFlags flags = PORTABLE_INSPECT_EXTENSION_RELEASES;
|
|
const char *method;
|
|
- uint64_t flags = 0;
|
|
int r;
|
|
|
|
assert(bus);
|
|
@@ -365,6 +365,74 @@ static int inspect_image(int argc, char *argv[], void *userdata) {
|
|
if (r < 0)
|
|
return bus_log_parse_error(r);
|
|
|
|
+ /* If we specified any extensions, we'll first get back exactly the
|
|
+ * paths (and extension-release content) for each one of the arguments. */
|
|
+ for (size_t i = 0; i < strv_length(arg_extension_images); ++i) {
|
|
+ const char *name;
|
|
+
|
|
+ r = sd_bus_message_enter_container(reply, 'e', "say");
|
|
+ if (r < 0)
|
|
+ return bus_log_parse_error(r);
|
|
+ if (r == 0)
|
|
+ break;
|
|
+
|
|
+ r = sd_bus_message_read(reply, "s", &name);
|
|
+ if (r < 0)
|
|
+ return bus_log_parse_error(r);
|
|
+
|
|
+ r = sd_bus_message_read_array(reply, 'y', &data, &sz);
|
|
+ if (r < 0)
|
|
+ return bus_log_parse_error(r);
|
|
+
|
|
+ if (arg_cat) {
|
|
+ if (nl)
|
|
+ fputc('\n', stdout);
|
|
+
|
|
+ printf("%s-- Extension Release: %s --%s\n", ansi_highlight(), name, ansi_normal());
|
|
+ fwrite(data, sz, 1, stdout);
|
|
+ fflush(stdout);
|
|
+ nl = true;
|
|
+ } else {
|
|
+ _cleanup_free_ char *pretty_portable = NULL, *pretty_os = NULL, *sysext_level = NULL,
|
|
+ *id = NULL, *version_id = NULL, *sysext_scope = NULL, *portable_prefixes = NULL;
|
|
+ _cleanup_fclose_ FILE *f = NULL;
|
|
+
|
|
+ f = fmemopen_unlocked((void*) data, sz, "re");
|
|
+ if (!f)
|
|
+ return log_error_errno(errno, "Failed to open extension-release buffer: %m");
|
|
+
|
|
+ r = parse_env_file(f, name,
|
|
+ "ID", &id,
|
|
+ "VERSION_ID", &version_id,
|
|
+ "SYSEXT_SCOPE", &sysext_scope,
|
|
+ "SYSEXT_LEVEL", &sysext_level,
|
|
+ "PORTABLE_PRETTY_NAME", &pretty_portable,
|
|
+ "PORTABLE_PREFIXES", &portable_prefixes,
|
|
+ "PRETTY_NAME", &pretty_os);
|
|
+ if (r < 0)
|
|
+ return log_error_errno(r, "Failed to parse extension release from '%s': %m", name);
|
|
+
|
|
+ printf("Extension:\n\t%s\n"
|
|
+ "\tExtension Scope:\n\t\t%s\n"
|
|
+ "\tExtension Compatibility Level:\n\t\t%s\n"
|
|
+ "\tPortable Service:\n\t\t%s\n"
|
|
+ "\tPortable Prefixes:\n\t\t%s\n"
|
|
+ "\tOperating System:\n\t\t%s (%s %s)\n",
|
|
+ name,
|
|
+ strna(sysext_scope),
|
|
+ strna(sysext_level),
|
|
+ strna(pretty_portable),
|
|
+ strna(portable_prefixes),
|
|
+ strna(pretty_os),
|
|
+ strna(id),
|
|
+ strna(version_id));
|
|
+ }
|
|
+
|
|
+ r = sd_bus_message_exit_container(reply);
|
|
+ if (r < 0)
|
|
+ return bus_log_parse_error(r);
|
|
+ }
|
|
+
|
|
for (;;) {
|
|
const char *name;
|
|
|
|
@@ -699,6 +767,14 @@ static int maybe_stop_disable(sd_bus *bus, char *image, char *argv[]) {
|
|
if (r < 0)
|
|
return bus_log_parse_error(r);
|
|
|
|
+ /* If we specified any extensions, we'll first get back exactly the
|
|
+ * paths (and extension-release content) for each one of the arguments. */
|
|
+ for (size_t i = 0; i < strv_length(arg_extension_images); ++i) {
|
|
+ r = sd_bus_message_skip(reply, "{say}");
|
|
+ if (r < 0)
|
|
+ return bus_log_parse_error(r);
|
|
+ }
|
|
+
|
|
for (;;) {
|
|
const char *name;
|
|
|
|
diff --git a/src/portable/portabled-image-bus.c b/src/portable/portabled-image-bus.c
|
|
index 23c6e2633a..3e6310f23e 100644
|
|
--- a/src/portable/portabled-image-bus.c
|
|
+++ b/src/portable/portabled-image-bus.c
|
|
@@ -102,13 +102,13 @@ int bus_image_common_get_metadata(
|
|
Image *image,
|
|
sd_bus_error *error) {
|
|
|
|
+ _cleanup_ordered_hashmap_free_ OrderedHashmap *extension_releases = NULL;
|
|
_cleanup_(portable_metadata_unrefp) PortableMetadata *os_release = NULL;
|
|
_cleanup_strv_free_ char **matches = NULL, **extension_images = NULL;
|
|
_cleanup_hashmap_free_ Hashmap *unit_files = NULL;
|
|
_cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
|
|
_cleanup_free_ PortableMetadata **sorted = NULL;
|
|
- /* Unused for now, but added to the DBUS methods for future-proofing */
|
|
- uint64_t input_flags = 0;
|
|
+ PortableFlags flags = 0;
|
|
size_t i;
|
|
int r;
|
|
|
|
@@ -133,14 +133,17 @@ int bus_image_common_get_metadata(
|
|
|
|
if (sd_bus_message_is_method_call(message, NULL, "GetImageMetadataWithExtensions") ||
|
|
sd_bus_message_is_method_call(message, NULL, "GetMetadataWithExtensions")) {
|
|
+ uint64_t input_flags = 0;
|
|
+
|
|
r = sd_bus_message_read(message, "t", &input_flags);
|
|
if (r < 0)
|
|
return r;
|
|
- /* Let clients know that this version doesn't support any flags */
|
|
- if (input_flags != 0)
|
|
+
|
|
+ if ((input_flags & ~_PORTABLE_MASK_PUBLIC) != 0)
|
|
return sd_bus_reply_method_errorf(message, SD_BUS_ERROR_INVALID_ARGS,
|
|
"Invalid 'flags' parameter '%" PRIu64 "'",
|
|
input_flags);
|
|
+ flags |= input_flags;
|
|
}
|
|
|
|
r = bus_image_acquire(m,
|
|
@@ -161,6 +164,7 @@ int bus_image_common_get_metadata(
|
|
matches,
|
|
extension_images,
|
|
&os_release,
|
|
+ &extension_releases,
|
|
&unit_files,
|
|
error);
|
|
if (r < 0)
|
|
@@ -186,6 +190,32 @@ int bus_image_common_get_metadata(
|
|
if (r < 0)
|
|
return r;
|
|
|
|
+ /* If it was requested, also send back the extension path and the content
|
|
+ * of each extension-release file. Behind a flag, as it's an incompatible
|
|
+ * change. */
|
|
+ if (FLAGS_SET(flags, PORTABLE_INSPECT_EXTENSION_RELEASES)) {
|
|
+ PortableMetadata *extension_release;
|
|
+
|
|
+ ORDERED_HASHMAP_FOREACH(extension_release, extension_releases) {
|
|
+
|
|
+ r = sd_bus_message_open_container(reply, 'e', "say");
|
|
+ if (r < 0)
|
|
+ return r;
|
|
+
|
|
+ r = sd_bus_message_append(reply, "s", extension_release->image_path);
|
|
+ if (r < 0)
|
|
+ return r;
|
|
+
|
|
+ r = append_fd(reply, extension_release);
|
|
+ if (r < 0)
|
|
+ return r;
|
|
+
|
|
+ r = sd_bus_message_close_container(reply);
|
|
+ if (r < 0)
|
|
+ return r;
|
|
+ }
|
|
+ }
|
|
+
|
|
for (i = 0; i < hashmap_size(unit_files); i++) {
|
|
|
|
r = sd_bus_message_open_container(reply, 'e', "say");
|
|
diff --git a/test/units/testsuite-29.sh b/test/units/testsuite-29.sh
|
|
index 34fa730514..ca09f321b7 100755
|
|
--- a/test/units/testsuite-29.sh
|
|
+++ b/test/units/testsuite-29.sh
|
|
@@ -80,6 +80,9 @@ systemctl is-active app1.service
|
|
portablectl "${ARGS[@]}" reattach --now --runtime --extension ${app1} ${root} app1
|
|
|
|
systemctl is-active app1.service
|
|
+portablectl inspect --cat --extension ${app1} ${root} app1 | grep -F "MARKER=1"
|
|
+portablectl inspect --cat --extension ${app1} ${root} app1 | grep -F "Extension Release: /usr/share/app1.raw"
|
|
+portablectl inspect --cat --extension ${app1} ${root} app1 | grep -F "ExecStart=/opt/script1.sh"
|
|
|
|
portablectl detach --now --runtime --extension ${app1} ${root} app1
|
|
|
|
--
|
|
2.33.0
|
|
|