diff --git a/build-relocatable-install.py b/build-relocatable-install.py index 8bd52e776b120efcec360df4fefa9be7fe1a4a5f..9b645a2448f53372e401a9ce50242c92e42a4d82 100755 --- a/build-relocatable-install.py +++ b/build-relocatable-install.py @@ -108,6 +108,7 @@ SCRIPTS = [ ] EXECUTABLES = [ 'pressure-vessel-try-setlocale', + 'pressure-vessel-with-lock', 'pressure-vessel-wrap', ] LIBCAPSULE_TOOLS = [ diff --git a/meson.build b/meson.build index 0214231788c080732316cd8a240884ed34943b3a..217500ef0156641262042d29fb8bee29e7d599e6 100644 --- a/meson.build +++ b/meson.build @@ -164,6 +164,32 @@ add_project_arguments( language : 'c', ) +executable( + 'pressure-vessel-with-lock', + sources : [ + 'src/bwrap-lock.c', + 'src/bwrap-lock.h', + 'src/glib-backports.c', + 'src/glib-backports.h', + 'src/flatpak-utils.c', + 'src/flatpak-utils-private.h', + 'src/utils.c', + 'src/utils.h', + 'src/with-lock.c', + ], + c_args : [ + '-Wno-unused-local-typedefs', + ], + dependencies : [ + dependency('gio-unix-2.0', required : true), + subproject('libglnx').get_variable('libglnx_dep'), + ], + install : true, + install_dir : get_option('bindir'), + build_rpath : '${ORIGIN}/../' + get_option('libdir'), + install_rpath : '${ORIGIN}/../' + get_option('libdir'), +) + executable( 'pressure-vessel-wrap', sources : [ diff --git a/src/with-lock.c b/src/with-lock.c new file mode 100644 index 0000000000000000000000000000000000000000..4015086b5e376bab691d3d216a1406ba2b7ad1cb --- /dev/null +++ b/src/with-lock.c @@ -0,0 +1,347 @@ +/* pressure-vessel-with-lock — run a command with a lock held. + * Basically flock(1), but using fcntl locks compatible with those used + * by bubblewrap and Flatpak. + * + * Copyright © 2019 Collabora Ltd. + * + * SPDX-License-Identifier: LGPL-2.1-or-later + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "config.h" +#include "subprojects/libglnx/config.h" + +#include <fcntl.h> +#include <locale.h> +#include <sysexits.h> +#include <sys/prctl.h> +#include <sys/types.h> +#include <sys/wait.h> + +#include <glib.h> +#include <glib/gstdio.h> +#include <gio/gio.h> + +#include "glib-backports.h" +#include "libglnx.h" + +#include "bwrap-lock.h" +#include "utils.h" + +#ifndef PR_SET_CHILD_SUBREAPER +#define PR_SET_CHILD_SUBREAPER 36 +#endif + +static GPtrArray *global_locks = NULL; +static gboolean opt_create = FALSE; +static gboolean opt_subreaper = FALSE; +static gboolean opt_verbose = FALSE; +static gboolean opt_version = FALSE; +static gboolean opt_wait = FALSE; +static gboolean opt_write = FALSE; + +static gboolean +opt_fd_cb (const char *name, + const char *value, + gpointer data, + GError **error) +{ + char *endptr; + gint64 i64 = g_ascii_strtoll (value, &endptr, 10); + int fd; + int fd_flags; + + g_return_val_if_fail (global_locks != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + if (i64 < 0 || i64 > G_MAXINT || endptr == value || *endptr != '\0') + { + g_set_error (error, G_OPTION_ERROR, G_OPTION_ERROR_BAD_VALUE, + "Integer out of range or invalid: %s", value); + return FALSE; + } + + fd = (int) i64; + + fd_flags = fcntl (fd, F_GETFD); + + if (fd_flags < 0) + return glnx_throw_errno_prefix (error, "Unable to receive --fd %d", fd); + + if ((fd_flags & FD_CLOEXEC) == 0 + && fcntl (fd, F_SETFD, fd_flags | FD_CLOEXEC) != 0) + return glnx_throw_errno_prefix (error, + "Unable to configure --fd %d for " + "close-on-exec", + fd); + + g_ptr_array_add (global_locks, pv_bwrap_lock_new_take (fd)); + return TRUE; +} + +static gboolean +opt_lock_file_cb (const char *name, + const char *value, + gpointer data, + GError **error) +{ + PvBwrapLock *lock; + PvBwrapLockFlags flags = PV_BWRAP_LOCK_FLAGS_NONE; + + g_return_val_if_fail (global_locks != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + g_return_val_if_fail (value != NULL, FALSE); + + if (opt_create) + flags |= PV_BWRAP_LOCK_FLAGS_CREATE; + + if (opt_write) + flags |= PV_BWRAP_LOCK_FLAGS_WRITE; + + if (opt_wait) + flags |= PV_BWRAP_LOCK_FLAGS_WAIT; + + lock = pv_bwrap_lock_new (value, flags, error); + + if (lock == NULL) + return FALSE; + + g_ptr_array_add (global_locks, lock); + return TRUE; +} + +static GOptionEntry options[] = +{ + { "fd", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_fd_cb, + "Take a file descriptor, already locked if desired, and keep it " + "open. May be repeated.", + NULL }, + + { "create", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_create, + "Create each subsequent lock file if it doesn't exist.", + NULL }, + { "no-create", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_create, + "Don't create subsequent nonexistent lock files [default].", + NULL }, + + { "write", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_write, + "Lock each subsequent lock file for write access.", + NULL }, + { "no-write", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_write, + "Lock each subsequent lock file for read-only access [default].", + NULL }, + + { "wait", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_wait, + "Wait for each subsequent lock file.", + NULL }, + { "no-wait", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_wait, + "Exit unsuccessfully if a lock-file is busy [default].", + NULL }, + + { "lock-file", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, opt_lock_file_cb, + "Open the given file and lock it, affected by options appearing " + "earlier on the command-line. May be repeated.", + NULL }, + + { "subreaper", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_subreaper, + "Do not exit until all child processes have exited.", + NULL }, + { "no-subreaper", '\0', + G_OPTION_FLAG_REVERSE, G_OPTION_ARG_NONE, &opt_subreaper, + "Only wait for a direct child process [default].", + NULL }, + + { "verbose", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_verbose, + "Be more verbose.", NULL }, + { "version", '\0', + G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &opt_version, + "Print version number and exit.", NULL }, + { NULL } +}; + +static void +cli_log_func (const gchar *log_domain, + GLogLevelFlags log_level, + const gchar *message, + gpointer user_data) +{ + g_printerr ("%s: %s\n", (const char *) user_data, message); +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(GPtrArray) locks = NULL; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GError) local_error = NULL; + GError **error = &local_error; + int ret = EX_USAGE; + char **command_and_args; + GPid child_pid; + int wait_status = -1; + + setlocale (LC_ALL, ""); + pv_avoid_gvfs (); + + locks = g_ptr_array_new_with_free_func ((GDestroyNotify) pv_bwrap_lock_free); + global_locks = locks; + + g_set_prgname ("pressure-vessel-with-lock"); + + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_WARNING | G_LOG_LEVEL_MESSAGE, + cli_log_func, (void *) g_get_prgname ()); + + context = g_option_context_new ( + "COMMAND [ARG...]\n" + "Run COMMAND [ARG...] with a lock held, a subreaper, or similar.\n"); + + g_option_context_add_main_entries (context, options, NULL); + + if (!g_option_context_parse (context, &argc, &argv, error)) + { + if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY)) + ret = EX_TEMPFAIL; + else if (local_error->domain == G_OPTION_ERROR) + ret = EX_USAGE; + else + ret = EX_UNAVAILABLE; + + goto out; + } + + if (opt_version) + { + g_print ("%s:\n" + " Package: pressure-vessel\n" + " Version: %s\n", + argv[0], VERSION); + ret = 0; + goto out; + } + + if (opt_verbose) + g_log_set_handler (G_LOG_DOMAIN, + G_LOG_LEVEL_DEBUG | G_LOG_LEVEL_INFO, + cli_log_func, (void *) g_get_prgname ()); + + if (argc >= 2 && strcmp (argv[1], "--") == 0) + { + argv++; + argc--; + } + + if (argc < 2) + { + g_printerr ("%s: Usage: %s [OPTIONS] COMMAND [ARG...]\n", + g_get_prgname (), + g_get_prgname ()); + goto out; + } + + ret = EX_UNAVAILABLE; + + command_and_args = argv + 1; + + if (opt_subreaper + && prctl (PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0) != 0) + { + glnx_throw_errno_prefix (error, + "Unable to manage background processes"); + goto out; + } + + g_debug ("Launching child process..."); + + if (!g_spawn_async (NULL, /* working directory */ + command_and_args, + NULL, /* environment */ + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, /* child setup + user_data */ + &child_pid, + &local_error)) + { + ret = 127; + goto out; + } + + while (1) + { + pid_t died = wait (&wait_status); + + if (died < 0) + { + if (errno == EINTR) + { + continue; + } + else if (errno == ECHILD) + { + g_debug ("No more child processes, exiting"); + break; + } + else + { + glnx_throw_errno_prefix (error, "wait"); + goto out; + } + } + else if (died == child_pid) + { + if (WIFEXITED (wait_status)) + { + ret = WEXITSTATUS (wait_status); + g_debug ("Command exited with status %d", ret); + } + else if (WIFSIGNALED (wait_status)) + { + ret = 128 + WTERMSIG (wait_status); + g_debug ("Command killed by signal %d", ret - 128); + } + else + { + ret = EX_SOFTWARE; + g_debug ("Command terminated in an unknown way (wait status %d)", + wait_status); + } + } + else + { + g_debug ("Indirect child %lld exited with wait status %d", + (long long) died, wait_status); + g_warn_if_fail (opt_subreaper); + } + } + +out: + global_locks = NULL; + + if (local_error != NULL) + g_warning ("%s", local_error->message); + + return ret; +} diff --git a/tests/relocatable-install.py b/tests/relocatable-install.py index ef1d224c8aeeeaecc03d7129d833ce4f3259eb88..0110424bae3e54b8ea43d4c18f073c3c30a439b3 100755 --- a/tests/relocatable-install.py +++ b/tests/relocatable-install.py @@ -78,6 +78,7 @@ class TapTest: EXES = [ + 'pressure-vessel-with-lock', 'pressure-vessel-wrap', ] WRAPPED = [