diff --git a/Makefile-rpm-ostree.am b/Makefile-rpm-ostree.am
index 1e511eb9..b2fb067e 100644
--- a/Makefile-rpm-ostree.am
+++ b/Makefile-rpm-ostree.am
@@ -28,6 +28,7 @@ rpm_ostree_SOURCES = src/app/main.c \
src/app/rpmostree-builtin-deploy.c \
src/app/rpmostree-builtin-reload.c \
src/app/rpmostree-builtin-rebase.c \
+ src/app/rpmostree-builtin-cancel.c \
src/app/rpmostree-builtin-cleanup.c \
src/app/rpmostree-builtin-initramfs.c \
src/app/rpmostree-builtin-livefs.c \
diff --git a/man/rpm-ostree.xml b/man/rpm-ostree.xml
index 6ff05ae6..140e1561 100644
--- a/man/rpm-ostree.xml
+++ b/man/rpm-ostree.xml
@@ -116,6 +116,20 @@ Boston, MA 02111-1307, USA.
+
+ cancel
+
+
+
+ Cancel a pending transaction. Exits successfully and does
+ nothing if no transaction is running. Note that it is fully
+ safe to cancel transactions such as upgrade
+ in general.
+
+
+
+
+
db
diff --git a/src/app/main.c b/src/app/main.c
index 874df88f..2a40abaa 100644
--- a/src/app/main.c
+++ b/src/app/main.c
@@ -65,6 +65,9 @@ static RpmOstreeCommand commands[] = {
{ "reload", 0,
"Reload configuration",
rpmostree_builtin_reload },
+ { "cancel", 0,
+ "Cancel an active transaction",
+ rpmostree_builtin_cancel },
{ "initramfs", 0,
"Enable or disable local initramfs regeneration",
rpmostree_builtin_initramfs },
diff --git a/src/app/rpmostree-builtin-cancel.c b/src/app/rpmostree-builtin-cancel.c
new file mode 100644
index 00000000..cfcbfdc9
--- /dev/null
+++ b/src/app/rpmostree-builtin-cancel.c
@@ -0,0 +1,121 @@
+/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * 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 of the licence 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, write to the
+ * Free Software Foundation, Inc., 59 Temple Place, Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include "config.h"
+
+#include
+#include
+
+#include "rpmostree-builtins.h"
+#include "rpmostree-util.h"
+#include "rpmostree-libbuiltin.h"
+#include "rpmostree-dbus-helpers.h"
+
+#include
+
+static GOptionEntry option_entries[] = {
+ { NULL }
+};
+
+static void
+on_cancel_method_completed (GObject *src,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ /* Nothing right now - we ideally should check for an error, but the
+ * transaction could have gone away. A better fix here would be to keep
+ * transactions around for some period of time.
+ */
+ (void) rpmostree_transaction_call_cancel_finish ((RPMOSTreeTransaction*)src, res, NULL);
+}
+
+static void
+on_active_txn_path_changed (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ gboolean *donep = user_data;
+ *donep = TRUE;
+ g_main_context_wakeup (NULL);
+}
+
+int
+rpmostree_builtin_cancel (int argc,
+ char **argv,
+ RpmOstreeCommandInvocation *invocation,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autoptr(GOptionContext) context = g_option_context_new ("");
+ glnx_unref_object RPMOSTreeSysroot *sysroot_proxy = NULL;
+ _cleanup_peer_ GPid peer_pid = 0;
+
+ if (!rpmostree_option_context_parse (context,
+ option_entries,
+ &argc, &argv,
+ invocation,
+ cancellable,
+ NULL, NULL,
+ &sysroot_proxy,
+ &peer_pid,
+ error))
+ return EXIT_FAILURE;
+
+
+ /* Keep track of the txn path we saw first, as well as checking if it changed
+ * over time.
+ */
+ g_autofree char *txn_path = NULL;
+ glnx_unref_object RPMOSTreeTransaction *txn_proxy = NULL;
+ if (!rpmostree_transaction_connect_active (sysroot_proxy, &txn_path, &txn_proxy,
+ cancellable, error))
+ return EXIT_FAILURE;
+ if (!txn_proxy)
+ {
+ /* Let's not make this an error; cancellation may race with completion.
+ * Perhaps in the future we could try to see whether a txn exited
+ * "recently" but eh.
+ */
+ g_print ("No active transaction.\n");
+ return EXIT_SUCCESS;
+ }
+
+ const char *title = rpmostree_transaction_get_title (txn_proxy);
+ g_print ("Cancelling transaction: %s\n", title);
+
+ /* Asynchronously cancel, waiting for the sysroot property to change */
+ rpmostree_transaction_call_cancel (txn_proxy, cancellable,
+ on_cancel_method_completed, NULL);
+
+ gboolean done = FALSE;
+ g_signal_connect (txn_proxy, "notify::active-transaction-path",
+ G_CALLBACK (on_active_txn_path_changed), &done);
+ /* Wait for the transaction to go away */
+ while (!done)
+ {
+ const char *current_txn_path = rpmostree_sysroot_get_active_transaction_path (sysroot_proxy);
+ if (g_strcmp0 (txn_path, current_txn_path) != 0)
+ break;
+ g_main_context_iteration (NULL, TRUE);
+ }
+ g_print ("Cancelled.\n");
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/app/rpmostree-builtins.h b/src/app/rpmostree-builtins.h
index 17c930f9..a607f761 100644
--- a/src/app/rpmostree-builtins.h
+++ b/src/app/rpmostree-builtins.h
@@ -64,6 +64,7 @@ BUILTINPROTO(upgrade);
BUILTINPROTO(reload);
BUILTINPROTO(deploy);
BUILTINPROTO(rebase);
+BUILTINPROTO(cancel);
BUILTINPROTO(cleanup);
BUILTINPROTO(rollback);
BUILTINPROTO(initramfs);
diff --git a/src/daemon/org.projectatomic.rpmostree1.policy b/src/daemon/org.projectatomic.rpmostree1.policy
index 304e89db..8f180d3e 100644
--- a/src/daemon/org.projectatomic.rpmostree1.policy
+++ b/src/daemon/org.projectatomic.rpmostree1.policy
@@ -106,6 +106,16 @@
+
+ Cancel active transaction
+ Authentication is required to cancel a transaction
+
+ auth_admin
+ auth_admin
+ auth_admin_keep
+
+
+
Clear cache
Authentication is required to clear cache / pending data
diff --git a/src/daemon/rpmostreed-sysroot.c b/src/daemon/rpmostreed-sysroot.c
index d490f979..1a8f5390 100644
--- a/src/daemon/rpmostreed-sysroot.c
+++ b/src/daemon/rpmostreed-sysroot.c
@@ -569,6 +569,10 @@ sysroot_authorize_method (GDBusInterfaceSkeleton *interface,
{
action = "org.projectatomic.rpmostree1.reload-daemon";
}
+ else if (g_strcmp0 (method_name, "Cancel") == 0)
+ {
+ action = "org.projectatomic.rpmostree1.cancel";
+ }
else if (g_strcmp0 (method_name, "RegisterClient") == 0 ||
g_strcmp0 (method_name, "UnregisterClient") == 0)
{
diff --git a/tests/vmcheck/test-layering-scripts.sh b/tests/vmcheck/test-layering-scripts.sh
index 225a2cf4..7f97f1b2 100755
--- a/tests/vmcheck/test-layering-scripts.sh
+++ b/tests/vmcheck/test-layering-scripts.sh
@@ -159,22 +159,39 @@ if vm_rpmostree install rofiles-violation; then
assert_not_reached "installed test-post-rofiles-violation!"
fi
-# Test cancellation via having a script hang
-cursor=$(vm_get_journal_cursor)
+# Test cancellation via having a script hang; we interrupt directly by sending
+# SIGINT to the client binary.
vm_build_rpm post-that-hangs \
post "echo entering post-that-hangs-infloop 1>&2; while true; do sleep 1h; done"
-# use a systemd transient service as an easy way to run in the background
-vm_cmd systemd-run --unit vmcheck-install-hang rpm-ostree install post-that-hangs
-if ! vm_wait_content_after_cursor "${cursor}" "entering post-that-hangs-infloop"; then
- vm_cmd systemctl stop vmcheck-install-hang
- assert_not_reached "failed to wait for post-that-hangs"
-fi
+background_install_post_that_hangs() {
+ local cursor="$1"
+ # use a systemd transient service as an easy way to run in the background; be
+ # sure any previous failed instances are cleaned up
+ vm_cmd systemctl stop vmcheck-install-hang || true
+ vm_cmd systemctl reset-failed vmcheck-install-hang || true
+ vm_cmd systemd-run --unit vmcheck-install-hang rpm-ostree install post-that-hangs
+ if ! vm_wait_content_after_cursor "${cursor}" "entering post-that-hangs-infloop"; then
+ vm_cmd systemctl stop vmcheck-install-hang || true
+ assert_not_reached "failed to wait for post-that-hangs"
+ fi
+}
+cursor=$(vm_get_journal_cursor)
+background_install_post_that_hangs "${cursor}"
vm_cmd pkill --signal INT -f "'rpm-ostree install post-that-hangs'"
# Wait for our expected result
vm_wait_content_after_cursor "${cursor}" "Txn.*failed.*Running %post for post-that-hangs"
# Forcibly restart now to avoid any races with the txn finally exiting
vm_cmd systemctl restart rpm-ostreed
-echo "ok cancel infinite post"
+echo "ok cancel infinite post via SIGINT"
+
+# Test `rpm-ostree cancel` (which is the same as doing a Ctrl-C on the client)
+cursor=$(vm_get_journal_cursor)
+background_install_post_that_hangs "${cursor}"
+vm_rpmostree cancel
+vm_wait_content_after_cursor "${cursor}" "Txn.*failed.*Running %post for post-that-hangs"
+# Forcibly restart now to avoid any races with the txn finally exiting
+vm_cmd systemctl restart rpm-ostreed
+echo "ok cancel infinite post via `rpm-ostree cancel`"
# Test rm -rf /!
vm_cmd 'useradd testuser || true'