Skip to main content

reqsign_command_execute_tokio/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Tokio-based command execution implementation for reqsign.
19//!
20//! This crate provides `TokioCommandExecute`, an async command executor that implements
21//! the `CommandExecute` trait from `reqsign_core` using Tokio's process operations.
22//!
23//! ## Overview
24//!
25//! `TokioCommandExecute` enables reqsign to execute external commands asynchronously using
26//! Tokio's process spawning capabilities. This is particularly useful when retrieving
27//! credentials from external programs or CLI tools.
28//!
29//! ## Example
30//!
31//! ```ignore
32//! use reqsign_core::Context;
33//! use reqsign_command_execute_tokio::TokioCommandExecute;
34//! use reqsign_file_read_tokio::TokioFileRead;
35//! use reqsign_http_send_reqwest::ReqwestHttpSend;
36//!
37//! #[tokio::main]
38//! async fn main() {
39//!     // Create a context with Tokio command executor
40//!     let ctx = Context::new()
41//!         .with_file_read(TokioFileRead::default())
42//!         .with_http_send(ReqwestHttpSend::default())
43//!         .with_command_execute(TokioCommandExecute::default())
44//!
45//!         .unwrap();
46//!
47//!     // The context can now execute commands asynchronously
48//!     match ctx.command_execute("echo", &["hello", "world"]).await {
49//!         Ok(output) => {
50//!             if output.success() {
51//!                 println!("Output: {}", String::from_utf8_lossy(&output.stdout));
52//!             }
53//!         }
54//!         Err(e) => eprintln!("Failed to execute command: {}", e),
55//!     }
56//! }
57//! ```
58//!
59//! ## Usage with Service Signers
60//!
61//! ```ignore
62//! use reqsign_core::{Context, Signer};
63//! use reqsign_command_execute_tokio::TokioCommandExecute;
64//! use reqsign_file_read_tokio::TokioFileRead;
65//! use reqsign_http_send_reqwest::ReqwestHttpSend;
66//!
67//! # async fn example() -> anyhow::Result<()> {
68//! // Cloud services that use external credential processes need command execution
69//! let ctx = Context::new()
70//!     .with_file_read(TokioFileRead::default())
71//!     .with_http_send(ReqwestHttpSend::default())
72//!     .with_command_execute(TokioCommandExecute::default())
73//!     ?;
74//!
75//! // Create a signer that can execute credential helper processes
76//! // let signer = Signer::new(ctx, credential_loader, request_builder);
77//! # Ok(())
78//! # }
79//! ```
80
81use async_trait::async_trait;
82use reqsign_core::{CommandExecute, CommandOutput, Error, Result};
83use std::process::Stdio;
84use tokio::process::Command;
85
86/// Tokio-based implementation of the `CommandExecute` trait.
87///
88/// This struct provides async command execution capabilities using Tokio's
89/// process spawning operations.
90#[derive(Debug, Clone, Copy, Default)]
91pub struct TokioCommandExecute;
92
93#[async_trait]
94impl CommandExecute for TokioCommandExecute {
95    async fn command_execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
96        let output = Command::new(program)
97            .args(args)
98            .stdout(Stdio::piped())
99            .stderr(Stdio::piped())
100            .output()
101            .await
102            .map_err(|e| {
103                Error::unexpected(format!("failed to execute command '{program}'")).with_source(e)
104            })?;
105
106        Ok(CommandOutput {
107            status: output.status.code().unwrap_or(-1),
108            stdout: output.stdout,
109            stderr: output.stderr,
110        })
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[tokio::test]
119    async fn test_successful_command() {
120        let executor = TokioCommandExecute;
121        let output = executor.command_execute("echo", &["hello"]).await.unwrap();
122
123        assert!(output.success());
124        assert_eq!(output.status, 0);
125        assert!(!output.stdout.is_empty());
126    }
127
128    #[tokio::test]
129    async fn test_failed_command() {
130        let executor = TokioCommandExecute;
131        let result = executor
132            .command_execute("nonexistent_command_xyz", &[])
133            .await;
134
135        assert!(result.is_err());
136    }
137
138    #[tokio::test]
139    async fn test_command_with_non_zero_exit() {
140        let executor = TokioCommandExecute;
141
142        // Use 'false' command which always exits with status 1
143        #[cfg(unix)]
144        let output = executor.command_execute("false", &[]).await.unwrap();
145
146        #[cfg(unix)]
147        {
148            assert!(!output.success());
149            assert_eq!(output.status, 1);
150        }
151    }
152}