853 lines
16 KiB
Rust
853 lines
16 KiB
Rust
#![allow(clippy::unwrap_used)]
|
|
|
|
use std::{
|
|
num::NonZeroI64,
|
|
process::{ExitStatus, Output},
|
|
sync::{
|
|
atomic::{AtomicBool, Ordering},
|
|
Arc, Mutex,
|
|
},
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use tokio::time::sleep;
|
|
use watchexec_events::ProcessEnd;
|
|
|
|
#[cfg(unix)]
|
|
use crate::job::TestChildCall;
|
|
use crate::{
|
|
command::{Command, Program},
|
|
job::{start_job, CommandState},
|
|
};
|
|
|
|
use super::{Control, Job, Priority, TestChild};
|
|
|
|
const GRACE: u64 = 10; // millis
|
|
|
|
fn erroring_command() -> Arc<Command> {
|
|
Arc::new(Command {
|
|
program: Program::Exec {
|
|
prog: "/does/not/exist".into(),
|
|
args: Vec::new(),
|
|
},
|
|
options: Default::default(),
|
|
})
|
|
}
|
|
|
|
fn working_command() -> Arc<Command> {
|
|
Arc::new(Command {
|
|
program: Program::Exec {
|
|
prog: "/does/not/run".into(),
|
|
args: Vec::new(),
|
|
},
|
|
options: Default::default(),
|
|
})
|
|
}
|
|
|
|
fn ungraceful_command() -> Arc<Command> {
|
|
Arc::new(Command {
|
|
program: Program::Exec {
|
|
prog: "sleep".into(),
|
|
args: vec![(GRACE * 2).to_string()],
|
|
},
|
|
options: Default::default(),
|
|
})
|
|
}
|
|
|
|
fn graceful_command() -> Arc<Command> {
|
|
Arc::new(Command {
|
|
program: Program::Exec {
|
|
prog: "sleep".into(),
|
|
args: vec![(2 * GRACE / 3).to_string()],
|
|
},
|
|
options: Default::default(),
|
|
})
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sync_error_handler() {
|
|
let (job, task) = start_job(erroring_command());
|
|
let error_handler_called = Arc::new(AtomicBool::new(false));
|
|
|
|
job.set_error_handler({
|
|
let error_handler_called = error_handler_called.clone();
|
|
move |_| {
|
|
error_handler_called.store(true, Ordering::Relaxed);
|
|
}
|
|
})
|
|
.await;
|
|
|
|
job.start().await;
|
|
|
|
assert!(
|
|
error_handler_called.load(Ordering::Relaxed),
|
|
"called on start"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn async_error_handler() {
|
|
let (job, task) = start_job(erroring_command());
|
|
let error_handler_called = Arc::new(AtomicBool::new(false));
|
|
|
|
job.set_async_error_handler({
|
|
let error_handler_called = error_handler_called.clone();
|
|
move |_| {
|
|
let error_handler_called = error_handler_called.clone();
|
|
Box::new(async move {
|
|
error_handler_called.store(true, Ordering::Relaxed);
|
|
})
|
|
}
|
|
})
|
|
.await;
|
|
|
|
job.start().await;
|
|
|
|
assert!(
|
|
error_handler_called.load(Ordering::Relaxed),
|
|
"called on start"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unset_error_handler() {
|
|
let (job, task) = start_job(erroring_command());
|
|
let error_handler_called = Arc::new(AtomicBool::new(false));
|
|
|
|
job.set_error_handler({
|
|
let error_handler_called = error_handler_called.clone();
|
|
move |_| {
|
|
error_handler_called.store(true, Ordering::Relaxed);
|
|
}
|
|
})
|
|
.await;
|
|
|
|
job.unset_error_handler().await;
|
|
|
|
job.start().await;
|
|
|
|
assert!(
|
|
!error_handler_called.load(Ordering::Relaxed),
|
|
"not called even after start"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn queue_ordering() {
|
|
let (job, task) = start_job(working_command());
|
|
let error_handler_called = Arc::new(AtomicBool::new(false));
|
|
|
|
job.set_error_handler({
|
|
let error_handler_called = error_handler_called.clone();
|
|
move |_| {
|
|
error_handler_called.store(true, Ordering::Relaxed);
|
|
}
|
|
});
|
|
|
|
job.unset_error_handler();
|
|
|
|
// We're not awaiting until this one, but because the queue is processed in
|
|
// order, it's effectively the same as waiting them all.
|
|
job.start().await;
|
|
|
|
assert!(
|
|
!error_handler_called.load(Ordering::Relaxed),
|
|
"called after queue await"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn sync_func() {
|
|
let (job, task) = start_job(working_command());
|
|
let func_called = Arc::new(AtomicBool::new(false));
|
|
|
|
let ticket = job.run({
|
|
let func_called = func_called.clone();
|
|
move |_| {
|
|
func_called.store(true, Ordering::Relaxed);
|
|
}
|
|
});
|
|
|
|
assert!(
|
|
!func_called.load(Ordering::Relaxed),
|
|
"immediately after submit, likely before processed"
|
|
);
|
|
|
|
ticket.await;
|
|
assert!(
|
|
func_called.load(Ordering::Relaxed),
|
|
"after it's been processed"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn async_func() {
|
|
let (job, task) = start_job(working_command());
|
|
let func_called = Arc::new(AtomicBool::new(false));
|
|
|
|
let ticket = job.run_async({
|
|
let func_called = func_called.clone();
|
|
move |_| {
|
|
let func_called = func_called.clone();
|
|
Box::new(async move {
|
|
func_called.store(true, Ordering::Relaxed);
|
|
})
|
|
}
|
|
});
|
|
|
|
assert!(
|
|
!func_called.load(Ordering::Relaxed),
|
|
"immediately after submit, likely before processed"
|
|
);
|
|
|
|
ticket.await;
|
|
assert!(
|
|
func_called.load(Ordering::Relaxed),
|
|
"after it's been processed"
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
// TODO: figure out how to test spawn hooks
|
|
|
|
async fn refresh_state(job: &Job, state: &Arc<Mutex<Option<CommandState>>>, current: bool) {
|
|
job.send_controls(
|
|
[Control::SyncFunc(Box::new({
|
|
let state = state.clone();
|
|
move |context| {
|
|
if current {
|
|
state.lock().unwrap().replace(context.current.clone());
|
|
} else {
|
|
*state.lock().unwrap() = context.previous.cloned();
|
|
}
|
|
}
|
|
}))],
|
|
Priority::Urgent,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
async fn set_running_child_status(job: &Job, status: ExitStatus) {
|
|
job.send_controls(
|
|
[Control::AsyncFunc(Box::new({
|
|
move |context| {
|
|
let output_lock = if let CommandState::Running { child, .. } = context.current {
|
|
Some(child.output.clone())
|
|
} else {
|
|
None
|
|
};
|
|
|
|
Box::new(async move {
|
|
if let Some(output_lock) = output_lock {
|
|
*output_lock.lock().await = Some(Output {
|
|
status,
|
|
stdout: Vec::new(),
|
|
stderr: Vec::new(),
|
|
});
|
|
}
|
|
})
|
|
}
|
|
}))],
|
|
Priority::Urgent,
|
|
)
|
|
.await;
|
|
}
|
|
|
|
macro_rules! expect_state {
|
|
($current:literal, $job:expr, $expected:pat, $reason:literal) => {
|
|
let state = Arc::new(Mutex::new(None));
|
|
refresh_state(&$job, &state, $current).await;
|
|
{
|
|
let state = state.lock().unwrap();
|
|
let reason = $reason;
|
|
let reason = if reason.is_empty() {
|
|
String::new()
|
|
} else {
|
|
format!(" ({reason})")
|
|
};
|
|
assert!(
|
|
matches!(*state, Some($expected)),
|
|
"expected Some({}), got {state:?}{reason}",
|
|
stringify!($expected),
|
|
);
|
|
}
|
|
};
|
|
|
|
($job:expr, $expected:pat, $reason:literal) => {
|
|
expect_state!(true, $job, $expected, $reason)
|
|
};
|
|
|
|
($job:expr, $expected:pat) => {
|
|
expect_state!(true, $job, $expected, "")
|
|
};
|
|
|
|
(previous: $job:expr, $expected:pat, $reason:literal) => {
|
|
expect_state!(false, $job, $expected, $reason)
|
|
};
|
|
|
|
(previous: $job:expr, $expected:pat) => {
|
|
expect_state!(false, $job, $expected, "")
|
|
};
|
|
}
|
|
|
|
async fn get_child(job: &Job) -> TestChild {
|
|
let state = Arc::new(Mutex::new(None));
|
|
refresh_state(job, &state, true).await;
|
|
let state = state.lock().unwrap();
|
|
let state = state.as_ref().expect("no state");
|
|
match state {
|
|
CommandState::Running { ref child, .. } => child.clone(),
|
|
_ => panic!("get_child: expected IsRunning, got {state:?}"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn start() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[tokio::test]
|
|
async fn signal_unix() {
|
|
use nix::sys::signal::Signal;
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start();
|
|
job.signal(watchexec_signals::Signal::User1).await;
|
|
|
|
let calls = get_child(&job).await.calls;
|
|
assert!(calls.iter().any(
|
|
|(_, call)| matches!(call, TestChildCall::Signal(sig) if *sig == Signal::SIGUSR1 as i32)
|
|
));
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_when_running() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn stop_fail() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn restart() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
job.restart().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn graceful_stop() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
let stop = job.stop_with_signal(
|
|
watchexec_signals::Signal::Terminate,
|
|
Duration::from_millis(GRACE),
|
|
);
|
|
|
|
sleep(Duration::from_millis(GRACE / 2)).await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished { .. },
|
|
"after signal but before delayed force-stop"
|
|
);
|
|
|
|
stop.await;
|
|
|
|
expect_state!(job, CommandState::Finished { .. });
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn graceful_restart() {
|
|
let (job, task) = start_job(working_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
job.restart_with_signal(
|
|
watchexec_signals::Signal::Terminate,
|
|
Duration::from_millis(GRACE),
|
|
)
|
|
.await;
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn graceful_stop_beyond_grace() {
|
|
let (job, task) = start_job(ungraceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
let stop = job.stop_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use nix::sys::signal::Signal;
|
|
expect_state!(
|
|
job,
|
|
CommandState::Running { .. },
|
|
"after USR1 but before delayed stop"
|
|
);
|
|
|
|
let calls = get_child(&job).await.calls;
|
|
assert!(calls.iter().any(|(_, call)| matches!(
|
|
call,
|
|
TestChildCall::Signal(sig) if *sig == Signal::SIGUSR1 as i32
|
|
)));
|
|
}
|
|
|
|
stop.await;
|
|
|
|
expect_state!(job, CommandState::Finished { .. });
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn graceful_restart_beyond_grace() {
|
|
let (job, task) = start_job(ungraceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
let restart = job.restart_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
);
|
|
|
|
#[cfg(unix)]
|
|
{
|
|
use nix::sys::signal::Signal;
|
|
expect_state!(
|
|
job,
|
|
CommandState::Running { .. },
|
|
"after USR1 but before delayed restart"
|
|
);
|
|
|
|
let calls = get_child(&job).await.calls;
|
|
assert!(calls.iter().any(|(_, call)| matches!(
|
|
call,
|
|
TestChildCall::Signal(sig) if *sig == Signal::SIGUSR1 as i32
|
|
)));
|
|
}
|
|
|
|
restart.await;
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_restart() {
|
|
let (job, task) = start_job(graceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.try_restart().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Pending,
|
|
"command still not running after try-restart"
|
|
);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
let try_restart = job.try_restart();
|
|
|
|
eprintln!("[{:?}] test: await try_restart", Instant::now());
|
|
try_restart.await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished { .. }
|
|
);
|
|
|
|
expect_state!(job, CommandState::Finished { .. });
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_graceful_restart() {
|
|
let (job, task) = start_job(graceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.try_restart_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
)
|
|
.await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Pending,
|
|
"command still not running after try-graceful-restart"
|
|
);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
let restart = job.try_restart_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
);
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
eprintln!("[{:?}] await restart", Instant::now());
|
|
restart.await;
|
|
eprintln!("[{:?}] awaited restart", Instant::now());
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_restart_beyond_grace() {
|
|
let (job, task) = start_job(ungraceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.try_restart().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Pending,
|
|
"command still not running after try-restart"
|
|
);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
job.try_restart().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_graceful_restart_beyond_grace() {
|
|
let (job, task) = start_job(ungraceful_command());
|
|
|
|
expect_state!(job, CommandState::Pending);
|
|
|
|
job.try_restart_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
)
|
|
.await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Pending,
|
|
"command still not running after try-graceful-restart"
|
|
);
|
|
|
|
job.start().await;
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(
|
|
&job,
|
|
ProcessEnd::ExitError(NonZeroI64::new(1).unwrap()).into_exitstatus(),
|
|
)
|
|
.await;
|
|
|
|
let restart = job.try_restart_with_signal(
|
|
watchexec_signals::Signal::User1,
|
|
Duration::from_millis(GRACE),
|
|
);
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
restart.await;
|
|
|
|
expect_state!(
|
|
previous: job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::ExitError(_),
|
|
..
|
|
}
|
|
);
|
|
|
|
expect_state!(job, CommandState::Running { .. });
|
|
|
|
set_running_child_status(&job, ProcessEnd::Success.into_exitstatus()).await;
|
|
|
|
job.stop().await;
|
|
|
|
expect_state!(
|
|
job,
|
|
CommandState::Finished {
|
|
status: ProcessEnd::Success,
|
|
..
|
|
}
|
|
);
|
|
|
|
task.abort();
|
|
}
|