Interruptibility
How should the interrupt channel be initialized and stored?
Use Cases
When automation software is used as:
- CLI interactive / non-interactive.
- Web service + browser access.
- CLI + web service + browser access.
In all cases, we need to:
- Initialize the
CmdCtx
with theinterrupt_rx
- Spawn the listener that will send
InterruptSignal
ininterrupt_tx
.
Imagined Code
CLI Interactive / Non-interactive
Both interactive and non-interactive can listen for SIGINT
:
- Interactive:
SIGINT
will be sent by the user pressingCtrl + 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:
- CLI command running on the user's machine, web service that is a UI for that one command execution.
- 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.