mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-04-25 23:00:03 +00:00
Make extraction cancellable for bin
and tar
based formats (#481)
Extraction wasn't cancellable by `cancel_on_user_sig_term` used in `entry` since it calls `block_in_place`. This PR adds cancellation support to it by adding a `static` variable `OnceCell` to `wait_on_cancellation_signal` so that once it returns `Ok(())`, all other calls to it after that point also returns `Ok(())` immediately. `StreamReadable`, which is used in cancellation process, then stores a boxed future of `wait_on_cancellation_signal` and polled it in `BufReader::fill_buf`. Note that for zip, the extraction process takes `File` instead of `StreamReadable` due to `io::Seek` requirement, so it cancelling during extraction for zip is still not possible. This PR also optimized `extract_bin` and `extract_zip` by using `StreamReadable::copy` introduced to this PR instead of `io::copy`, which allocates an internal buffer on stack, which imposes extra copy. It also fixed `StreamReadable::fill_buf` by ensuring that empty buffer is only returned on eof. * Make `wait_on_cancellation_signal` pub * Enable feature `parking_lot` of dep tokio * Mod `wait_on_cancellation_signal`: Use `OnceCell` internally to archive the effect that once call to it return `Ok(())`, all calls to it after that also returns `Ok(())`. * Impl `From<BinstallError>` for `io::Error` * Impl cancellation on user signal in `StreamReadable` * Fix err msg when cancelling during extraction in `ops::resolve` * Optimize: Impl & use `StreamReadable::copy` which is same as `io::copy` but does not allocate any internal buffer since `StreamReadable` is buffered. * Fix `next_stream`: Return non-empty bytes on `Ok(Some(bytes))` Signed-off-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
parent
fdc617d870
commit
aa6012baae
7 changed files with 109 additions and 20 deletions
|
@ -1,24 +1,26 @@
|
|||
use std::{
|
||||
cmp::min,
|
||||
io::{self, BufRead, Read},
|
||||
future::Future,
|
||||
io::{self, BufRead, Read, Write},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use bytes::{Buf, Bytes};
|
||||
use futures_util::stream::{Stream, StreamExt};
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::errors::BinstallError;
|
||||
use crate::{errors::BinstallError, helpers::signal::wait_on_cancellation_signal};
|
||||
|
||||
/// This wraps an AsyncIterator as a `Read`able.
|
||||
/// It must be used in non-async context only,
|
||||
/// meaning you have to use it with
|
||||
/// `tokio::task::{block_in_place, spawn_blocking}` or
|
||||
/// `std::thread::spawn`.
|
||||
#[derive(Debug)]
|
||||
pub struct StreamReadable<S> {
|
||||
stream: S,
|
||||
handle: Handle,
|
||||
bytes: Bytes,
|
||||
cancellation_future: Pin<Box<dyn Future<Output = Result<(), io::Error>> + Send>>,
|
||||
}
|
||||
|
||||
impl<S> StreamReadable<S> {
|
||||
|
@ -27,6 +29,39 @@ impl<S> StreamReadable<S> {
|
|||
stream,
|
||||
handle: Handle::current(),
|
||||
bytes: Bytes::new(),
|
||||
cancellation_future: Box::pin(wait_on_cancellation_signal()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> StreamReadable<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
BinstallError: From<E>,
|
||||
{
|
||||
/// Copies from `self` to `writer`.
|
||||
///
|
||||
/// Same as `io::copy` but does not allocate any internal buffer
|
||||
/// since `self` is buffered.
|
||||
pub(super) fn copy<W>(&mut self, mut writer: W) -> io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
self.copy_inner(&mut writer)
|
||||
}
|
||||
|
||||
fn copy_inner(&mut self, writer: &mut dyn Write) -> io::Result<()> {
|
||||
loop {
|
||||
let buf = self.fill_buf()?;
|
||||
if buf.is_empty() {
|
||||
// Eof
|
||||
break Ok(());
|
||||
}
|
||||
|
||||
writer.write_all(buf)?;
|
||||
|
||||
let n = buf.len();
|
||||
self.consume(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +91,27 @@ where
|
|||
Ok(n)
|
||||
}
|
||||
}
|
||||
|
||||
/// If `Ok(Some(bytes))` if returned, then `bytes.is_empty() == false`.
|
||||
async fn next_stream<S, E>(stream: &mut S) -> io::Result<Option<Bytes>>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
BinstallError: From<E>,
|
||||
{
|
||||
loop {
|
||||
let option = stream
|
||||
.next()
|
||||
.await
|
||||
.transpose()
|
||||
.map_err(BinstallError::from)?;
|
||||
|
||||
match option {
|
||||
Some(bytes) if bytes.is_empty() => continue,
|
||||
option => break Ok(option),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> BufRead for StreamReadable<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin,
|
||||
|
@ -65,13 +121,18 @@ where
|
|||
let bytes = &mut self.bytes;
|
||||
|
||||
if !bytes.has_remaining() {
|
||||
match self.handle.block_on(async { self.stream.next().await }) {
|
||||
Some(Ok(new_bytes)) => *bytes = new_bytes,
|
||||
Some(Err(e)) => {
|
||||
let e: BinstallError = e.into();
|
||||
return Err(io::Error::new(io::ErrorKind::Other, e));
|
||||
let option = self.handle.block_on(async {
|
||||
tokio::select! {
|
||||
res = next_stream(&mut self.stream) => res,
|
||||
res = self.cancellation_future.as_mut() => {
|
||||
Err(res.err().unwrap_or_else(|| io::Error::from(BinstallError::UserAbort)))
|
||||
},
|
||||
}
|
||||
None => (),
|
||||
})?;
|
||||
|
||||
if let Some(new_bytes) = option {
|
||||
// new_bytes are guaranteed to be non-empty.
|
||||
*bytes = new_bytes;
|
||||
}
|
||||
}
|
||||
Ok(&*bytes)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue