rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 heartwood317c1d1814dce27c568f276c3a414e66a46d86e3
{
"request": "trigger",
"version": 1,
"event_type": "patch",
"repository": {
"id": "rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5",
"name": "heartwood",
"description": "Radicle Heartwood Protocol & Stack",
"private": false,
"default_branch": "master",
"delegates": [
"did:key:z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT",
"did:key:z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW",
"did:key:z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM",
"did:key:z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz",
"did:key:z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz"
]
},
"action": "Created",
"patch": {
"id": "6fd6891f3fd4802d5b354c9f54e30a9f8324159c",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"title": "cli: Handle ctrl+c interrupts during an active spinner thread",
"state": {
"status": "merged",
"conflicts": []
},
"before": "92c9e21873be1c9107f906f826d4774a8a950d79",
"after": "317c1d1814dce27c568f276c3a414e66a46d86e3",
"commits": [
"317c1d1814dce27c568f276c3a414e66a46d86e3"
],
"target": "6cfed884bf37cba1e0d8e97fa8b0e94df4a04b1f",
"labels": [],
"assignees": [],
"revisions": [
{
"id": "6fd6891f3fd4802d5b354c9f54e30a9f8324159c",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "This is in response to issue# d2aeb57, where the cursor is not shown when\ninterrupting `rad` when a progress spinner thread is present.\n\nThis is mainly achieved by adding signal handling for spinners now using `radicle-signals` and\n`crossbeam-channel`. When a `SIGINT` signal fires off while a spinner is active, it will drop its \nrespective `HideCursor` object and exit the process entirely.\n\nSome other somewhat related changes to this patch:\n - to better pass on handling to the OS/another CLI element, `radicle-signals::uninstall()`\n is implemented and used for the spinners.\n - the CLI pager also used `radicle-signals`, so that was also modified to use the above\n - doc changes to the spinner/pager to include the tidbits on signal handling",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "6c20af4fb2c7d1889ec38d0f3525c5160fa47817",
"timestamp": 1719412249
},
{
"id": "bb1e20e2d6a76156058c9d43e1f2deb4209b674d",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "No longer use `ctrlc` to handle interruption. Using `radicle-signals` + `crossbeam_channel` for that now.\nImplementation of this is working but may need some (significant) improvement.",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "afcc9acba535515a3a3a2e06564659ae699d3b88",
"timestamp": 1719418431
},
{
"id": "1f5fdeaf4a016f57c410095635cb4b033270eb95",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "Clean up",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "f94832ccd934168cdf66e9f94a66fdc7aa248f66",
"timestamp": 1719437826
},
{
"id": "18f9f61c0bfdcd33b42aae74fd04d4b53a15fc4f",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "e757af5c8ba548d0e3c8b9c59bd5fc87bf8bf073",
"timestamp": 1719438010
},
{
"id": "aa5d8da23247483c7c99ad266575127d2f211087",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "Adhere to \"Radicle house style choice\" + improve(?) errors for `init_channels()`",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "8ff4267f1526c9fd46b3a280e137fe7754e45a6f",
"timestamp": 1719498678
},
{
"id": "0b7e95044b6737c15fe979f1ee199262a15e45d4",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "`radicle-signals` can now uninstall handlers now.\n - made spinner uninstall signal on any state of finish\n\nReason why I made that happen is because the pager also installs\nsignal handlers and I didn't want either to conflict.\n\nI forgot to uninstall handlers for pagers so I'll do that now",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "e087739a8dfe20c15b45793116acbcc9dd0ef113",
"timestamp": 1719584938
},
{
"id": "a432d044090155ca33c6e9ca461ac39b0f2a6bfc",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "- Pagers now uninstall signal handlers when finished\n - `(un)init_channels()` now actually returns `io::Result<()>`\n - ran `$ cargo fmt`",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "1b092aeabec41f8cc1316fa5b68d0616fb818f8b",
"timestamp": 1719587169
},
{
"id": "5204884cf1e7fa52152f8c136e5d706a48a76fa3",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "remove `CHANNEL` static and `(un)init_channel()` functions.",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "5acf848b5820edf29564350c6ece11b231e074b8",
"timestamp": 1719621338
},
{
"id": "48053c250cf1cad276f116096e1f8e2e157affd1",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "- Don't panic if we can't install signal handlers, just keep spinning.\n - also added sections to `spinner_to()`/`page()`'s comments on signal handling (`spinner()` kinda counts but its mostly meant to redirect to `spinner_to()`).",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "dff5922e50ed6be35cfcfd338b799a79da42bd8e",
"timestamp": 1719886410
},
{
"id": "c3f33ea7012a8910605496a2e458ceb4e6db3a8f",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "- `_(un)install()` iterates over a slice of signals (now const `SIGNALS`).",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "3441744c96be1112aa6d73114dab03dbbc26d260",
"timestamp": 1719933596
},
{
"id": "926316062e70b5bb106a78b1ded2d9f6f51c8cb2",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "- use `sig_rx.try_recv()` rather than `chan::select!` for getting signals\n - draw spinner as being cancelled before exiting\n - change exit code to -1 rather than 0",
"base": "92c9e21873be1c9107f906f826d4774a8a950d79",
"oid": "317c1d1814dce27c568f276c3a414e66a46d86e3",
"timestamp": 1721305601
},
{
"id": "52473f081668d6001619ad2e47b5f9b8620923cd",
"author": {
"id": "did:key:z6Mktnv1oLyUHMHm7j3p5D119V2yRWPrfMXAn3W42irs8DHL",
"alias": "radsammyt"
},
"description": "Rebase.",
"base": "a831e18a72107abfbf11d2ba3c12f8467a345009",
"oid": "1848c2b85d1bb9a12531b3f31a59e8b2958917a2",
"timestamp": 1721310490
}
]
}
}
{
"response": "triggered",
"run_id": {
"id": "9153220d-c35f-4289-823d-4ee9a2316065"
},
"info_url": "https://cci.rad.levitte.org//9153220d-c35f-4289-823d-4ee9a2316065.html"
}
Started at: 2025-10-21 20:21:00.643864+02:00
Commands:
$ rad clone rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5 .
✓ Creating checkout in ./...
✓ Remote cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT added
✓ Remote-tracking branch cloudhead@z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT/master created for z6MksFqXN3Yhqk8pTJdUGLwATkRfQvwZXPqR2qMEhbS9wzpT
✓ Remote cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW added
✓ Remote-tracking branch cloudhead@z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW/master created for z6MktaNvN1KVFMkSRAiN4qK5yvX1zuEEaseeX5sffhzPZRZW
✓ Remote fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM added
✓ Remote-tracking branch fintohaps@z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM/master created for z6MkireRatUThvd3qzfKht1S44wpm4FEWSSa4PRMTSQZ3voM
✓ Remote erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz added
✓ Remote-tracking branch erikli@z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz/master created for z6MkgFq6z5fkF2hioLLSNu1zP2qEL1aHXHZzGH1FLFGAnBGz
✓ Remote lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz added
✓ Remote-tracking branch lorenz@z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz/master created for z6MkkPvBfjP4bQmco5Dm7UGsX2ruDBieEHi8n9DVJWX5sTEz
✓ Repository successfully cloned under /opt/radcis/ci.rad.levitte.org/cci/state/9153220d-c35f-4289-823d-4ee9a2316065/w/
╭────────────────────────────────────╮
│ heartwood │
│ Radicle Heartwood Protocol & Stack │
│ 125 issues · 15 patches │
╰────────────────────────────────────╯
Run `cd ./.` to go to the repository directory.
Exit code: 0
$ rad patch checkout 6fd6891f3fd4802d5b354c9f54e30a9f8324159c
✓ Switched to branch patch/6fd6891 at revision 9263160
✓ Branch patch/6fd6891 setup to track rad/patches/6fd6891f3fd4802d5b354c9f54e30a9f8324159c
Exit code: 0
$ git config advice.detachedHead false
Exit code: 0
$ git checkout 317c1d1814dce27c568f276c3a414e66a46d86e3
HEAD is now at 317c1d18 cli: Handle ctrl+c interrupts during an active spinner thread
Exit code: 0
$ git show 317c1d1814dce27c568f276c3a414e66a46d86e3
commit 317c1d1814dce27c568f276c3a414e66a46d86e3
Author: RadsammyT <radsammyt@gmail.com>
Date: Wed Jun 26 09:43:33 2024 -0400
cli: Handle ctrl+c interrupts during an active spinner thread
diff --git a/radicle-signals/src/lib.rs b/radicle-signals/src/lib.rs
index 98591cd5..5cabe19f 100644
--- a/radicle-signals/src/lib.rs
+++ b/radicle-signals/src/lib.rs
@@ -33,6 +33,9 @@ impl TryFrom<i32> for Signal {
/// Signal notifications are sent via this channel.
static NOTIFY: Mutex<Option<chan::Sender<Signal>>> = Mutex::new(None);
+/// A slice of signals to handle.
+const SIGNALS: &[i32] = &[libc::SIGINT, libc::SIGTERM, libc::SIGHUP, libc::SIGWINCH];
+
/// Install global signal handlers.
pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
if let Ok(mut channel) = NOTIFY.try_lock() {
@@ -54,23 +57,51 @@ pub fn install(notify: chan::Sender<Signal>) -> io::Result<()> {
Ok(())
}
+/// Uninstall global signal handlers.
+pub fn uninstall() -> io::Result<()> {
+ if let Ok(mut channel) = NOTIFY.try_lock() {
+ if channel.is_none() {
+ return Err(io::Error::new(
+ io::ErrorKind::NotFound,
+ "signal handler is already uninstalled",
+ ));
+ }
+ *channel = None;
+
+ unsafe { _uninstall() }?;
+ } else {
+ return Err(io::Error::new(
+ io::ErrorKind::WouldBlock,
+ "unable to uninstall signal handler",
+ ));
+ }
+ Ok(())
+}
+
/// Install global signal handlers.
///
/// # Safety
///
/// Calls `libc` functions safely.
unsafe fn _install() -> io::Result<()> {
- if libc::signal(libc::SIGTERM, handler as libc::sighandler_t) == libc::SIG_ERR {
- return Err(io::Error::last_os_error());
- }
- if libc::signal(libc::SIGINT, handler as libc::sighandler_t) == libc::SIG_ERR {
- return Err(io::Error::last_os_error());
- }
- if libc::signal(libc::SIGHUP, handler as libc::sighandler_t) == libc::SIG_ERR {
- return Err(io::Error::last_os_error());
+ for signal in SIGNALS {
+ if libc::signal(*signal, handler as libc::sighandler_t) == libc::SIG_ERR {
+ return Err(io::Error::last_os_error());
+ }
}
- if libc::signal(libc::SIGWINCH, handler as libc::sighandler_t) == libc::SIG_ERR {
- return Err(io::Error::last_os_error());
+ Ok(())
+}
+
+/// Uninstall global signal handlers.
+///
+/// # Safety
+///
+/// Calls `libc` functions safely.
+unsafe fn _uninstall() -> io::Result<()> {
+ for signal in SIGNALS {
+ if libc::signal(*signal, libc::SIG_DFL) == libc::SIG_ERR {
+ return Err(io::Error::last_os_error());
+ }
}
Ok(())
}
diff --git a/radicle-term/src/pager.rs b/radicle-term/src/pager.rs
index a25a6e50..43425b7f 100644
--- a/radicle-term/src/pager.rs
+++ b/radicle-term/src/pager.rs
@@ -24,6 +24,12 @@ pub enum Error {
/// A pager for the given element. Re-renders the element when the terminal is resized so that
/// it doesn't wrap. If the output device is not a TTY, just prints the element via
/// [`Element::print`].
+///
+/// # Signal Handling
+///
+/// This will install handlers for the pager until finished by the user, with there
+/// being only one element handling signals at a time. If the pager cannot install
+/// handlers, then it will return with an error.
pub fn page<E: Element + Send + 'static>(element: E) -> Result<(), Error> {
let (events_tx, events_rx) = chan::unbounded();
let (signals_tx, signals_rx) = chan::unbounded();
@@ -35,9 +41,13 @@ pub fn page<E: Element + Send + 'static>(element: E) -> Result<(), Error> {
events_tx.send(e).ok();
}
});
- thread::spawn(move || main(element, signals_rx, events_rx))
+ let result = thread::spawn(move || main(element, signals_rx, events_rx))
.join()
- .unwrap()
+ .unwrap();
+
+ signals::uninstall()?;
+
+ result
}
fn main<E: Element>(
diff --git a/radicle-term/src/spinner.rs b/radicle-term/src/spinner.rs
index 3a6564e5..09056970 100644
--- a/radicle-term/src/spinner.rs
+++ b/radicle-term/src/spinner.rs
@@ -3,6 +3,11 @@ use std::mem::ManuallyDrop;
use std::sync::{Arc, Mutex};
use std::{fmt, io, thread, time};
+use crossbeam_channel as chan;
+
+use radicle_signals as signals;
+use signals::Signal;
+
use crate::io::{ERROR_PREFIX, WARNING_PREFIX};
use crate::Paint;
@@ -103,10 +108,10 @@ impl Spinner {
}
/// Create a new spinner with the given message. Sends animation output to `stderr` and success or
-/// failure messages to `stdout`.
+/// failure messages to `stdout`. This function handles signals, with there being only one
+/// element handling signals at a time, and is a wrapper to [`spinner_to()`].
pub fn spinner(message: impl ToString) -> Spinner {
let (stdout, stderr) = (io::stdout(), io::stderr());
-
if stderr.is_terminal() {
spinner_to(message, stdout, stderr)
} else {
@@ -115,6 +120,12 @@ pub fn spinner(message: impl ToString) -> Spinner {
}
/// Create a new spinner with the given message, and send output to the given writers.
+///
+/// # Signal Handling
+///
+/// This will install handlers for the spinner until cancelled or dropped, with there
+/// being only one element handling signals at a time. If the spinner cannot install
+/// handlers, then it will not attempt to install handlers again, and continue running.
pub fn spinner_to(
message: impl ToString,
mut completion: impl io::Write + Send + 'static,
@@ -122,6 +133,10 @@ pub fn spinner_to(
) -> Spinner {
let message = message.to_string();
let progress = Arc::new(Mutex::new(Progress::new(Paint::new(message))));
+ let (sig_tx, sig_rx) = chan::unbounded();
+
+ let sig_result = signals::install(sig_tx);
+
let handle = thread::Builder::new()
.name(String::from("spinner"))
.spawn({
@@ -134,6 +149,25 @@ pub fn spinner_to(
let Ok(mut progress) = progress.lock() else {
break;
};
+ // If were unable to install handles, skip signal processing entirely.
+ if sig_result.is_ok() {
+ match sig_rx.try_recv() {
+ Ok(sig) if sig == Signal::Interrupt || sig == Signal::Terminate => {
+ write!(animation, "\r{}", termion::clear::UntilNewline).ok();
+ writeln!(
+ completion,
+ "{ERROR_PREFIX} {} {}",
+ &progress.message,
+ Paint::red("<canceled>")
+ )
+ .ok();
+ drop(animation);
+ std::process::exit(-1);
+ }
+ Ok(_) => {}
+ Err(_) => {}
+ }
+ }
match &mut *progress {
Progress {
state: State::Running { cursor },
@@ -192,6 +226,9 @@ pub fn spinner_to(
drop(progress);
thread::sleep(DEFAULT_TICK);
}
+ if sig_result.is_ok() {
+ let _ = signals::uninstall();
+ }
}
})
// SAFETY: Only panics if the thread name contains `null` bytes, which isn't the case here.
Exit code: 0
shell: 'cargo --version rustc --version cargo fmt --check cargo clippy --all-targets --workspace -- --deny clippy::all cargo build --all-targets --workspace cargo doc --workspace cargo test --workspace --no-fail-fast '
Commands:
$ podman run --name 9153220d-c35f-4289-823d-4ee9a2316065 -v /opt/radcis/ci.rad.levitte.org/cci/state/9153220d-c35f-4289-823d-4ee9a2316065/s:/9153220d-c35f-4289-823d-4ee9a2316065/s:ro -v /opt/radcis/ci.rad.levitte.org/cci/state/9153220d-c35f-4289-823d-4ee9a2316065/w:/9153220d-c35f-4289-823d-4ee9a2316065/w -w /9153220d-c35f-4289-823d-4ee9a2316065/w -v /opt/radcis/ci.rad.levitte.org/.radicle:/${id}/.radicle:ro -e RAD_HOME=/${id}/.radicle rust:bookworm bash /9153220d-c35f-4289-823d-4ee9a2316065/s/script.sh
+ cargo --version
info: syncing channel updates for '1.77-x86_64-unknown-linux-gnu'
info: latest update on 2024-04-09, rust version 1.77.2 (25ef9e3d8 2024-04-09)
info: downloading component 'cargo'
info: downloading component 'rust-std'
info: downloading component 'rustc'
info: installing component 'cargo'
info: installing component 'rust-std'
info: installing component 'rustc'
cargo 1.77.2 (e52e36006 2024-03-26)
+ rustc --version
rustc 1.77.2 (25ef9e3d8 2024-04-09)
+ cargo fmt --check
error: 'cargo-fmt' is not installed for the toolchain '1.77-x86_64-unknown-linux-gnu'.
To install, run `rustup component add --toolchain 1.77-x86_64-unknown-linux-gnu rustfmt`
Exit code: 1
{
"response": "finished",
"result": "failure"
}