blob: df4926bc2c4c22de528b42dbe52f6d71cc7012d0 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::{
PipeEvent, failed,
output::{Captured, Outcome, Termination},
tail,
};
use maka_agent_facts::shell_run::PipeStream;
use maka_agent_facts::tools::ToolError;
use std::{io, time::Duration};
use tokio_util::sync::CancellationToken;
pub(crate) async fn run(
plan: crate::Command,
timeout_ms: Option<u64>,
cancellation: CancellationToken,
observer: Option<tokio::sync::mpsc::Sender<PipeEvent>>,
) -> Result<Captured, ToolError> {
if cancellation.is_cancelled() {
return Err(failed("Shell cancelled before spawn"));
}
let plan = plan.prepare().await?;
if cancellation.is_cancelled() {
plan.abort().await?;
return Err(failed("Shell cancelled before spawn"));
}
let crate::pipe::Spawned {
mut child,
stdin,
stdout,
stderr,
} = crate::pipe::spawn_prepared(plan).await?;
drop(stdin);
if let Some(observer) = &observer {
let _ = observer.try_send(PipeEvent::Started);
}
let exited = CancellationToken::new();
let lifecycle = async {
let outcome = settle(&mut child, timeout_ms, cancellation).await;
exited.cancel();
outcome
};
let (outcome, stdout, stderr) = tokio::join!(
lifecycle,
tail::capture(
stdout,
exited.clone(),
observer.clone().map(|o| (o, PipeStream::Stdout))
),
tail::capture(
stderr,
exited.clone(),
observer.map(|o| (o, PipeStream::Stderr))
),
);
Ok(Captured::new(outcome?, stdout, stderr))
}
async fn settle(
child: &mut crate::pipe::Child,
timeout_ms: Option<u64>,
cancellation: CancellationToken,
) -> Result<Outcome, ToolError> {
let reason = tokio::select! {
biased;
result = child.wait() => return result.map(Outcome::Exited).map_err(unknown),
_ = cancellation.cancelled() => Termination::Cancelled,
_ = crate::timeout(timeout_ms) => Termination::Timeout,
};
#[cfg(unix)]
let signal_applied = {
let applied = child.interrupt().unwrap_or(false);
// The original unreaped root pins its group until the final signal.
tokio::time::sleep(Duration::from_secs(2)).await;
applied
};
child.terminate().map_err(unknown)?;
#[cfg(windows)]
let signal_applied = true;
tokio::time::timeout(Duration::from_secs(4), child.wait())
.await
.map_err(|_| {
ToolError::CleanupUnconfirmed("Shell process group/tree exit not confirmed".into())
})?
.map_err(unknown)?;
Ok(Outcome::Interrupted {
reason,
signal_applied,
})
}
fn unknown(error: io::Error) -> ToolError {
ToolError::CleanupUnconfirmed(format!(
"Shell process group/tree cleanup not confirmed: {error}"
))
}