| /* | |
| * 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}" | |
| )) | |
| } |