Interruptibility

How should the interrupt channel be initialized and stored?

Use Cases

When automation software is used as:

  1. CLI interactive / non-interactive.
  2. Web service + browser access.
  3. CLI + web service + browser access.

In all cases, we need to:

  1. Initialize the CmdCtx with the interrupt_rx
  2. Spawn the listener that will send InterruptSignal in interrupt_tx.

Imagined Code

CLI Interactive / Non-interactive

Both interactive and non-interactive can listen for SIGINT:

  • Interactive: SIGINT will be sent by the user pressing Ctrl + C.
  • Non-interactive: SIGINT could be sent by a CI thread.
#![allow(unused)]
fn main() {
let (interrupt_tx, interrupt_rx) = oneshot::channel::<InterruptSignal>();
tokio::task::spawn(async move {
// Note: Once tokio takes over the process' `SIGINT` handler, it cannot be undone.
//
// This limitation is due to how Linux currently works.
tokio::signal::ctrl_c()
.await
.expect("Failed to initialize signal handler for SIGINT");
let (Ok(()) | Err(InterruptSignal)) = interrupt_tx.send(InterruptSignal);
});
let mut cmd_ctx = CmdCtx::single_profile_single_flow(output, workspace, interrupt_rx)
.build();
let cmd_outcome = EnsureCmd::exec(&mut cmd_ctx).await?;
}

Web Service

The interrupt_tx must be accessible from a separate web request.

#![allow(unused)]
fn main() {
async fn cmd_exec_start_handler(params: Params) -> CmdExecutionId {
let (interrupt_tx, interrupt_rx) = oneshot::channel::<InterruptSignal>();
let mut cmd_ctx = CmdCtx::single_profile_single_flow(output, workspace, interrupt_rx)
.build();
let cmd_execution_id = EnsureCmd::exec_bg(cmd_ctx);
let cmd_execution_by_id = cmd_execution_by_id
.lock()
.await;
cmd_execution_by_id.insert(cmd_execution_id, interrupt_tx);
cmd_execution_id
}
/// Returns the progress of the `CmdExecution`.
async fn cmd_exec_progress_handler(cmd_execution_id: CmdExecutionId) -> Result<CmdProgress, E> {
self.cmd_progress_storage.get(cmd_execution_id).await
}
async fn cmd_exec_interrupt_handler(cmd_execution_id: CmdExecutionId) -> Result<(), E> {
let cmd_execution_by_id = cmd_execution_by_id
.lock()
.await;
if let Some(interrupt_tx) = cmd_execution_by_id.get(cmd_execution_id) {
let (Ok(()) | Err(InterruptSignal)) = interrupt_tx.send(InterruptSignal);
Ok(())
} else {
Err(E::from(Error::CmdExecutionIdNotFound { cmd_execution_id }))
}
}
}

CLI + Web Service

There are two variants of CLI and web service:

  1. CLI command running on the user's machine, web service that is a UI for that one command execution.
  2. CLI client to a web service, so the CLI is just a REST client.

CLI on User's Machine + Web UI

For the first variant, the CmdExecution invocation is similar to Web Service, with the following differences:

  • Output progress is pushed to both CLI and CmdProgress storage.
  • Interruptions are received from both process SIGINT and client requests.
#![allow(unused)]
fn main() {
async fn cmd_exec_start(params: Params) {
let (interrupt_tx, interrupt_rx) = oneshot::channel::<InterruptSignal>();
let mut cmd_ctx = CmdCtx::single_profile_single_flow(output, workspace, interrupt_rx)
.build();
let cmd_execution_id = EnsureCmd::exec_bg(cmd_ctx);
// We store an `interrupt_tx` per `CmdExecutionId`,
// as well as spawn a Ctrl C handler.
let cmd_execution_by_id = cmd_execution_by_id
.lock()
.await;
cmd_execution_by_id.insert(cmd_execution_id, interrupt_tx.clone());
tokio::task::spawn(async move {
tokio::signal::ctrl_c()
.await
.expect("Failed to initialize signal handler for SIGINT");
let (Ok(()) | Err(InterruptSignal)) = interrupt_tx.send(InterruptSignal);
});
// TODO: store `cmd_execution_id` as the only running `CmdExecution`.
}
/// Returns the progress of the `CmdExecution`.
async fn cmd_exec_progress_handler(cmd_execution_id: CmdExecutionId) -> Result<CmdProgress, E> {
self.cmd_progress_storage.get(cmd_execution_id).await
}
async fn cmd_exec_interrupt_handler(cmd_execution_id: CmdExecutionId) -> Result<(), E> {
let cmd_execution_by_id = cmd_execution_by_id
.lock()
.await;
if let Some(interrupt_tx) = cmd_execution_by_id.get(cmd_execution_id) {
let (Ok(()) | Err(InterruptSignal)) = interrupt_tx.send(InterruptSignal);
Ok(())
} else {
Err(E::from(Error::CmdExecutionIdNotFound { cmd_execution_id }))
}
}
}

CLI as Rest Client to Web Service

This is essentially the Web Service implementation, but rendering the progress on the machine with the CLI.