diff --git a/pressure-vessel/pressure-vessel-test-ui b/pressure-vessel/pressure-vessel-test-ui index 2e212023502ad99f74a8f1550f44d4f7d7bea906..61f9e5c2079c2e02be9586b35ab3f8cd7a6b6445 100755 --- a/pressure-vessel/pressure-vessel-test-ui +++ b/pressure-vessel/pressure-vessel-test-ui @@ -81,6 +81,151 @@ def boolean_environment(name, default): return default +class ContainerRuntime: + def __init__( + self, + path, # type: str + home, # type: str + ): # type: (...) -> None + self.home = home + self.path = path + self.description = '' + + +class DirectoryRuntime(ContainerRuntime): + def __init__( + self, + path, # type: str + home, # type: str + ): # type: (...) -> None + super().__init__(path, home=home) + + self.description = self.__describe_runtime(path) + + def __describe_runtime( + self, + path # type: str + ): + # type: (...) -> str + + description = path + files = os.path.join(path, 'files') + metadata = os.path.join(path, 'metadata') + + if os.path.islink(files): + description = os.path.realpath(files) + + if description.startswith(self.home + '/'): + description = '~' + description[len(self.home):] + + name = None # type: typing.Optional[str] + pretty_name = None # type: typing.Optional[str] + build_id = None # type: typing.Optional[str] + variant = None # type: typing.Optional[str] + + try: + keyfile = GLib.KeyFile.new() + keyfile.load_from_file( + metadata, GLib.KeyFileFlags.NONE) + try: + build_id = keyfile.get_string('Runtime', 'x-flatdeb-build-id') + except GLib.Error: + pass + + try: + name = keyfile.get_string('Runtime', 'runtime') + except GLib.Error: + pass + else: + assert name is not None + variant = name.split('.')[-1] + except GLib.Error: + pass + + try: + with open( + os.path.join(files, 'lib', 'os-release') + ) as reader: + for line in reader: + if line.startswith('PRETTY_NAME='): + pretty_name = line.split('=', 1)[1].strip() + pretty_name = GLib.shell_unquote(pretty_name) + elif line.startswith('BUILD_ID='): + build_id = line.split('=', 1)[1].strip() + build_id = GLib.shell_unquote(build_id) + elif line.startswith('VARIANT='): + variant = line.split('=', 1)[1].strip() + variant = GLib.shell_unquote(variant) + except (GLib.Error, EnvironmentError): + pass + + if pretty_name is None: + pretty_name = name + + if pretty_name is None: + pretty_name = os.path.basename(path) + + if build_id is None: + build_id = '' + else: + build_id = ' build {}'.format(build_id) + + if variant is None: + variant = '' + else: + variant = ' {}'.format(variant) + + description = '{}{}{}\n({})'.format( + pretty_name, + variant, + build_id, + description, + ) + + return description + + +class ArchiveRuntime(ContainerRuntime): + def __init__( + self, + path, # type: str + buildid_file, # type: str + home, # type: str + ): # type: (...) -> None + super().__init__(path, home=home) + + if path.startswith(self.home + '/'): + path = '~' + path[len(self.home):] + + description = os.path.basename(path) + sdk_suffix = '' + + if description.startswith('com.valvesoftware.SteamRuntime.'): + description = description[len('com.valvesoftware.SteamRuntime.'):] + + if description.startswith('Platform-'): + description = description[len('Platform-'):] + + if description.startswith('Sdk-'): + sdk_suffix = '-sdk' + description = description[len('Sdk-'):] + + if description.startswith('amd64,i386-'): + description = description[len('amd64,i386-'):] + + if description.endswith('.tar.gz'): + description = description[:-len('.tar.gz')] + + if description.endswith('-runtime'): + description = description[:-len('-runtime')] + + with open(buildid_file) as reader: + build = reader.read().strip() + + self.deploy_id = '{}{}_{}'.format(description, sdk_suffix, build) + self.description = '{} build {}\n({})'.format(description, build, path) + + class Gui: def __init__(self): # type: (...) -> None @@ -88,7 +233,8 @@ class Gui: self.failed = False self.home = GLib.get_home_dir() - self.container_runtimes = {} # type: typing.Dict[str, str] + self.container_runtimes = { + } # type: typing.Dict[str, ContainerRuntime] for search in ( os.getenv('PRESSURE_VESSEL_RUNTIME_BASE'), @@ -115,8 +261,25 @@ class Gui: files = os.path.join(path, 'files') if os.path.isdir(files): - description = self._describe_runtime(path) - self.container_runtimes[path] = description + self.container_runtimes[path] = DirectoryRuntime( + path, + home=self.home, + ) + continue + + if member.endswith(('-runtime.tar.gz', '-sysroot.tar.gz')): + # runtime and sysroot happen to be the same length! + buildid_file = os.path.join( + source_of_runtimes, + member[:-len('-runtime.tar.gz')] + '-buildid.txt', + ) + + if os.path.exists(buildid_file): + self.container_runtimes[path] = ArchiveRuntime( + path, + buildid_file=buildid_file, + home=self.home, + ) self.window = Gtk.Window() self.window.set_default_size(600, 300) @@ -156,8 +319,8 @@ class Gui: 'None (use host system and traditional LD_LIBRARY_PATH runtime)' ) - for path, description in sorted(self.container_runtimes.items()): - self.container_runtime_combo.append(path, description) + for path, runtime in sorted(self.container_runtimes.items()): + self.container_runtime_combo.append(path, runtime.description) if self.container_runtimes: self.container_runtime_combo.set_active(1) @@ -172,10 +335,13 @@ class Gui: if var_path is None: value = False - var_path = os.getenv('PRESSURE_VESSEL_VARIABLE_DIR', None) + var_path = None else: value = True + var_path = os.getenv('PRESSURE_VESSEL_VARIABLE_DIR', var_path) + value = boolean_environment('PRESSURE_VESSEL_COPY_RUNTIME', value) + if var_path is None: var_path = os.getenv( 'PRESSURE_VESSEL_RUNTIME_BASE', @@ -184,11 +350,34 @@ class Gui: assert var_path is not None self.var_path = os.path.realpath(var_path) - self.copy_runtime_into_check = Gtk.CheckButton.new_with_label( + + self.gc_runtimes_check = Gtk.CheckButton.new_with_label( + 'Clean up old runtimes' + ) + # Deliberately ignoring PRESSURE_VESSEL_GC_RUNTIMES: a lot of the + # point of this test UI is the ability to switch between runtimes, + # but if we GC non-current runtimes, that'll be really slow. + self.gc_runtimes_check.set_active(False) + self.grid.attach(self.gc_runtimes_check, 1, row, 1, 1) + row += 1 + + label = Gtk.Label.new('') + label.set_markup( + '<small><i>' + 'Normally this is enabled, but this test UI disables it by ' + 'default for quicker switching between multiple runtimes.' + '</i></small>' + ) + label.set_halign(Gtk.Align.START) + label.set_line_wrap(True) + self.grid.attach(label, 1, row, 1, 1) + row += 1 + + self.copy_runtime_check = Gtk.CheckButton.new_with_label( 'Create temporary runtime copy on disk' ) - self.copy_runtime_into_check.set_active(value) - self.grid.attach(self.copy_runtime_into_check, 1, row, 1, 1) + self.copy_runtime_check.set_active(value) + self.grid.attach(self.copy_runtime_check, 1, row, 1, 1) row += 1 label = Gtk.Label.new('') @@ -411,94 +600,14 @@ class Gui: def _container_runtime_changed(self, combo): if combo.get_active_id() == '/': - self.copy_runtime_into_check.set_sensitive(False) + self.copy_runtime_check.set_sensitive(False) + self.gc_runtimes_check.set_sensitive(False) self.host_graphics_check.set_sensitive(False) else: - self.copy_runtime_into_check.set_sensitive(True) + self.copy_runtime_check.set_sensitive(True) + self.gc_runtimes_check.set_sensitive(True) self.host_graphics_check.set_sensitive(True) - def _describe_runtime( - self, - path # type: str - ): - # type: (...) -> str - - description = path - files = os.path.join(path, 'files') - metadata = os.path.join(path, 'metadata') - - if os.path.islink(files): - description = os.path.realpath(files) - - if description.startswith(self.home + '/'): - description = '~' + description[len(self.home):] - - name = None # type: typing.Optional[str] - pretty_name = None # type: typing.Optional[str] - build_id = None # type: typing.Optional[str] - variant = None # type: typing.Optional[str] - - try: - keyfile = GLib.KeyFile.new() - keyfile.load_from_file( - metadata, GLib.KeyFileFlags.NONE) - try: - build_id = keyfile.get_string('Runtime', 'x-flatdeb-build-id') - except GLib.Error: - pass - - try: - name = keyfile.get_string('Runtime', 'runtime') - except GLib.Error: - pass - else: - assert name is not None - variant = name.split('.')[-1] - except GLib.Error: - pass - - try: - with open( - os.path.join(files, 'lib', 'os-release') - ) as reader: - for line in reader: - if line.startswith('PRETTY_NAME='): - pretty_name = line.split('=', 1)[1].strip() - pretty_name = GLib.shell_unquote(pretty_name) - elif line.startswith('BUILD_ID='): - build_id = line.split('=', 1)[1].strip() - build_id = GLib.shell_unquote(build_id) - elif line.startswith('VARIANT='): - variant = line.split('=', 1)[1].strip() - variant = GLib.shell_unquote(variant) - except (GLib.Error, EnvironmentError): - pass - - if pretty_name is None: - pretty_name = name - - if pretty_name is None: - pretty_name = os.path.basename(path) - - if build_id is None: - build_id = '' - else: - build_id = ' build {}'.format(build_id) - - if variant is None: - variant = '' - else: - variant = ' {}'.format(variant) - - description = '{}{}{}\n({})'.format( - pretty_name, - variant, - build_id, - description, - ) - - return description - def run_cb(self, _ignored=None): # type: (typing.Any) -> None @@ -516,7 +625,13 @@ class Gui: elif id == '/': argv.append('--runtime=') else: - argv.append('--runtime=' + id) + runtime = self.container_runtimes[id] + + if isinstance(runtime, ArchiveRuntime): + argv.append('--runtime-archive=' + id) + argv.append('--runtime-id=' + runtime.deploy_id) + else: + argv.append('--runtime=' + id) if self.host_graphics_check.get_active(): if os.path.isdir('/run/host'): @@ -526,11 +641,17 @@ class Gui: else: argv.append('--graphics-provider=') - if self.copy_runtime_into_check.get_active(): + if self.copy_runtime_check.get_active(): os.makedirs(self.var_path, mode=0o755, exist_ok=True) - argv.append('--copy-runtime-into=' + self.var_path) + argv.append('--copy-runtime') + argv.append('--variable-dir=' + self.var_path) + else: + argv.append('--no-copy-runtime') + + if self.gc_runtimes_check.get_active(): + argv.append('--gc-runtimes') else: - argv.append('--copy-runtime-into=') + argv.append('--no-gc-runtimes') if self.unshare_home_check.get_active(): argv.append('--unshare-home') diff --git a/pressure-vessel/runtime.c b/pressure-vessel/runtime.c index 90ed82324f594c9be3c87923bf9fe09c1faedd1e..fb9b0fca9a35f2a1327b8785346c62cbbda6561e 100644 --- a/pressure-vessel/runtime.c +++ b/pressure-vessel/runtime.c @@ -54,6 +54,8 @@ struct _PvRuntime GObject parent; gchar *bubblewrap; + gchar *source; + gchar *id; gchar *deployment; gchar *source_files; /* either deployment or that + "/files" */ gchar *tools_dir; @@ -61,7 +63,7 @@ struct _PvRuntime GStrv original_environ; gchar *libcapsule_knowledge; - gchar *mutable_parent; + gchar *variable_dir; gchar *mutable_sysroot; gchar *tmpdir; gchar *overrides; @@ -78,7 +80,7 @@ struct _PvRuntime const gchar *host_in_current_namespace; PvRuntimeFlags flags; - int mutable_parent_fd; + int variable_dir_fd; int mutable_sysroot_fd; int provider_fd; gboolean any_libc_from_provider; @@ -96,10 +98,11 @@ struct _PvRuntimeClass enum { PROP_0, PROP_BUBBLEWRAP, - PROP_DEPLOYMENT, + PROP_SOURCE, PROP_ORIGINAL_ENVIRON, PROP_FLAGS, - PROP_MUTABLE_PARENT, + PROP_ID, + PROP_VARIABLE_DIR, PROP_PROVIDER_IN_CURRENT_NAMESPACE, PROP_PROVIDER_IN_CONTAINER_NAMESPACE, PROP_TOOLS_DIRECTORY, @@ -373,7 +376,7 @@ pv_runtime_init (PvRuntime *self) { self->any_libc_from_provider = FALSE; self->all_libc_from_provider = FALSE; - self->mutable_parent_fd = -1; + self->variable_dir_fd = -1; self->mutable_sysroot_fd = -1; } @@ -399,8 +402,12 @@ pv_runtime_get_property (GObject *object, g_value_set_flags (value, self->flags); break; - case PROP_MUTABLE_PARENT: - g_value_set_string (value, self->mutable_parent); + case PROP_ID: + g_value_set_string (value, self->id); + break; + + case PROP_VARIABLE_DIR: + g_value_set_string (value, self->variable_dir); break; case PROP_PROVIDER_IN_CURRENT_NAMESPACE: @@ -411,8 +418,8 @@ pv_runtime_get_property (GObject *object, g_value_set_string (value, self->provider_in_container_namespace); break; - case PROP_DEPLOYMENT: - g_value_set_string (value, self->deployment); + case PROP_SOURCE: + g_value_set_string (value, self->source); break; case PROP_TOOLS_DIRECTORY: @@ -452,25 +459,31 @@ pv_runtime_set_property (GObject *object, self->flags = g_value_get_flags (value); break; - case PROP_MUTABLE_PARENT: + case PROP_VARIABLE_DIR: /* Construct-only */ - g_return_if_fail (self->mutable_parent == NULL); + g_return_if_fail (self->variable_dir == NULL); path = g_value_get_string (value); if (path != NULL) { - self->mutable_parent = realpath (path, NULL); + self->variable_dir = realpath (path, NULL); - if (self->mutable_parent == NULL) + if (self->variable_dir == NULL) { /* It doesn't exist. Keep the non-canonical path so we * can warn about it later */ - self->mutable_parent = g_strdup (path); + self->variable_dir = g_strdup (path); } } break; + case PROP_ID: + /* Construct-only */ + g_return_if_fail (self->id == NULL); + self->id = g_value_dup_string (value); + break; + case PROP_PROVIDER_IN_CURRENT_NAMESPACE: /* Construct-only */ g_return_if_fail (self->provider_in_current_namespace == NULL); @@ -483,20 +496,20 @@ pv_runtime_set_property (GObject *object, self->provider_in_container_namespace = g_value_dup_string (value); break; - case PROP_DEPLOYMENT: + case PROP_SOURCE: /* Construct-only */ - g_return_if_fail (self->deployment == NULL); + g_return_if_fail (self->source == NULL); path = g_value_get_string (value); if (path != NULL) { - self->deployment = realpath (path, NULL); + self->source = realpath (path, NULL); - if (self->deployment == NULL) + if (self->source == NULL) { /* It doesn't exist. Keep the non-canonical path so we * can warn about it later */ - self->deployment = g_strdup (path); + self->source = g_strdup (path); } } @@ -524,36 +537,238 @@ pv_runtime_constructed (GObject *object) g_return_if_fail (self->original_environ != NULL); g_return_if_fail (self->provider_in_current_namespace != NULL); g_return_if_fail (self->provider_in_container_namespace != NULL); - g_return_if_fail (self->deployment != NULL); + g_return_if_fail (self->source != NULL); g_return_if_fail (self->tools_dir != NULL); } +static void +pv_runtime_maybe_garbage_collect_subdir (const char *description, + const char *parent, + int parent_fd, + const char *member) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(PvBwrapLock) temp_lock = NULL; + g_autofree gchar *keep = NULL; + g_autofree gchar *ref = NULL; + struct stat ignore; + + g_return_if_fail (parent != NULL); + g_return_if_fail (parent_fd >= 0); + g_return_if_fail (member != NULL); + + g_debug ("Found %s %s/%s, considering whether to delete it...", + description, parent, member); + + keep = g_build_filename (member, "keep", NULL); + + if (glnx_fstatat (parent_fd, keep, &ignore, + AT_SYMLINK_NOFOLLOW, &local_error)) + { + g_debug ("Not deleting \"%s/%s\": ./keep exists", + parent, member); + return; + } + else if (!g_error_matches (local_error, G_IO_ERROR, + G_IO_ERROR_NOT_FOUND)) + { + /* EACCES or something? Give it the benefit of the doubt */ + g_warning ("Not deleting \"%s/%s\": unable to stat ./keep: %s", + parent, member, local_error->message); + return; + } + + g_clear_error (&local_error); + + ref = g_build_filename (member, ".ref", NULL); + temp_lock = pv_bwrap_lock_new (parent_fd, ref, + (PV_BWRAP_LOCK_FLAGS_CREATE | + PV_BWRAP_LOCK_FLAGS_WRITE), + &local_error); + + if (temp_lock == NULL) + { + g_info ("Not deleting \"%s/%s\": unable to get lock: %s", + parent, member, local_error->message); + return; + } + + g_debug ("Deleting \"%s/%s\"...", parent, member); + + /* We have the lock, which would not have happened if someone was + * still using the runtime, so we can safely delete it. */ + if (!glnx_shutil_rm_rf_at (parent_fd, member, NULL, &local_error)) + { + g_debug ("Unable to delete %s/%s: %s", + parent, member, local_error->message); + } +} + +static gboolean +is_old_runtime_deployment (const char *name) +{ + if (g_str_has_prefix (name, "scout_before_")) + return TRUE; + + if (g_str_has_prefix (name, "soldier_before_")) + return TRUE; + + if (g_str_has_prefix (name, "scout_0.")) + return TRUE; + + if (g_str_has_prefix (name, "soldier_0.")) + return TRUE; + + if (g_str_has_prefix (name, ".scout_") + && g_str_has_suffix (name, "_unpack-temp")) + return TRUE; + + if (g_str_has_prefix (name, ".soldier_") + && g_str_has_suffix (name, "_unpack-temp")) + return TRUE; + + return FALSE; +} + +gboolean +pv_runtime_garbage_collect_legacy (const char *variable_dir, + const char *runtime_base, + GError **error) +{ + g_autoptr(GError) local_error = NULL; + g_autoptr(PvBwrapLock) variable_lock = NULL; + g_autoptr(PvBwrapLock) base_lock = NULL; + g_auto(GLnxDirFdIterator) variable_dir_iter = { FALSE }; + g_auto(GLnxDirFdIterator) runtime_base_iter = { FALSE }; + glnx_autofd int variable_dir_fd = -1; + glnx_autofd int runtime_base_fd = -1; + struct + { + const char *path; + GLnxDirFdIterator *iter; + } iters[] = { + { variable_dir, &variable_dir_iter }, + { runtime_base, &runtime_base_iter }, + }; + gsize i; + + g_return_val_if_fail (variable_dir != NULL, FALSE); + g_return_val_if_fail (runtime_base != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + if (!glnx_opendirat (AT_FDCWD, variable_dir, TRUE, + &variable_dir_fd, error)) + return FALSE; + + if (!glnx_opendirat (AT_FDCWD, runtime_base, TRUE, + &runtime_base_fd, error)) + return FALSE; + + variable_lock = pv_bwrap_lock_new (variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WRITE), + &local_error); + + /* If we can't take the lock immediately, just don't do GC */ + if (variable_lock == NULL) + return TRUE; + + /* We take out locks on both the variable directory and the base + * directory, because historically in the shell scripts we only + * locked the base directory, and we later moved to locking only the + * variable directory. Now that we're in C code it seems safest to + * lock both. */ + base_lock = pv_bwrap_lock_new (runtime_base_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WRITE), + &local_error); + + /* Same here */ + if (base_lock == NULL) + return TRUE; + + for (i = 0; i < G_N_ELEMENTS (iters); i++) + { + const char * const symlinks[] = { "scout", "soldier" }; + gsize j; + + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, iters[i].path, + TRUE, iters[i].iter, error)) + return FALSE; + + g_debug ("Cleaning up old subdirectories in %s...", + iters[i].path); + + while (TRUE) + { + struct dirent *dent; + + if (!glnx_dirfd_iterator_next_dent_ensure_dtype (iters[i].iter, + &dent, NULL, error)) + return FALSE; + + if (dent == NULL) + break; + + switch (dent->d_type) + { + case DT_DIR: + break; + + case DT_BLK: + case DT_CHR: + case DT_FIFO: + case DT_LNK: + case DT_REG: + case DT_SOCK: + case DT_UNKNOWN: + default: + g_debug ("Ignoring %s/%s: not a directory", + iters[i].path, dent->d_name); + continue; + } + + if (!is_old_runtime_deployment (dent->d_name)) + continue; + + pv_runtime_maybe_garbage_collect_subdir ("legacy runtime", + iters[i].path, + iters[i].iter->fd, + dent->d_name); + } + + g_debug ("Cleaning up old symlinks in %s...", + iters[i].path); + + for (j = 0; j < G_N_ELEMENTS (symlinks); j++) + pv_delete_dangling_symlink (iters[i].iter->fd, iters[i].path, + symlinks[j]); + } + + return TRUE; +} + static gboolean pv_runtime_garbage_collect (PvRuntime *self, - PvBwrapLock *mutable_parent_lock, + PvBwrapLock *variable_dir_lock, GError **error) { g_auto(GLnxDirFdIterator) iter = { FALSE }; g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); - g_return_val_if_fail (self->mutable_parent != NULL, FALSE); + g_return_val_if_fail (self->variable_dir != NULL, FALSE); g_return_val_if_fail (error == NULL || *error == NULL, FALSE); /* We don't actually *use* this: it just acts as an assertion that * we are holding the lock on the parent directory. */ - g_return_val_if_fail (mutable_parent_lock != NULL, FALSE); + g_return_val_if_fail (variable_dir_lock != NULL, FALSE); - if (!glnx_dirfd_iterator_init_at (AT_FDCWD, self->mutable_parent, + if (!glnx_dirfd_iterator_init_at (AT_FDCWD, self->variable_dir, TRUE, &iter, error)) return FALSE; while (TRUE) { - g_autoptr(GError) local_error = NULL; - g_autoptr(PvBwrapLock) temp_lock = NULL; - g_autofree gchar *keep = NULL; - g_autofree gchar *ref = NULL; struct dirent *dent; - struct stat ignore; if (!glnx_dirfd_iterator_next_dent_ensure_dtype (&iter, &dent, NULL, error)) @@ -576,126 +791,89 @@ pv_runtime_garbage_collect (PvRuntime *self, case DT_UNKNOWN: default: g_debug ("Ignoring %s/%s: not a directory", - self->mutable_parent, dent->d_name); + self->variable_dir, dent->d_name); continue; } - if (!g_str_has_prefix (dent->d_name, "tmp-")) + if (g_str_has_prefix (dent->d_name, "deploy-")) { - g_debug ("Ignoring %s/%s: not tmp-*", - self->mutable_parent, dent->d_name); - continue; - } - - g_debug ("Found temporary runtime %s/%s, considering whether to " - "delete it...", - self->mutable_parent, dent->d_name); - - keep = g_build_filename (dent->d_name, "keep", NULL); + /* Don't GC old deployments unless we know which one is current + * and therefore should not be deleted */ + if (self->id == NULL) + { + g_debug ("Ignoring %s/deploy-*: current ID not known", + self->variable_dir); + continue; + } - if (glnx_fstatat (self->mutable_parent_fd, keep, &ignore, - AT_SYMLINK_NOFOLLOW, &local_error)) - { - g_debug ("Not deleting \"%s/%s\": ./keep exists", - self->mutable_parent, dent->d_name); - continue; + /* Don't GC the current deployment */ + if (strcmp (dent->d_name + strlen ("deploy-"), self->id) == 0) + { + g_debug ("Ignoring %s/%s: is the current version", + self->variable_dir, dent->d_name); + continue; + } } - else if (!g_error_matches (local_error, G_IO_ERROR, - G_IO_ERROR_NOT_FOUND)) + else if (!g_str_has_prefix (dent->d_name, "tmp-")) { - /* EACCES or something? Give it the benefit of the doubt */ - g_warning ("Not deleting \"%s/%s\": unable to stat ./keep: %s", - self->mutable_parent, dent->d_name, local_error->message); - g_clear_error (&local_error); + g_debug ("Ignoring %s/%s: not tmp-*", + self->variable_dir, dent->d_name); continue; } - g_clear_error (&local_error); + pv_runtime_maybe_garbage_collect_subdir ("temporary runtime", + self->variable_dir, + self->variable_dir_fd, + dent->d_name); + } - ref = g_build_filename (dent->d_name, ".ref", NULL); - temp_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ref, - (PV_BWRAP_LOCK_FLAGS_CREATE | - PV_BWRAP_LOCK_FLAGS_WRITE), - &local_error); + return TRUE; +} - if (temp_lock == NULL) - { - g_info ("Not deleting \"%s/%s\": unable to get lock: %s", - self->mutable_parent, dent->d_name, - local_error->message); - g_clear_error (&local_error); - continue; - } +static gboolean +pv_runtime_init_variable_dir (PvRuntime *self, + GError **error) +{ + /* Nothing to do in this case */ + if (self->variable_dir == NULL) + return TRUE; - g_debug ("Deleting \"%s/%s\"...", self->mutable_parent, dent->d_name); + if (g_mkdir_with_parents (self->variable_dir, 0700) != 0) + return glnx_throw_errno_prefix (error, "Unable to create %s", + self->variable_dir); - /* We have the lock, which would not have happened if someone was - * still using the runtime, so we can safely delete it. */ - if (!glnx_shutil_rm_rf_at (self->mutable_parent_fd, dent->d_name, - NULL, &local_error)) - { - g_debug ("Unable to delete %s/%s: %s", - self->mutable_parent, dent->d_name, local_error->message); - g_clear_error (&local_error); - continue; - } - } + if (!glnx_opendirat (AT_FDCWD, self->variable_dir, TRUE, + &self->variable_dir_fd, error)) + return FALSE; return TRUE; } static gboolean -pv_runtime_init_mutable (PvRuntime *self, - GError **error) +pv_runtime_create_copy (PvRuntime *self, + PvBwrapLock *variable_dir_lock, + GError **error) { g_autofree gchar *dest_usr = NULL; g_autofree gchar *source_usr_subdir = NULL; g_autofree gchar *temp_dir = NULL; g_autoptr(GDir) dir = NULL; g_autoptr(PvBwrapLock) copy_lock = NULL; - g_autoptr(PvBwrapLock) mutable_lock = NULL; g_autoptr(PvBwrapLock) source_lock = NULL; const char *member; const char *source_usr; glnx_autofd int temp_dir_fd = -1; gboolean is_just_usr; - /* Nothing to do in this case */ - if (self->mutable_parent == NULL) - return TRUE; - - if (g_mkdir_with_parents (self->mutable_parent, 0700) != 0) - return glnx_throw_errno_prefix (error, "Unable to create %s", - self->mutable_parent); - - if (!glnx_opendirat (AT_FDCWD, self->mutable_parent, TRUE, - &self->mutable_parent_fd, error)) - return FALSE; - - /* Lock the parent directory. Anything that directly manipulates the - * temporary runtimes is expected to do the same, so that - * it cannot be deleting temporary runtimes at the same time we're - * creating them. - * - * This is a read-mode lock: it's OK to create more than one temporary - * runtime in parallel, as long as nothing is deleting them - * concurrently. */ - mutable_lock = pv_bwrap_lock_new (self->mutable_parent_fd, ".ref", - PV_BWRAP_LOCK_FLAGS_CREATE, - error); - - if (mutable_lock == NULL) - return glnx_prefix_error (error, "Unable to lock \"%s/%s\"", - self->mutable_parent, ".ref"); - - /* GC old runtimes (if they have become unused) before we create a - * new one. This means we should only ever have one temporary runtime - * copy per game that is run concurrently. */ - if ((self->flags & PV_RUNTIME_FLAGS_GC_RUNTIMES) != 0 && - !pv_runtime_garbage_collect (self, mutable_lock, error)) - return FALSE; + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (self->variable_dir != NULL, FALSE); + g_return_val_if_fail (self->flags & PV_RUNTIME_FLAGS_COPY_RUNTIME, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + /* We don't actually *use* this: it just acts as an assertion that + * we are holding the lock on the parent directory. */ + g_return_val_if_fail (variable_dir_lock != NULL, FALSE); - temp_dir = g_build_filename (self->mutable_parent, "tmp-XXXXXX", NULL); + temp_dir = g_build_filename (self->variable_dir, "tmp-XXXXXX", NULL); if (g_mkdtemp (temp_dir) == NULL) return glnx_throw_errno_prefix (error, @@ -828,14 +1006,210 @@ pv_runtime_init_mutable (PvRuntime *self, return TRUE; } +static gboolean +gstring_replace_suffix (GString *s, + const char *suffix, + const char *replacement) +{ + gsize len = strlen (suffix); + + if (s->len >= len + && strcmp (&s->str[s->len - len], suffix) == 0) + { + g_string_truncate (s, s->len - len); + g_string_append (s, replacement); + return TRUE; + } + + return FALSE; +} + +/* + * mutable_lock: (out) (not optional): + */ +static gboolean +pv_runtime_unpack (PvRuntime *self, + PvBwrapLock **mutable_lock, + GError **error) +{ + g_autoptr(GString) debug_tarball = NULL; + g_autofree gchar *deploy_basename = NULL; + g_autofree gchar *unpack_dir = NULL; + + g_return_val_if_fail (PV_IS_RUNTIME (self), FALSE); + g_return_val_if_fail (mutable_lock != NULL, FALSE); + g_return_val_if_fail (*mutable_lock == NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (self->source != NULL, FALSE); + g_return_val_if_fail (self->variable_dir != NULL, FALSE); + g_return_val_if_fail (self->variable_dir_fd >= 0, FALSE); + g_return_val_if_fail (self->deployment == NULL, FALSE); + + if (!g_file_test (self->source, G_FILE_TEST_IS_REGULAR)) + return glnx_throw (error, "\"%s\" is not a regular file", self->source); + + if (!g_str_has_suffix (self->source, ".tar.gz")) + return glnx_throw (error, "\"%s\" is not a .tar.gz file", self->source); + + if (self->id == NULL) + { + g_autoptr(GString) build_id_file = g_string_new (self->source); + g_autofree char *id = NULL; + gsize len; + gsize i; + + if (gstring_replace_suffix (build_id_file, "-runtime.tar.gz", + "-buildid.txt") + || gstring_replace_suffix (build_id_file, "-sysroot.tar.gz", + "-buildid.txt")) + { + if (!g_file_get_contents (build_id_file->str, &id, &len, error)) + { + g_prefix_error (error, "Unable to determine build ID from \"%s\": ", + build_id_file->str); + return FALSE; + } + + if (len == 0) + return glnx_throw (error, "Build ID in \"%s\" is empty", + build_id_file->str); + + for (i = 0; i < len; i++) + { + /* Ignore a trailing newline */ + if (i + 1 == len && id[i] == '\n') + { + id[i] = '\0'; + break; + } + + /* Allow dot, dash or underscore, but not at the beginning */ + if (i > 0 && strchr (".-_", id[i]) != NULL) + continue; + + if (!g_ascii_isalnum (id[i])) + return glnx_throw (error, "Build ID in \"%s\" is invalid", + build_id_file->str); + } + + self->id = g_steal_pointer (&id); + } + } + + if (self->id == NULL) + return glnx_throw (error, "Cannot unpack archive without unique ID"); + + deploy_basename = g_strdup_printf ("deploy-%s", self->id); + self->deployment = g_build_filename (self->variable_dir, + deploy_basename, NULL); + + /* Fast path: if we already unpacked it, nothing more to do! */ + if (g_file_test (self->deployment, G_FILE_TEST_IS_DIR)) + return TRUE; + + /* Lock the parent directory. Anything that directly manipulates the + * unpacked runtimes is expected to do the same, so that + * it cannot be deleting unpacked runtimes at the same time we're + * creating them. + * + * This is an exclusive lock, to avoid two concurrent processes trying + * to unpack the same runtime. */ + *mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WAIT), + error); + + if (*mutable_lock == NULL) + return FALSE; + + /* Slow path: we need to do this the hard way. */ + unpack_dir = g_build_filename (self->variable_dir, "tmp-XXXXXX", NULL); + + if (g_mkdtemp (unpack_dir) == NULL) + return glnx_throw_errno_prefix (error, + "Cannot create temporary directory \"%s\"", + unpack_dir); + + g_info ("Unpacking \"%s\" into \"%s\"...", self->source, unpack_dir); + + { + g_autoptr(FlatpakBwrap) tar = flatpak_bwrap_new (NULL); + + flatpak_bwrap_add_args (tar, + "tar", + "--force-local", + "-C", unpack_dir, + NULL); + + if (self->flags & PV_RUNTIME_FLAGS_VERBOSE) + flatpak_bwrap_add_arg (tar, "-v"); + + flatpak_bwrap_add_args (tar, + "-xf", self->source, + NULL); + flatpak_bwrap_finish (tar); + + if (!pv_bwrap_run_sync (tar, NULL, error)) + { + glnx_shutil_rm_rf_at (-1, unpack_dir, NULL, NULL); + return FALSE; + } + } + + debug_tarball = g_string_new (self->source); + + if (gstring_replace_suffix (debug_tarball, "-runtime.tar.gz", + "-debug.tar.gz") + && g_file_test (debug_tarball->str, G_FILE_TEST_EXISTS)) + { + g_autoptr(FlatpakBwrap) tar = flatpak_bwrap_new (NULL); + g_autoptr(GError) local_error = NULL; + g_autofree char *files_lib_debug = NULL; + + files_lib_debug = g_build_filename (unpack_dir, "files", "lib", + "debug", NULL); + + flatpak_bwrap_add_args (tar, + "tar", + "--force-local", + "-C", files_lib_debug, + NULL); + + if (self->flags & PV_RUNTIME_FLAGS_VERBOSE) + flatpak_bwrap_add_arg (tar, "-v"); + + flatpak_bwrap_add_args (tar, + "-xf", debug_tarball->str, + "files/", + NULL); + flatpak_bwrap_finish (tar); + + if (!pv_bwrap_run_sync (tar, NULL, &local_error)) + g_debug ("Ignoring error unpacking detached debug symbols: %s", + local_error->message); + } + + g_info ("Renaming \"%s\" to \"%s\"...", unpack_dir, deploy_basename); + + if (!glnx_renameat (self->variable_dir_fd, unpack_dir, + self->variable_dir_fd, deploy_basename, + error)) + { + glnx_shutil_rm_rf_at (-1, unpack_dir, NULL, NULL); + return FALSE; + } + + return TRUE; +} + static gboolean pv_runtime_initable_init (GInitable *initable, GCancellable *cancellable G_GNUC_UNUSED, GError **error) { PvRuntime *self = PV_RUNTIME (initable); + g_autoptr(PvBwrapLock) mutable_lock = NULL; g_autofree gchar *contents = NULL; - g_autofree gchar *files_ref = NULL; g_autofree gchar *os_release = NULL; gsize len; @@ -850,11 +1224,24 @@ pv_runtime_initable_init (GInitable *initable, self->bubblewrap); } - if (self->mutable_parent != NULL - && !g_file_test (self->mutable_parent, G_FILE_TEST_IS_DIR)) + if (!pv_runtime_init_variable_dir (self, error)) + return FALSE; + + if (self->flags & PV_RUNTIME_FLAGS_UNPACK_ARCHIVE) { - return glnx_throw (error, "\"%s\" is not a directory", - self->mutable_parent); + if (self->variable_dir_fd < 0) + return glnx_throw (error, + "Cannot unpack archive without variable directory"); + + if (!pv_runtime_unpack (self, &mutable_lock, error)) + return FALSE; + + /* Set by pv_runtime_unpack */ + g_assert (self->deployment != NULL); + } + else + { + self->deployment = g_strdup (self->source); } if (!g_file_test (self->deployment, G_FILE_TEST_IS_DIR)) @@ -893,17 +1280,66 @@ pv_runtime_initable_init (GInitable *initable, * continue to be locked until all processes in the container exit. * If we make a temporary mutable copy, we only hold this lock until * setup has finished. */ - files_ref = g_build_filename (self->source_files, ".ref", NULL); - self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref, - PV_BWRAP_LOCK_FLAGS_CREATE, - error); + if (self->runtime_lock == NULL) + { + g_autofree gchar *files_ref = NULL; + + files_ref = g_build_filename (self->source_files, ".ref", NULL); + self->runtime_lock = pv_bwrap_lock_new (AT_FDCWD, files_ref, + PV_BWRAP_LOCK_FLAGS_CREATE, + error); + } /* If the runtime is being deleted, ... don't use it, I suppose? */ if (self->runtime_lock == NULL) return FALSE; - if (!pv_runtime_init_mutable (self, error)) - return FALSE; + /* GC old runtimes (if they have become unused) before we create a + * new one. This means we should only ever have one temporary runtime + * copy per game that is run concurrently. */ + if (self->variable_dir_fd >= 0 + && (self->flags & PV_RUNTIME_FLAGS_GC_RUNTIMES)) + { + g_autoptr(GError) local_error = NULL; + + /* Take out an exclusive lock for GC so that we will not conflict + * with other concurrent processes that are halfway through + * deploying or unpacking a runtime. */ + if (mutable_lock == NULL) + mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WRITE), + &local_error); + + if (mutable_lock == NULL) + g_debug ("Unable to take an exclusive lock, skipping GC: %s", + local_error->message); + else if (!pv_runtime_garbage_collect (self, mutable_lock, error)) + return FALSE; + } + + if (self->flags & PV_RUNTIME_FLAGS_COPY_RUNTIME) + { + if (self->variable_dir_fd < 0) + return glnx_throw (error, + "Cannot copy runtime without variable directory"); + + /* This time take out a non-exclusive lock: any number of processes + * can safely be creating their own temporary copy at the same + * time. If another process is doing GC, wait for it to finish, + * then take our lock. */ + if (mutable_lock == NULL) + mutable_lock = pv_bwrap_lock_new (self->variable_dir_fd, ".ref", + (PV_BWRAP_LOCK_FLAGS_CREATE + | PV_BWRAP_LOCK_FLAGS_WAIT), + error); + + if (mutable_lock == NULL) + return FALSE; + + if (!pv_runtime_create_copy (self, mutable_lock, error)) + return FALSE; + } if (self->mutable_sysroot != NULL) { @@ -1043,8 +1479,8 @@ pv_runtime_finalize (GObject *object) g_free (self->bubblewrap); g_strfreev (self->original_environ); g_free (self->libcapsule_knowledge); - glnx_close_fd (&self->mutable_parent_fd); - g_free (self->mutable_parent); + glnx_close_fd (&self->variable_dir_fd); + g_free (self->variable_dir); glnx_close_fd (&self->mutable_sysroot_fd); g_free (self->mutable_sysroot); glnx_close_fd (&self->provider_fd); @@ -1053,6 +1489,7 @@ pv_runtime_finalize (GObject *object) g_free (self->provider_in_container_namespace); g_free (self->runtime_files_on_host); g_free (self->runtime_usr); + g_free (self->source); g_free (self->source_files); g_free (self->deployment); g_free (self->tools_dir); @@ -1094,10 +1531,16 @@ pv_runtime_class_init (PvRuntimeClass *cls) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); - properties[PROP_MUTABLE_PARENT] = - g_param_spec_string ("mutable-parent", "Mutable parent", - ("Path to a directory in which to create a " - "mutable copy of source-files, or NULL"), + properties[PROP_ID] = + g_param_spec_string ("id", "ID", + "Unique identifier of runtime to be unpacked", + NULL, + (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + properties[PROP_VARIABLE_DIR] = + g_param_spec_string ("variable-dir", "Variable directory", + ("Path to directory for temporary files, or NULL"), NULL, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); @@ -1120,10 +1563,10 @@ pv_runtime_class_init (PvRuntimeClass *cls) (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); - properties[PROP_DEPLOYMENT] = - g_param_spec_string ("deployment", "Deployment", + properties[PROP_SOURCE] = + g_param_spec_string ("source", "Source", ("Path to read-only runtime files (merged-/usr " - "or sysroot) in current namespace"), + "or sysroot) or archive, in current namespace"), NULL, (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); @@ -1139,8 +1582,9 @@ pv_runtime_class_init (PvRuntimeClass *cls) } PvRuntime * -pv_runtime_new (const char *deployment, - const char *mutable_parent, +pv_runtime_new (const char *source, + const char *id, + const char *variable_dir, const char *bubblewrap, const char *tools_dir, const char *provider_in_current_namespace, @@ -1149,7 +1593,7 @@ pv_runtime_new (const char *deployment, PvRuntimeFlags flags, GError **error) { - g_return_val_if_fail (deployment != NULL, NULL); + g_return_val_if_fail (source != NULL, NULL); g_return_val_if_fail (bubblewrap != NULL, NULL); g_return_val_if_fail (tools_dir != NULL, NULL); g_return_val_if_fail ((flags & ~(PV_RUNTIME_FLAGS_MASK)) == 0, NULL); @@ -1159,8 +1603,9 @@ pv_runtime_new (const char *deployment, error, "bubblewrap", bubblewrap, "original-environ", original_environ, - "mutable-parent", mutable_parent, - "deployment", deployment, + "variable-dir", variable_dir, + "source", source, + "id", id, "tools-directory", tools_dir, "provider-in-current-namespace", provider_in_current_namespace, diff --git a/pressure-vessel/runtime.h b/pressure-vessel/runtime.h index b0223bad6fd7652bca605e164d9acbf9afc33252..3669a67cbd906041cb62e5c97882896bab97b2b6 100644 --- a/pressure-vessel/runtime.h +++ b/pressure-vessel/runtime.h @@ -38,6 +38,8 @@ * @PV_RUNTIME_FLAGS_GC_RUNTIMES: Garbage-collect old temporary runtimes * @PV_RUNTIME_FLAGS_VERBOSE: Be more verbose * @PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS: Include host Vulkan layers + * @PV_RUNTIME_FLAGS_COPY_RUNTIME: Copy the runtime and modify the copy + * @PV_RUNTIME_FLAGS_UNPACK_ARCHIVE: Source is an archive, not a deployment * @PV_RUNTIME_FLAGS_NONE: None of the above * * Flags affecting how we set up the runtime. @@ -49,6 +51,8 @@ typedef enum PV_RUNTIME_FLAGS_GC_RUNTIMES = (1 << 2), PV_RUNTIME_FLAGS_VERBOSE = (1 << 3), PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS = (1 << 4), + PV_RUNTIME_FLAGS_COPY_RUNTIME = (1 << 5), + PV_RUNTIME_FLAGS_UNPACK_ARCHIVE = (1 << 6), PV_RUNTIME_FLAGS_NONE = 0 } PvRuntimeFlags; @@ -58,6 +62,8 @@ typedef enum | PV_RUNTIME_FLAGS_GC_RUNTIMES \ | PV_RUNTIME_FLAGS_VERBOSE \ | PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS \ + | PV_RUNTIME_FLAGS_COPY_RUNTIME \ + | PV_RUNTIME_FLAGS_UNPACK_ARCHIVE \ ) typedef struct _PvRuntime PvRuntime; @@ -71,8 +77,9 @@ typedef struct _PvRuntimeClass PvRuntimeClass; #define PV_RUNTIME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), PV_TYPE_RUNTIME, PvRuntimeClass) GType pv_runtime_get_type (void); -PvRuntime *pv_runtime_new (const char *deployment, - const char *mutable_parent, +PvRuntime *pv_runtime_new (const char *source, + const char *id, + const char *variable_dir, const char *bubblewrap, const char *tools_dir, const char *provider_in_current_namespace, @@ -90,4 +97,8 @@ gboolean pv_runtime_bind (PvRuntime *self, GError **error); void pv_runtime_cleanup (PvRuntime *self); +gboolean pv_runtime_garbage_collect_legacy (const char *variable_dir, + const char *runtime_base, + GError **error); + G_DEFINE_AUTOPTR_CLEANUP_FUNC (PvRuntime, g_object_unref) diff --git a/pressure-vessel/utils.c b/pressure-vessel/utils.c index 5cb4950d666a27aa21d0f30a3ae0c9229a415895..af84f80484f2e2b5aa94bb2dda78a773d50ddf95 100644 --- a/pressure-vessel/utils.c +++ b/pressure-vessel/utils.c @@ -977,3 +977,72 @@ pv_set_up_logging (gboolean opt_verbose) opt_timestamp ? pv_log_to_stderr_with_timestamp : pv_log_to_stderr, NULL); } + +/** + * pv_delete_dangling_symlink: + * @dirfd: An open file descriptor for a directory + * @debug_path: Path to directory represented by @dirfd, used in debug messages + * @name: A filename in @dirfd that is thought to be a symbolic link + * + * If @name exists in @dirfd and is a symbolic link whose target does not + * exist, delete it. + */ +void +pv_delete_dangling_symlink (int dirfd, + const char *debug_path, + const char *name) +{ + struct stat stat_buf, lstat_buf; + + g_return_if_fail (dirfd >= 0); + g_return_if_fail (name != NULL); + g_return_if_fail (strcmp (name, "") != 0); + g_return_if_fail (strcmp (name, ".") != 0); + g_return_if_fail (strcmp (name, "..") != 0); + + if (fstatat (dirfd, name, &lstat_buf, AT_SYMLINK_NOFOLLOW) == 0) + { + if (!S_ISLNK (lstat_buf.st_mode)) + { + g_debug ("Ignoring %s/%s: not a symlink", + debug_path, name); + } + else if (fstatat (dirfd, name, &stat_buf, 0) == 0) + { + g_debug ("Ignoring %s/%s: symlink target still exists", + debug_path, name); + } + else if (errno != ENOENT) + { + int saved_errno = errno; + + g_debug ("Ignoring %s/%s: fstatat(!NOFOLLOW): %s", + debug_path, name, g_strerror (saved_errno)); + } + else + { + g_debug ("Target of %s/%s no longer exists, deleting it", + debug_path, name); + + if (unlinkat (dirfd, name, 0) != 0) + { + int saved_errno = errno; + + g_debug ("Could not delete %s/%s: unlinkat: %s", + debug_path, name, g_strerror (saved_errno)); + } + } + } + else if (errno == ENOENT) + { + /* Silently ignore: symlink doesn't exist so we don't need to + * delete it */ + } + else + { + int saved_errno = errno; + + g_debug ("Ignoring %s/%s: fstatat(NOFOLLOW): %s", + debug_path, name, g_strerror (saved_errno)); + } +} diff --git a/pressure-vessel/utils.h b/pressure-vessel/utils.h index 5d647afa0e79652e3ac90594aacc132a561e6daa..5920b7b6ee4a30b1e2163f1e753fa0cf14795e23 100644 --- a/pressure-vessel/utils.h +++ b/pressure-vessel/utils.h @@ -76,3 +76,7 @@ const char *pv_get_path_after (const char *str, const char *prefix); void pv_set_up_logging (gboolean opt_verbose); + +void pv_delete_dangling_symlink (int dirfd, + const char *debug_path, + const char *name); diff --git a/pressure-vessel/wrap.1.md b/pressure-vessel/wrap.1.md index 25c5b00a5950cc796c80f5dc1b9725eab4112dfc..e682891fdbe0243696c8aaca34916f07318bc0a4 100644 --- a/pressure-vessel/wrap.1.md +++ b/pressure-vessel/wrap.1.md @@ -28,10 +28,18 @@ pressure-vessel-wrap - run programs in a bubblewrap container : Disable all interactivity and redirection: ignore `--shell`, all `--shell-` options, `--terminal`, `--tty` and `--xterm`. +`--copy-runtime`, `--no-copy-runtime` +: If a `--runtime` is active, copy it into a subdirectory of the + `--variable-dir`, edit the copy in-place, and mount the copy read-only + in the container, instead of setting up elaborate bind-mount structures. + This option requires the `--variable-dir` option to be used. + + `--no-copy-runtime` disables this behaviour and is currently + the default. + `--copy-runtime-into` *DIR* -: If a `--runtime` is active, copy it into a subdirectory of *DIR*, - edit the copy in-place, and mount the copy read-only in the container, - instead of setting up elaborate bind-mount structures. +: If *DIR* is an empty string, equivalent to `--no-copy-runtime`. + Otherwise, equivalent to `--copy-runtime --variable-dir=DIR`. `--env-if-host` *VAR=VAL* : If *COMMAND* is run with `/usr` from the host system, set @@ -46,7 +54,7 @@ pressure-vessel-wrap - run programs in a bubblewrap container freedesktop.org app *ID* would use. `--gc-runtimes`, `--no-gc-runtimes` -: If using `--copy-runtime-into`, garbage-collect old temporary +: If using `--variable-dir`, garbage-collect old temporary runtimes that are left over from a previous **pressure-vessel-wrap**. This is the default. `--no-gc-runtimes` disables this behaviour. @@ -81,8 +89,8 @@ pressure-vessel-wrap - run programs in a bubblewrap container `--only-prepare` : Prepare the runtime, but do not actually run *COMMAND*. - With `--copy-runtime-into`, the prepared runtime will appear in - a subdirectory of *DIR*. + With `--copy-runtime`, the prepared runtime will appear in + a subdirectory of the `--variable-dir`. `--pass-fd` *FD* : Pass the file descriptor *FD* (specified as a small positive integer) @@ -107,9 +115,38 @@ pressure-vessel-wrap - run programs in a bubblewrap container (containing bin/sh, bin/env and many other OS files). For example, a Flatpak runtime is a suitable value for *PATH*. +`--runtime-archive` *ARCHIVE* +: Unpack *ARCHIVE* and use it to provide /usr in the container, similar + to `--runtime`. The `--runtime-id` option is also required, unless + the filename of the *ARCHIVE* ends with a supported suffix + (`-runtime.tar.gz` or `-sysroot.tar.gz`) and it is accompanied by a + `-buildid.txt` file. + + If this option is used, then `--variable-dir` + (or its environment variable equivalent) is also required. + This option and `--runtime` cannot both be used. + + The archive will be unpacked into a subdirectory of the `--variable-dir`. + Any other subdirectories of the `--variable-dir` that appear to be + different runtimes will be deleted, unless they contain a file + at the top level named `keep` or are currently in use. + + The archive must currently be a gzipped tar file whose name ends + with `.tar.gz`. Other formats might be allowed in future. + `--runtime-base` *PATH* -: If `--runtime` specifies a relative path, look for it relative - to *PATH*. +: If `--runtime` or `--runtime-archive` is specified as a relative path, + look for it relative to *PATH*. + +`--runtime-id` *ID* +: Use *ID* to construct a directory into which the `--runtime-archive` + will be unpacked, overriding an accompanying `-buildid.txt` file + if present. + + If the *ID* is the same as in a previous run of pressure-vessel-wrap, + the content of the `--runtime-archive` will be assumed to be the + same as in that previous run, resulting in the previous runtime + being reused. `--share-home`, `--unshare-home` : If `--unshare-home` is specified, use the home directory given @@ -168,6 +205,10 @@ pressure-vessel-wrap - run programs in a bubblewrap container : Perform a smoke-test to determine whether **pressure-vessel-wrap** can work, and exit. Exit with status 0 if it can or 1 if it cannot. +`--variable-dir` *PATH* +: Use *PATH* as a cache directory for files that are temporarily + unpacked or copied. It will be created automatically if necessary. + `--verbose` : Be more verbose. @@ -211,9 +252,14 @@ The following environment variables (among others) are read by : If set to `1`, equivalent to `--batch`. If set to `0`, no effect. +`PRESSURE_VESSEL_COPY_RUNTIME` (boolean) +: If set to `1`, equivalent to `--copy-runtime`. + If set to `0`, equivalent to `--no-copy-runtime`. + `PRESSURE_VESSEL_COPY_RUNTIME_INTO` (path or empty string) -: Equivalent to - `--copy-runtime-into="$PRESSURE_VESSEL_COPY_RUNTIME_INTO"`. +: If the string is empty, equivalent to `--no-copy-runtime`. + Otherwise, equivalent to + `--copy-runtime --variable-dir="$PRESSURE_VESSEL_COPY_RUNTIME_INTO"`. `PRESSURE_VESSEL_FILESYSTEMS_RO` (`:`-separated list of paths) : Make these paths available read-only inside the container if they @@ -281,6 +327,9 @@ The following environment variables (among others) are read by `PRESSURE_VESSEL_TERMINAL` (`none`, `auto`, `tty` or `xterm`) : Equivalent to `--terminal="$PRESSURE_VESSEL_TERMINAL"`. +`PRESSURE_VESSEL_VARIABLE_DIR` (path) +: Equivalent to `--variable-dir="$PRESSURE_VESSEL_VARIABLE_DIR"`. + `PRESSURE_VESSEL_VERBOSE` (boolean) : If set to `1`, equivalent to `--verbose`. diff --git a/pressure-vessel/wrap.c b/pressure-vessel/wrap.c index 964b1b3ceb010a2305ecfd103771fa83e551717a..fbe9436dc4600042d1c62f6016704cfef84a769f 100644 --- a/pressure-vessel/wrap.c +++ b/pressure-vessel/wrap.c @@ -706,12 +706,13 @@ typedef enum } Tristate; static gboolean opt_batch = FALSE; -static char *opt_copy_runtime_into = NULL; +static gboolean opt_copy_runtime = FALSE; static char **opt_env_if_host = NULL; static char *opt_fake_home = NULL; static char **opt_filesystems = NULL; static char *opt_freedesktop_app_id = NULL; static char *opt_steam_app_id = NULL; +static gboolean opt_gc_legacy_runtimes = FALSE; static gboolean opt_gc_runtimes = TRUE; static gboolean opt_generate_locales = TRUE; static char *opt_home = NULL; @@ -724,12 +725,15 @@ static gboolean opt_import_vulkan_layers = TRUE; static PvShell opt_shell = PV_SHELL_NONE; static GPtrArray *opt_ld_preload = NULL; static GArray *opt_pass_fds = NULL; -static char *opt_runtime_base = NULL; static char *opt_runtime = NULL; +static char *opt_runtime_archive = NULL; +static char *opt_runtime_base = NULL; +static char *opt_runtime_id = NULL; static Tristate opt_share_home = TRISTATE_MAYBE; static gboolean opt_share_pid = TRUE; static double opt_terminate_idle_timeout = 0.0; static double opt_terminate_timeout = -1.0; +static char *opt_variable_dir = NULL; static gboolean opt_verbose = FALSE; static gboolean opt_version = FALSE; static gboolean opt_version_only = FALSE; @@ -753,6 +757,35 @@ opt_host_ld_preload_cb (const gchar *option_name, return TRUE; } +static gboolean +opt_copy_runtime_into_cb (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + if (value == NULL) + { + opt_copy_runtime = FALSE; + } + else if (value[0] == '\0') + { + g_warning ("%s is deprecated, disable with --no-copy-runtime instead", + option_name); + opt_copy_runtime = FALSE; + } + else + { + g_warning ("%s is deprecated, use --copy-runtime and " + "--variable-dir instead", + option_name); + opt_copy_runtime = TRUE; + g_free (opt_variable_dir); + opt_variable_dir = g_strdup (value); + } + + return TRUE; +} + static gboolean opt_pass_fd_cb (const char *name, const char *value, @@ -966,11 +999,20 @@ static GOptionEntry options[] = G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_batch, "Disable all interactivity and redirection: ignore --shell*, " "--terminal, --xterm, --tty. [Default: if $PRESSURE_VESSEL_BATCH]", NULL }, + { "copy-runtime", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_copy_runtime, + "If a --runtime is used, copy it into --variable-dir and edit the " + "copy in-place.", + NULL }, + { "no-copy-runtime", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_copy_runtime, + "Don't behave as described for --copy-runtime. " + "[Default unless $PRESSURE_VESSEL_COPY_RUNTIME is 1 or running in Flatpak]", + NULL }, { "copy-runtime-into", '\0', - G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_copy_runtime_into, - "If a --runtime is used, copy it into DIR and edit the copy in-place. " - "[Default: $PRESSURE_VESSEL_COPY_RUNTIME_INTO or empty]", - "DIR" }, + G_OPTION_FLAG_FILENAME|G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, + &opt_copy_runtime_into_cb, + "Deprecated alias for --copy-runtime and --variable-dir", "DIR" }, { "env-if-host", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME_ARRAY, &opt_env_if_host, "Set VAR=VAL if COMMAND is run with /usr from the host system, " @@ -991,14 +1033,23 @@ static GOptionEntry options[] = "Make --unshare-home use ~/.var/app/com.steampowered.AppN " "as home directory. [Default: $STEAM_COMPAT_APP_ID or $SteamAppId]", "N" }, + { "gc-legacy-runtimes", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_legacy_runtimes, + "Garbage-collect old unpacked runtimes in $PRESSURE_VESSEL_RUNTIME_BASE.", + NULL }, + { "no-gc-legacy-runtimes", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_gc_legacy_runtimes, + "Don't garbage-collect old unpacked runtimes in " + "$PRESSURE_VESSEL_RUNTIME_BASE [default].", + NULL }, { "gc-runtimes", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_gc_runtimes, - "If using --copy-runtime-into, garbage-collect old temporary " + "If using --variable-dir, garbage-collect old temporary " "runtimes. [Default, unless $PRESSURE_VESSEL_GC_RUNTIMES is 0]", NULL }, { "no-gc-runtimes", '\0', G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_gc_runtimes, - "If using --copy-runtime-into, don't garbage-collect old " + "If using --variable-dir, don't garbage-collect old " "temporary runtimes.", NULL }, { "generate-locales", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_generate_locales, @@ -1066,11 +1117,22 @@ static GOptionEntry options[] = "it with the provider's graphics stack. The empty string " "means don't use a runtime. [Default: $PRESSURE_VESSEL_RUNTIME or '']", "RUNTIME" }, + { "runtime-archive", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_archive, + "Unpack the ARCHIVE and use it as the runtime, using --runtime-id to " + "avoid repeatedly unpacking the same archive. " + "[Default: $PRESSURE_VESSEL_RUNTIME_ARCHIVE]", + "ARCHIVE" }, { "runtime-base", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_base, - "If a --runtime is a relative path, look for it relative to BASE. " + "If a --runtime or --runtime-archive is a relative path, look for " + "it relative to BASE. " "[Default: $PRESSURE_VESSEL_RUNTIME_BASE or '.']", "BASE" }, + { "runtime-id", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_runtime_id, + "Reuse a previously-unpacked --runtime-archive if its ID matched this", + "ID" }, { "share-home", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_share_home_cb, "Use the real home directory. " @@ -1141,6 +1203,10 @@ static GOptionEntry options[] = "skip SIGTERM and use SIGKILL immediately. Implies --subreaper. " "[Default: -1.0, meaning don't signal].", "SECONDS" }, + { "variable-dir", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_FILENAME, &opt_variable_dir, + "If a runtime needs to be unpacked or copied, put it in DIR.", + "DIR" }, { "verbose", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, "Be more verbose.", NULL }, @@ -1246,8 +1312,29 @@ main (int argc, original_environ = g_get_environ (); + if (is_flatpak_env) + opt_copy_runtime = TRUE; + /* Set defaults */ opt_batch = pv_boolean_environment ("PRESSURE_VESSEL_BATCH", FALSE); + /* Process COPY_RUNTIME_INFO first so that COPY_RUNTIME and VARIABLE_DIR + * can override it */ + opt_copy_runtime_into_cb ("$PRESSURE_VESSEL_COPY_RUNTIME_INTO", + g_getenv ("PRESSURE_VESSEL_COPY_RUNTIME_INTO"), + NULL, NULL); + opt_copy_runtime = pv_boolean_environment ("PRESSURE_VESSEL_COPY_RUNTIME", + opt_copy_runtime); + opt_runtime_id = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_ID")); + + { + const char *value = g_getenv ("PRESSURE_VESSEL_VARIABLE_DIR"); + + if (value != NULL) + { + g_free (opt_variable_dir); + opt_variable_dir = g_strdup (value); + } + } opt_freedesktop_app_id = g_strdup (g_getenv ("PRESSURE_VESSEL_FDO_APP_ID")); @@ -1265,6 +1352,7 @@ main (int argc, TRUE); opt_share_home = tristate_environment ("PRESSURE_VESSEL_SHARE_HOME"); + opt_gc_legacy_runtimes = pv_boolean_environment ("PRESSURE_VESSEL_GC_LEGACY_RUNTIMES", FALSE); opt_gc_runtimes = pv_boolean_environment ("PRESSURE_VESSEL_GC_RUNTIMES", TRUE); opt_generate_locales = pv_boolean_environment ("PRESSURE_VESSEL_GENERATE_LOCALES", TRUE); @@ -1290,29 +1378,52 @@ main (int argc, if (opt_verbose) pv_set_up_logging (opt_verbose); - if (opt_runtime == NULL) - opt_runtime = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME")); + /* Specifying either one of these mutually-exclusive options as a + * command-line option disables use of the environment variable for + * the other one */ + if (opt_runtime == NULL && opt_runtime_archive == NULL) + { + opt_runtime = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME")); - if (opt_runtime_base == NULL) - opt_runtime_base = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_BASE")); + /* Normalize empty string to NULL to simplify later code */ + if (opt_runtime != NULL && opt_runtime[0] == '\0') + g_clear_pointer (&opt_runtime, g_free); - if (opt_runtime != NULL - && opt_runtime[0] != '\0' - && !g_path_is_absolute (opt_runtime) - && opt_runtime_base != NULL - && opt_runtime_base[0] != '\0') + opt_runtime_archive = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_ARCHIVE")); + + if (opt_runtime_archive != NULL && opt_runtime_archive[0] == '\0') + g_clear_pointer (&opt_runtime_archive, g_free); + } + + if (opt_runtime_id != NULL) { - g_autofree gchar *tmp = g_steal_pointer (&opt_runtime); + const char *p; - opt_runtime = g_build_filename (opt_runtime_base, tmp, NULL); + if (opt_runtime_id[0] == '-' || opt_runtime_id[0] == '.') + { + g_warning ("--runtime-id must not start with dash or dot"); + goto out; + } + + for (p = opt_runtime_id; *p != '\0'; p++) + { + if (!g_ascii_isalnum (*p) && *p != '_' && *p != '-' && *p != '.') + { + g_warning ("--runtime-id may only contain " + "alphanumerics, underscore, dash or dot"); + goto out; + } + } } - if (opt_copy_runtime_into == NULL) - opt_copy_runtime_into = g_strdup (g_getenv ("PRESSURE_VESSEL_COPY_RUNTIME_INTO")); + if (opt_runtime_base == NULL) + opt_runtime_base = g_strdup (g_getenv ("PRESSURE_VESSEL_RUNTIME_BASE")); - if (opt_copy_runtime_into != NULL - && opt_copy_runtime_into[0] == '\0') - opt_copy_runtime_into = NULL; + if (opt_runtime != NULL && opt_runtime_archive != NULL) + { + g_warning ("--runtime and --runtime-archive cannot both be used"); + goto out; + } if (opt_graphics_provider == NULL) opt_graphics_provider = g_strdup (g_getenv ("PRESSURE_VESSEL_GRAPHICS_PROVIDER")); @@ -1493,6 +1604,12 @@ main (int argc, } } + if (opt_copy_runtime && opt_variable_dir == NULL) + { + g_warning ("--copy-runtime requires --variable-dir"); + goto out; + } + /* Finished parsing arguments, so any subsequent failures will make * us exit 1. */ ret = 1; @@ -1644,9 +1761,26 @@ main (int argc, NULL); } - if (opt_runtime != NULL && opt_runtime[0] != '\0') + if (opt_gc_legacy_runtimes + && opt_runtime_base != NULL + && opt_runtime_base[0] != '\0' + && opt_variable_dir != NULL) + { + if (!pv_runtime_garbage_collect_legacy (opt_variable_dir, + opt_runtime_base, + &local_error)) + { + g_warning ("Unable to clean up old runtimes: %s", + local_error->message); + g_clear_error (&local_error); + } + } + + if (opt_runtime != NULL || opt_runtime_archive != NULL) { PvRuntimeFlags flags = PV_RUNTIME_FLAGS_NONE; + g_autofree gchar *runtime_resolved = NULL; + const char *runtime_path = NULL; if (opt_gc_runtimes) flags |= PV_RUNTIME_FLAGS_GC_RUNTIMES; @@ -1663,9 +1797,33 @@ main (int argc, if (opt_import_vulkan_layers) flags |= PV_RUNTIME_FLAGS_IMPORT_VULKAN_LAYERS; - g_debug ("Configuring runtime %s...", opt_runtime); + if (opt_copy_runtime) + flags |= PV_RUNTIME_FLAGS_COPY_RUNTIME; + + if (opt_runtime != NULL) + { + /* already checked for mutually exclusive options */ + g_assert (opt_runtime_archive == NULL); + runtime_path = opt_runtime; + } + else + { + flags |= PV_RUNTIME_FLAGS_UNPACK_ARCHIVE; + runtime_path = opt_runtime_archive; + } + + if (!g_path_is_absolute (runtime_path) + && opt_runtime_base != NULL + && opt_runtime_base[0] != '\0') + { + runtime_resolved = g_build_filename (opt_runtime_base, + runtime_path, NULL); + runtime_path = runtime_resolved; + } + + g_debug ("Configuring runtime %s...", runtime_path); - if (is_flatpak_env && opt_copy_runtime_into == NULL) + if (is_flatpak_env && !opt_copy_runtime) { glnx_throw (error, "Cannot set up a runtime inside Flatpak without " @@ -1673,8 +1831,9 @@ main (int argc, goto out; } - runtime = pv_runtime_new (opt_runtime, - opt_copy_runtime_into, + runtime = pv_runtime_new (runtime_path, + opt_runtime_id, + opt_variable_dir, bwrap_executable, tools_dir, opt_graphics_provider, @@ -2539,9 +2698,12 @@ out: g_clear_pointer (&opt_steam_app_id, g_free); g_clear_pointer (&opt_home, g_free); g_clear_pointer (&opt_fake_home, g_free); - g_clear_pointer (&opt_runtime_base, g_free); g_clear_pointer (&opt_runtime, g_free); + g_clear_pointer (&opt_runtime_archive, g_free); + g_clear_pointer (&opt_runtime_base, g_free); + g_clear_pointer (&opt_runtime_id, g_free); g_clear_pointer (&opt_pass_fds, g_array_unref); + g_clear_pointer (&opt_variable_dir, g_free); g_debug ("Exiting with status %d", ret); return ret; diff --git a/tests/pressure-vessel/containers.py b/tests/pressure-vessel/containers.py index 549366f692a14b25e65fe8d8f8e1086b6a2f945d..6e07123436a21f03d1566e2bd1eef847f62de3dc 100755 --- a/tests/pressure-vessel/containers.py +++ b/tests/pressure-vessel/containers.py @@ -482,6 +482,7 @@ class TestContainers(BaseTest): *, copy: bool = False, gc: bool = True, + gc_legacy: bool = True, locales: bool = False, only_prepare: bool = False ) -> None: @@ -490,6 +491,7 @@ class TestContainers(BaseTest): runtime, copy=copy, gc=gc, + gc_legacy=gc_legacy, is_scout=True, locales=locales, only_prepare=only_prepare, @@ -502,6 +504,7 @@ class TestContainers(BaseTest): *, copy: bool = False, gc: bool = True, + gc_legacy: bool = True, locales: bool = False, only_prepare: bool = False ) -> None: @@ -510,6 +513,7 @@ class TestContainers(BaseTest): runtime, copy=copy, gc=gc, + gc_legacy=gc_legacy, is_soldier=True, locales=locales, only_prepare=only_prepare, @@ -520,8 +524,11 @@ class TestContainers(BaseTest): test_name: str, runtime: str, *, + archive: str = '', copy: bool = False, + fast_path: bool = False, gc: bool = True, + gc_legacy: bool = True, is_scout: bool = False, is_soldier: bool = False, locales: bool = False, @@ -530,8 +537,15 @@ class TestContainers(BaseTest): if self.bwrap is None and not only_prepare: self.skipTest('Unable to run bwrap (in a container?)') - if not os.path.isdir(runtime): - self.skipTest('{} not found'.format(runtime)) + if archive: + if not os.path.isfile(archive): + self.skipTest('{} not found'.format(archive)) + + if fast_path and not os.path.isdir(runtime): + self.skipTest('{} not found'.format(runtime)) + else: + if not os.path.isdir(runtime): + self.skipTest('{} not found'.format(runtime)) artifacts = os.path.join( self.artifacts, @@ -543,23 +557,74 @@ class TestContainers(BaseTest): argv = [ self.pv_wrap, - '--runtime', runtime, '--verbose', '--write-final-argv', final_argv_temp.name ] - var = os.path.join(self.containers_dir, 'var') - os.makedirs(var, exist_ok=True) + if archive and (fast_path or copy): + argv.extend([ + '--runtime-archive', archive, + # For simplicity, we rely on this below + '--runtime-id', 'myruntime_0.1.2', + ]) + elif archive: + # Assume the archive is accompanied by a -buildid.txt file + argv.extend([ + '--runtime-archive', archive, + ]) + else: + argv.extend(['--runtime', runtime]) + + var_dir = os.path.join(self.containers_dir, 'var') + os.makedirs(var_dir, exist_ok=True) if not locales: argv.append('--no-generate-locales') - with tempfile.TemporaryDirectory(prefix='test-', dir=var) as temp: + with tempfile.TemporaryDirectory( + prefix='test-', dir=var_dir + ) as temp, tempfile.TemporaryDirectory( + prefix='test-mock-base-', dir=var_dir + ) as mock_base: + argv.extend(['--runtime-base', mock_base]) + argv.extend(['--variable-dir', temp]) + + if fast_path: + # Pretend we had already run this runtime. Rather than + # using the real runtime's name, for simplicity this is + # a fake name. + old_dir = os.path.join(temp, 'deploy-myruntime_0.1.2') + + run_subprocess( + ['cp', '-al', runtime, old_dir], + check=True, + ) + + if os.path.isdir(os.path.join(old_dir, 'files')): + old_dir = os.path.join(old_dir, 'files') + + if os.path.isdir(os.path.join(old_dir, 'usr')): + old_dir = os.path.join(old_dir, 'usr') + + # Touch a flag file so we can detect that we reused the + # old deployment + with open(os.path.join(old_dir, 'OLD-DEPLOYMENT'), 'w'): + pass + if copy: - argv.extend(['--copy-runtime-into', temp]) + argv.append('--copy-runtime') + else: + argv.append('--no-copy-runtime') - if not gc: - argv.append('--no-gc-runtimes') + if gc: + argv.append('--gc-runtimes') + else: + argv.append('--no-gc-runtimes') + + if gc_legacy: + argv.append('--gc-legacy-runtimes') + else: + argv.append('--no-gc-legacy-runtimes') if is_scout: python = 'python3.5' @@ -595,7 +660,7 @@ class TestContainers(BaseTest): os.makedirs(os.path.join(temp, 'tmp-deleteme'), exist_ok=True) # Delete, and assert that it is recursive os.makedirs( - os.path.join(temp, 'tmp-deleteme2', 'usr', 'lib'), + os.path.join(temp, 'deploy-deleteme', 'usr', 'lib'), exist_ok=True, ) # Do not delete because it has ./keep @@ -605,6 +670,34 @@ class TestContainers(BaseTest): # Do not delete because we will write-lock .ref os.makedirs(os.path.join(temp, 'tmp-wlock'), exist_ok=True) + for d in [mock_base, temp]: + os.makedirs( + os.path.join(d, 'scout_before_0.20200101.0'), + exist_ok=True, + ) + os.makedirs( + os.path.join(d, 'soldier_0.20200101.0'), + exist_ok=True, + ) + os.makedirs( + os.path.join(d, '.scout_0.20200202.0_unpack-temp'), + exist_ok=True, + ) + os.makedirs( + os.path.join(d, '.soldier_dontdelete'), + exist_ok=True, + ) + os.makedirs( + os.path.join(d, 'scout_dontdelete'), + exist_ok=True, + ) + os.makedirs( + os.path.join(d, 'soldier_0.20200101.0_keep', 'keep'), + exist_ok=True, + ) + os.symlink('soldier_0.20200101.0', os.path.join(d, 'soldier')) + os.symlink('scout_dontdelete', os.path.join(d, 'scout')) + with open( os.path.join(temp, 'tmp-rlock', '.ref'), 'w+' ) as rlock_writer, open( @@ -655,40 +748,82 @@ class TestContainers(BaseTest): final_argv_temp.close() + for d in [mock_base, temp]: + members = set(os.listdir(d)) + + if gc_legacy: + self.assertNotIn('scout_before_0.20200101.0', members) + self.assertNotIn('soldier', members) + self.assertNotIn('soldier_0.20200101.0', members) + self.assertNotIn( + '.scout_0.20200202.0_unpack-temp', members, + ) + else: + self.assertIn('.scout_0.20200202.0_unpack-temp', members) + self.assertIn('scout_before_0.20200101.0', members) + self.assertIn('soldier', members) + self.assertIn('soldier_0.20200101.0', members) + + self.assertIn('.soldier_dontdelete', members) + self.assertIn('scout', members) + self.assertIn('scout_dontdelete', members) + self.assertIn('soldier_0.20200101.0_keep', members) + if copy: members = set(os.listdir(temp)) self.assertIn('.ref', members) + + if fast_path: + self.assertIn('deploy-myruntime_0.1.2', members) + self.assertIn('donotdelete', members) self.assertIn('tmp-keep', members) self.assertIn('tmp-rlock', members) self.assertIn('tmp-wlock', members) if gc: + if archive: + self.assertNotIn('deploy-deleteme', members) + self.assertNotIn('tmp-deleteme', members) - self.assertNotIn('tmp-deleteme2', members) else: # These would have been deleted if not for --no-gc-runtimes + self.assertIn('deploy-deleteme', members) self.assertIn('tmp-deleteme', members) - self.assertIn('tmp-deleteme2', members) members.discard('.ref') + members.discard('.scout_0.20200202.0_unpack-temp') + members.discard('.soldier_dontdelete') members.discard('donotdelete') + members.discard('scout') + members.discard('scout_before_0.20200101.0') + members.discard('scout_dontdelete') + members.discard('soldier') + members.discard('soldier_0.20200101.0') + members.discard('soldier_0.20200101.0_keep') members.discard('tmp-deleteme') - members.discard('tmp-deleteme2') members.discard('tmp-keep') members.discard('tmp-rlock') members.discard('tmp-wlock') + members.discard('deploy-deleteme') + members.discard('deploy-myruntime_0.1.2') # After discarding those, there should be exactly one left: # the one we just created self.assertEqual(len(members), 1) tree = os.path.join(temp, members.pop()) + if fast_path: + require_flag_file = 'OLD-DEPLOYMENT' + else: + require_flag_file = '' + with self.subTest('mutable sysroot'): self._assert_mutable_sysroot( tree, artifacts, is_scout=is_scout, is_soldier=is_soldier, + require_flag_file=require_flag_file, ) def _assert_mutable_sysroot( @@ -697,7 +832,8 @@ class TestContainers(BaseTest): artifacts: str, *, is_scout: bool = False, - is_soldier: bool = False + is_soldier: bool = False, + require_flag_file: str = '' ) -> None: with open( os.path.join(artifacts, 'contents.txt'), @@ -718,6 +854,11 @@ class TestContainers(BaseTest): ) self.assertTrue(os.path.isdir(os.path.join(tree, 'sbin'))) + if require_flag_file: + self.assertTrue( + os.path.exists(os.path.join(tree, 'usr', require_flag_file)), + ) + target = os.readlink(os.path.join(tree, 'overrides')) self.assertEqual(target, 'usr/lib/pressure-vessel/overrides') self.assertTrue( @@ -1088,7 +1229,7 @@ class TestContainers(BaseTest): with self.subTest('copy'): self._test_scout( 'scout_sysroot_copy_usrmerge', scout, - copy=True, gc=False, + copy=True, gc=False, gc_legacy=False, ) with self.subTest('transient'): @@ -1106,6 +1247,38 @@ class TestContainers(BaseTest): with self.subTest('transient'): self._test_scout('scout', scout) + def test_unpack(self) -> None: + scout = os.path.join( + self.containers_dir, + 'scout', + ) + archive = os.path.join( + self.containers_dir, + ('com.valvesoftware.SteamRuntime.Platform-amd64,i386-' + 'scout-runtime.tar.gz'), + ) + + self._test_container( + 'scout_unpack', + scout, + archive=archive, + copy=True, + is_scout=True, + locales=False, + only_prepare=True, + ) + + self._test_container( + 'scout_skip_unpack', + scout, + archive=archive, + copy=True, + fast_path=True, + is_scout=True, + locales=False, + only_prepare=True, + ) + def test_soldier_sysroot(self) -> None: soldier = os.path.join(self.containers_dir, 'soldier_sysroot') diff --git a/tests/pressure-vessel/utils.c b/tests/pressure-vessel/utils.c index 5508fb04f3dd20cd1f6c1ba99873eca932aaeed4..9fa84bb99e0e26d7134acaf3b2d6b9e00c014c9c 100644 --- a/tests/pressure-vessel/utils.c +++ b/tests/pressure-vessel/utils.c @@ -38,7 +38,7 @@ typedef struct { - int unused; + TestsOpenFdSet old_fds; } Fixture; typedef struct @@ -51,6 +51,8 @@ setup (Fixture *f, gconstpointer context) { G_GNUC_UNUSED const Config *config = context; + + f->old_fds = tests_check_fd_leaks_enter (); } static void @@ -58,6 +60,8 @@ teardown (Fixture *f, gconstpointer context) { G_GNUC_UNUSED const Config *config = context; + + tests_check_fd_leaks_leave (f->old_fds); } static void @@ -162,6 +166,68 @@ test_capture_output (Fixture *f, g_clear_pointer (&output, g_free); } +static void +test_delete_dangling_symlink (Fixture *f, + gconstpointer context) +{ + g_autoptr(GError) error = NULL; + g_auto(GLnxTmpDir) tmpdir = { FALSE }; + struct stat stat_buf; + + glnx_mkdtemp ("test-XXXXXX", 0700, &tmpdir, &error); + g_assert_no_error (error); + + glnx_file_replace_contents_at (tmpdir.fd, "exists", (const guint8 *) "", + 0, 0, NULL, &error); + g_assert_no_error (error); + + g_assert_no_errno (mkdirat (tmpdir.fd, "subdir", 0755)); + g_assert_no_errno (symlinkat ("exists", tmpdir.fd, "target-exists")); + g_assert_no_errno (symlinkat ("does-not-exist", tmpdir.fd, + "target-does-not-exist")); + g_assert_no_errno (symlinkat ("/etc/ssl/private/nope", tmpdir.fd, + "cannot-stat-target")); + + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "cannot-stat-target"); + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "does-not-exist"); + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "exists"); + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "subdir"); + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "target-does-not-exist"); + pv_delete_dangling_symlink (tmpdir.fd, tmpdir.path, "target-exists"); + + /* We cannot tell whether ./cannot-stat-target is dangling or not + * (assuming we're not root) so we give it the benefit of the doubt + * and do not delete it */ + if (G_LIKELY (stat ("/etc/ssl/private/nope", &stat_buf) < 0 + && errno == EACCES)) + { + g_assert_no_errno (fstatat (tmpdir.fd, "cannot-stat-target", &stat_buf, + AT_SYMLINK_NOFOLLOW)); + } + + /* ./does-not-exist never existed */ + g_assert_cmpint (fstatat (tmpdir.fd, "does-not-exist", &stat_buf, + AT_SYMLINK_NOFOLLOW) == 0 ? 0 : errno, + ==, ENOENT); + + /* ./exists is not a symlink and so was not deleted */ + g_assert_no_errno (fstatat (tmpdir.fd, "exists", &stat_buf, + AT_SYMLINK_NOFOLLOW)); + + /* ./subdir is not a symlink and so was not deleted */ + g_assert_no_errno (fstatat (tmpdir.fd, "subdir", &stat_buf, + AT_SYMLINK_NOFOLLOW)); + + /* ./target-does-not-exist is a dangling symlink and so was deleted */ + g_assert_cmpint (fstatat (tmpdir.fd, "target-does-not-exist", &stat_buf, + AT_SYMLINK_NOFOLLOW) == 0 ? 0 : errno, + ==, ENOENT); + + /* ./target-exists is a non-dangling symlink and so was not deleted */ + g_assert_no_errno (fstatat (tmpdir.fd, "target-exists", &stat_buf, + AT_SYMLINK_NOFOLLOW)); +} + static void test_envp_cmp (Fixture *f, gconstpointer context) @@ -307,6 +373,8 @@ main (int argc, setup, test_arbitrary_key, teardown); g_test_add ("/capture-output", Fixture, NULL, setup, test_capture_output, teardown); + g_test_add ("/delete-dangling-symlink", Fixture, NULL, + setup, test_delete_dangling_symlink, teardown); g_test_add ("/envp-cmp", Fixture, NULL, setup, test_envp_cmp, teardown); g_test_add ("/get-path-after", Fixture, NULL, setup, test_get_path_after, teardown); diff --git a/tests/test-utils.h b/tests/test-utils.h index 3bcc6840e8a659f620d2fe1fa0c0aeb8c1c95154..dc38143944a5904b919de499afb9bd7c34ab6763 100644 --- a/tests/test-utils.h +++ b/tests/test-utils.h @@ -56,6 +56,11 @@ #define g_assert_cmpstr(a, op, b) g_assert (g_strcmp0 ((a), (b)) op 0) #endif +#ifndef g_assert_no_errno +#define g_assert_no_errno(expr) \ + g_assert_cmpstr ((expr) >= 0 ? NULL : g_strerror (errno), ==, NULL) +#endif + /* * Other assorted test helpers. */