command-stream 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/js/src/$.ansi.mjs +147 -0
- package/js/src/$.mjs +49 -6382
- package/js/src/$.process-runner-base.mjs +563 -0
- package/js/src/$.process-runner-execution.mjs +1497 -0
- package/js/src/$.process-runner-orchestration.mjs +250 -0
- package/js/src/$.process-runner-pipeline.mjs +1162 -0
- package/js/src/$.process-runner-stream-kill.mjs +312 -0
- package/js/src/$.process-runner-virtual.mjs +297 -0
- package/js/src/$.quote.mjs +161 -0
- package/js/src/$.result.mjs +23 -0
- package/js/src/$.shell-settings.mjs +84 -0
- package/js/src/$.shell.mjs +157 -0
- package/js/src/$.state.mjs +401 -0
- package/js/src/$.stream-emitter.mjs +111 -0
- package/js/src/$.stream-utils.mjs +390 -0
- package/js/src/$.trace.mjs +36 -0
- package/js/src/$.utils.mjs +2 -23
- package/js/src/$.virtual-commands.mjs +113 -0
- package/js/src/commands/$.which.mjs +3 -1
- package/js/src/commands/index.mjs +24 -0
- package/js/src/shell-parser.mjs +125 -83
- package/js/tests/resource-cleanup-internals.test.mjs +22 -24
- package/js/tests/sigint-cleanup.test.mjs +3 -0
- package/package.json +1 -1
- package/rust/src/ansi.rs +194 -0
- package/rust/src/events.rs +305 -0
- package/rust/src/lib.rs +71 -60
- package/rust/src/macros.rs +165 -0
- package/rust/src/pipeline.rs +411 -0
- package/rust/src/quote.rs +161 -0
- package/rust/src/state.rs +333 -0
- package/rust/src/stream.rs +369 -0
- package/rust/src/trace.rs +152 -0
- package/rust/src/utils.rs +53 -158
- package/rust/tests/events.rs +207 -0
- package/rust/tests/macros.rs +77 -0
- package/rust/tests/pipeline.rs +93 -0
- package/rust/tests/state.rs +207 -0
- package/rust/tests/stream.rs +102 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
//! Global state management for command-stream
|
|
2
|
+
//!
|
|
3
|
+
//! This module handles signal handlers, process tracking, and cleanup,
|
|
4
|
+
//! similar to the JavaScript $.state.mjs module.
|
|
5
|
+
|
|
6
|
+
use std::collections::HashSet;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
use tokio::sync::RwLock;
|
|
10
|
+
|
|
11
|
+
use crate::trace::trace_lazy;
|
|
12
|
+
|
|
13
|
+
/// Shell settings for controlling execution behavior
|
|
14
|
+
#[derive(Debug, Clone, Default)]
|
|
15
|
+
pub struct ShellSettings {
|
|
16
|
+
/// Exit immediately if a command exits with non-zero status (set -e)
|
|
17
|
+
pub errexit: bool,
|
|
18
|
+
/// Print commands as they are executed (set -v)
|
|
19
|
+
pub verbose: bool,
|
|
20
|
+
/// Print trace of commands (set -x)
|
|
21
|
+
pub xtrace: bool,
|
|
22
|
+
/// Return value of a pipeline is the status of the last command to exit with non-zero (set -o pipefail)
|
|
23
|
+
pub pipefail: bool,
|
|
24
|
+
/// Treat unset variables as an error (set -u)
|
|
25
|
+
pub nounset: bool,
|
|
26
|
+
/// Disable filename globbing (set -f)
|
|
27
|
+
pub noglob: bool,
|
|
28
|
+
/// Export all variables (set -a)
|
|
29
|
+
pub allexport: bool,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl ShellSettings {
|
|
33
|
+
/// Create new shell settings with defaults
|
|
34
|
+
pub fn new() -> Self {
|
|
35
|
+
Self::default()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Reset all settings to their defaults
|
|
39
|
+
pub fn reset(&mut self) {
|
|
40
|
+
*self = Self::default();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Set a shell option by name
|
|
44
|
+
///
|
|
45
|
+
/// Supports both short flags (e, v, x, u, f, a) and long names
|
|
46
|
+
pub fn set(&mut self, option: &str, value: bool) {
|
|
47
|
+
match option {
|
|
48
|
+
"e" | "errexit" => self.errexit = value,
|
|
49
|
+
"v" | "verbose" => self.verbose = value,
|
|
50
|
+
"x" | "xtrace" => self.xtrace = value,
|
|
51
|
+
"u" | "nounset" => self.nounset = value,
|
|
52
|
+
"f" | "noglob" => self.noglob = value,
|
|
53
|
+
"a" | "allexport" => self.allexport = value,
|
|
54
|
+
"o pipefail" | "pipefail" => self.pipefail = value,
|
|
55
|
+
_ => {
|
|
56
|
+
trace_lazy("ShellSettings", || {
|
|
57
|
+
format!("Unknown shell option: {}", option)
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Enable a shell option
|
|
64
|
+
pub fn enable(&mut self, option: &str) {
|
|
65
|
+
self.set(option, true);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/// Disable a shell option
|
|
69
|
+
pub fn disable(&mut self, option: &str) {
|
|
70
|
+
self.set(option, false);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Global state for the command-stream library
|
|
75
|
+
pub struct GlobalState {
|
|
76
|
+
/// Current shell settings
|
|
77
|
+
shell_settings: RwLock<ShellSettings>,
|
|
78
|
+
/// Set of active process runner IDs
|
|
79
|
+
active_runners: RwLock<HashSet<u64>>,
|
|
80
|
+
/// Counter for generating runner IDs
|
|
81
|
+
next_runner_id: std::sync::atomic::AtomicU64,
|
|
82
|
+
/// Whether signal handlers are installed
|
|
83
|
+
signal_handlers_installed: AtomicBool,
|
|
84
|
+
/// Whether virtual commands are enabled
|
|
85
|
+
virtual_commands_enabled: AtomicBool,
|
|
86
|
+
/// Initial working directory
|
|
87
|
+
initial_cwd: RwLock<Option<std::path::PathBuf>>,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
impl Default for GlobalState {
|
|
91
|
+
fn default() -> Self {
|
|
92
|
+
Self::new()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
impl GlobalState {
|
|
97
|
+
/// Create a new global state
|
|
98
|
+
pub fn new() -> Self {
|
|
99
|
+
let initial_cwd = std::env::current_dir().ok();
|
|
100
|
+
|
|
101
|
+
GlobalState {
|
|
102
|
+
shell_settings: RwLock::new(ShellSettings::new()),
|
|
103
|
+
active_runners: RwLock::new(HashSet::new()),
|
|
104
|
+
next_runner_id: std::sync::atomic::AtomicU64::new(1),
|
|
105
|
+
signal_handlers_installed: AtomicBool::new(false),
|
|
106
|
+
virtual_commands_enabled: AtomicBool::new(true),
|
|
107
|
+
initial_cwd: RwLock::new(initial_cwd),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Get the current shell settings
|
|
112
|
+
pub async fn get_shell_settings(&self) -> ShellSettings {
|
|
113
|
+
self.shell_settings.read().await.clone()
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Set shell settings
|
|
117
|
+
pub async fn set_shell_settings(&self, settings: ShellSettings) {
|
|
118
|
+
*self.shell_settings.write().await = settings;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Modify shell settings with a closure
|
|
122
|
+
pub async fn with_shell_settings<F>(&self, f: F)
|
|
123
|
+
where
|
|
124
|
+
F: FnOnce(&mut ShellSettings),
|
|
125
|
+
{
|
|
126
|
+
let mut settings = self.shell_settings.write().await;
|
|
127
|
+
f(&mut settings);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Enable a shell option
|
|
131
|
+
pub async fn enable_shell_option(&self, option: &str) {
|
|
132
|
+
self.shell_settings.write().await.enable(option);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Disable a shell option
|
|
136
|
+
pub async fn disable_shell_option(&self, option: &str) {
|
|
137
|
+
self.shell_settings.write().await.disable(option);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Register a new active runner and return its ID
|
|
141
|
+
pub async fn register_runner(&self) -> u64 {
|
|
142
|
+
let id = self
|
|
143
|
+
.next_runner_id
|
|
144
|
+
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
145
|
+
self.active_runners.write().await.insert(id);
|
|
146
|
+
|
|
147
|
+
trace_lazy("GlobalState", || {
|
|
148
|
+
format!("Registered runner {}", id)
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
id
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Unregister an active runner
|
|
155
|
+
pub async fn unregister_runner(&self, id: u64) {
|
|
156
|
+
self.active_runners.write().await.remove(&id);
|
|
157
|
+
|
|
158
|
+
trace_lazy("GlobalState", || {
|
|
159
|
+
format!("Unregistered runner {}", id)
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// Get the count of active runners
|
|
164
|
+
pub async fn active_runner_count(&self) -> usize {
|
|
165
|
+
self.active_runners.read().await.len()
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Check if signal handlers are installed
|
|
169
|
+
pub fn are_signal_handlers_installed(&self) -> bool {
|
|
170
|
+
self.signal_handlers_installed.load(Ordering::SeqCst)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Mark signal handlers as installed
|
|
174
|
+
pub fn set_signal_handlers_installed(&self, installed: bool) {
|
|
175
|
+
self.signal_handlers_installed.store(installed, Ordering::SeqCst);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Check if virtual commands are enabled
|
|
179
|
+
pub fn are_virtual_commands_enabled(&self) -> bool {
|
|
180
|
+
self.virtual_commands_enabled.load(Ordering::SeqCst)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Enable virtual commands
|
|
184
|
+
pub fn enable_virtual_commands(&self) {
|
|
185
|
+
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
|
|
186
|
+
trace_lazy("GlobalState", || "Virtual commands enabled".to_string());
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/// Disable virtual commands
|
|
190
|
+
pub fn disable_virtual_commands(&self) {
|
|
191
|
+
self.virtual_commands_enabled.store(false, Ordering::SeqCst);
|
|
192
|
+
trace_lazy("GlobalState", || "Virtual commands disabled".to_string());
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Get the initial working directory
|
|
196
|
+
pub async fn get_initial_cwd(&self) -> Option<std::path::PathBuf> {
|
|
197
|
+
self.initial_cwd.read().await.clone()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/// Reset global state to defaults
|
|
201
|
+
pub async fn reset(&self) {
|
|
202
|
+
// Reset shell settings
|
|
203
|
+
*self.shell_settings.write().await = ShellSettings::new();
|
|
204
|
+
|
|
205
|
+
// Clear active runners
|
|
206
|
+
self.active_runners.write().await.clear();
|
|
207
|
+
|
|
208
|
+
// Reset virtual commands flag
|
|
209
|
+
self.virtual_commands_enabled.store(true, Ordering::SeqCst);
|
|
210
|
+
|
|
211
|
+
// Don't reset signal handlers installed flag - that's managed separately
|
|
212
|
+
|
|
213
|
+
trace_lazy("GlobalState", || "Global state reset completed".to_string());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// Restore working directory to initial
|
|
217
|
+
pub async fn restore_cwd(&self) -> std::io::Result<()> {
|
|
218
|
+
if let Some(ref initial) = *self.initial_cwd.read().await {
|
|
219
|
+
if initial.exists() {
|
|
220
|
+
std::env::set_current_dir(initial)?;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
Ok(())
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Global state singleton
|
|
228
|
+
static GLOBAL_STATE: std::sync::OnceLock<Arc<GlobalState>> = std::sync::OnceLock::new();
|
|
229
|
+
|
|
230
|
+
/// Get the global state instance
|
|
231
|
+
pub fn global_state() -> Arc<GlobalState> {
|
|
232
|
+
GLOBAL_STATE
|
|
233
|
+
.get_or_init(|| Arc::new(GlobalState::new()))
|
|
234
|
+
.clone()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// Reset the global state (for testing)
|
|
238
|
+
pub async fn reset_global_state() {
|
|
239
|
+
global_state().reset().await;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/// Get current shell settings
|
|
243
|
+
pub async fn get_shell_settings() -> ShellSettings {
|
|
244
|
+
global_state().get_shell_settings().await
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/// Enable a shell option globally
|
|
248
|
+
pub async fn set_shell_option(option: &str) {
|
|
249
|
+
global_state().enable_shell_option(option).await;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// Disable a shell option globally
|
|
253
|
+
pub async fn unset_shell_option(option: &str) {
|
|
254
|
+
global_state().disable_shell_option(option).await;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[cfg(test)]
|
|
258
|
+
mod tests {
|
|
259
|
+
use super::*;
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn test_shell_settings_default() {
|
|
263
|
+
let settings = ShellSettings::new();
|
|
264
|
+
assert!(!settings.errexit);
|
|
265
|
+
assert!(!settings.verbose);
|
|
266
|
+
assert!(!settings.xtrace);
|
|
267
|
+
assert!(!settings.pipefail);
|
|
268
|
+
assert!(!settings.nounset);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
#[test]
|
|
272
|
+
fn test_shell_settings_set() {
|
|
273
|
+
let mut settings = ShellSettings::new();
|
|
274
|
+
|
|
275
|
+
settings.set("e", true);
|
|
276
|
+
assert!(settings.errexit);
|
|
277
|
+
|
|
278
|
+
settings.set("errexit", false);
|
|
279
|
+
assert!(!settings.errexit);
|
|
280
|
+
|
|
281
|
+
settings.set("o pipefail", true);
|
|
282
|
+
assert!(settings.pipefail);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[tokio::test]
|
|
286
|
+
async fn test_global_state_runners() {
|
|
287
|
+
let state = GlobalState::new();
|
|
288
|
+
|
|
289
|
+
let id1 = state.register_runner().await;
|
|
290
|
+
let id2 = state.register_runner().await;
|
|
291
|
+
|
|
292
|
+
assert_eq!(state.active_runner_count().await, 2);
|
|
293
|
+
assert!(id1 != id2);
|
|
294
|
+
|
|
295
|
+
state.unregister_runner(id1).await;
|
|
296
|
+
assert_eq!(state.active_runner_count().await, 1);
|
|
297
|
+
|
|
298
|
+
state.unregister_runner(id2).await;
|
|
299
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[tokio::test]
|
|
303
|
+
async fn test_global_state_virtual_commands() {
|
|
304
|
+
let state = GlobalState::new();
|
|
305
|
+
|
|
306
|
+
assert!(state.are_virtual_commands_enabled());
|
|
307
|
+
|
|
308
|
+
state.disable_virtual_commands();
|
|
309
|
+
assert!(!state.are_virtual_commands_enabled());
|
|
310
|
+
|
|
311
|
+
state.enable_virtual_commands();
|
|
312
|
+
assert!(state.are_virtual_commands_enabled());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#[tokio::test]
|
|
316
|
+
async fn test_global_state_reset() {
|
|
317
|
+
let state = GlobalState::new();
|
|
318
|
+
|
|
319
|
+
// Modify state
|
|
320
|
+
state.enable_shell_option("errexit").await;
|
|
321
|
+
state.register_runner().await;
|
|
322
|
+
state.disable_virtual_commands();
|
|
323
|
+
|
|
324
|
+
// Reset
|
|
325
|
+
state.reset().await;
|
|
326
|
+
|
|
327
|
+
// Verify reset
|
|
328
|
+
let settings = state.get_shell_settings().await;
|
|
329
|
+
assert!(!settings.errexit);
|
|
330
|
+
assert_eq!(state.active_runner_count().await, 0);
|
|
331
|
+
assert!(state.are_virtual_commands_enabled());
|
|
332
|
+
}
|
|
333
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
//! Streaming and async iteration support
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides async streaming capabilities similar to JavaScript's
|
|
4
|
+
//! async iterators and stream handling in `$.stream-utils.mjs`.
|
|
5
|
+
//!
|
|
6
|
+
//! ## Usage
|
|
7
|
+
//!
|
|
8
|
+
//! ```rust,no_run
|
|
9
|
+
//! use command_stream::{StreamingRunner, OutputChunk};
|
|
10
|
+
//!
|
|
11
|
+
//! #[tokio::main]
|
|
12
|
+
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
13
|
+
//! let runner = StreamingRunner::new("yes hello");
|
|
14
|
+
//!
|
|
15
|
+
//! // Stream output as it arrives
|
|
16
|
+
//! let mut stream = runner.stream();
|
|
17
|
+
//! let mut count = 0;
|
|
18
|
+
//! while let Some(chunk) = stream.next().await {
|
|
19
|
+
//! match chunk {
|
|
20
|
+
//! OutputChunk::Stdout(data) => {
|
|
21
|
+
//! print!("{}", String::from_utf8_lossy(&data));
|
|
22
|
+
//! count += 1;
|
|
23
|
+
//! if count >= 5 {
|
|
24
|
+
//! break;
|
|
25
|
+
//! }
|
|
26
|
+
//! }
|
|
27
|
+
//! OutputChunk::Stderr(data) => {
|
|
28
|
+
//! eprint!("{}", String::from_utf8_lossy(&data));
|
|
29
|
+
//! }
|
|
30
|
+
//! OutputChunk::Exit(code) => {
|
|
31
|
+
//! println!("Process exited with code: {}", code);
|
|
32
|
+
//! break;
|
|
33
|
+
//! }
|
|
34
|
+
//! }
|
|
35
|
+
//! }
|
|
36
|
+
//!
|
|
37
|
+
//! Ok(())
|
|
38
|
+
//! }
|
|
39
|
+
//! ```
|
|
40
|
+
|
|
41
|
+
use std::collections::HashMap;
|
|
42
|
+
use std::path::PathBuf;
|
|
43
|
+
use std::process::Stdio;
|
|
44
|
+
use tokio::io::BufReader;
|
|
45
|
+
use tokio::process::Command;
|
|
46
|
+
use tokio::sync::mpsc;
|
|
47
|
+
|
|
48
|
+
use crate::trace::trace_lazy;
|
|
49
|
+
use crate::{CommandResult, Result};
|
|
50
|
+
|
|
51
|
+
/// A chunk of output from a streaming process
|
|
52
|
+
#[derive(Debug, Clone)]
|
|
53
|
+
pub enum OutputChunk {
|
|
54
|
+
/// Stdout data
|
|
55
|
+
Stdout(Vec<u8>),
|
|
56
|
+
/// Stderr data
|
|
57
|
+
Stderr(Vec<u8>),
|
|
58
|
+
/// Process exit code
|
|
59
|
+
Exit(i32),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/// A streaming process runner that allows async iteration over output
|
|
63
|
+
pub struct StreamingRunner {
|
|
64
|
+
command: String,
|
|
65
|
+
cwd: Option<PathBuf>,
|
|
66
|
+
env: Option<HashMap<String, String>>,
|
|
67
|
+
stdin_content: Option<String>,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
impl StreamingRunner {
|
|
71
|
+
/// Create a new streaming runner
|
|
72
|
+
pub fn new(command: impl Into<String>) -> Self {
|
|
73
|
+
StreamingRunner {
|
|
74
|
+
command: command.into(),
|
|
75
|
+
cwd: None,
|
|
76
|
+
env: None,
|
|
77
|
+
stdin_content: None,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Set the working directory
|
|
82
|
+
pub fn cwd(mut self, path: impl Into<PathBuf>) -> Self {
|
|
83
|
+
self.cwd = Some(path.into());
|
|
84
|
+
self
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Set environment variables
|
|
88
|
+
pub fn env(mut self, env: HashMap<String, String>) -> Self {
|
|
89
|
+
self.env = Some(env);
|
|
90
|
+
self
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Set stdin content
|
|
94
|
+
pub fn stdin(mut self, content: impl Into<String>) -> Self {
|
|
95
|
+
self.stdin_content = Some(content.into());
|
|
96
|
+
self
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Start the process and return a stream of output chunks
|
|
100
|
+
pub fn stream(mut self) -> OutputStream {
|
|
101
|
+
let (tx, rx) = mpsc::channel(1024);
|
|
102
|
+
|
|
103
|
+
// Spawn the process handling task
|
|
104
|
+
let command = self.command.clone();
|
|
105
|
+
let cwd = self.cwd.take();
|
|
106
|
+
let env = self.env.take();
|
|
107
|
+
let stdin_content = self.stdin_content.take();
|
|
108
|
+
|
|
109
|
+
tokio::spawn(async move {
|
|
110
|
+
if let Err(e) = run_streaming_process(command, cwd, env, stdin_content, tx.clone()).await {
|
|
111
|
+
trace_lazy("StreamingRunner", || format!("Error: {}", e));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
OutputStream { rx }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Run to completion and collect all output
|
|
119
|
+
pub async fn collect(self) -> Result<CommandResult> {
|
|
120
|
+
let mut stdout = Vec::new();
|
|
121
|
+
let mut stderr = Vec::new();
|
|
122
|
+
let mut exit_code = 0;
|
|
123
|
+
|
|
124
|
+
let mut stream = self.stream();
|
|
125
|
+
while let Some(chunk) = stream.rx.recv().await {
|
|
126
|
+
match chunk {
|
|
127
|
+
OutputChunk::Stdout(data) => stdout.extend(data),
|
|
128
|
+
OutputChunk::Stderr(data) => stderr.extend(data),
|
|
129
|
+
OutputChunk::Exit(code) => exit_code = code,
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Ok(CommandResult {
|
|
134
|
+
stdout: String::from_utf8_lossy(&stdout).to_string(),
|
|
135
|
+
stderr: String::from_utf8_lossy(&stderr).to_string(),
|
|
136
|
+
code: exit_code,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/// Stream of output chunks from a process
|
|
142
|
+
pub struct OutputStream {
|
|
143
|
+
rx: mpsc::Receiver<OutputChunk>,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
impl OutputStream {
|
|
147
|
+
/// Receive the next chunk
|
|
148
|
+
pub async fn next(&mut self) -> Option<OutputChunk> {
|
|
149
|
+
self.rx.recv().await
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Collect all remaining output into vectors
|
|
153
|
+
pub async fn collect(mut self) -> (Vec<u8>, Vec<u8>, i32) {
|
|
154
|
+
let mut stdout = Vec::new();
|
|
155
|
+
let mut stderr = Vec::new();
|
|
156
|
+
let mut exit_code = 0;
|
|
157
|
+
|
|
158
|
+
while let Some(chunk) = self.rx.recv().await {
|
|
159
|
+
match chunk {
|
|
160
|
+
OutputChunk::Stdout(data) => stdout.extend(data),
|
|
161
|
+
OutputChunk::Stderr(data) => stderr.extend(data),
|
|
162
|
+
OutputChunk::Exit(code) => exit_code = code,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
(stdout, stderr, exit_code)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Collect stdout only, discarding stderr
|
|
170
|
+
pub async fn collect_stdout(mut self) -> Vec<u8> {
|
|
171
|
+
let mut stdout = Vec::new();
|
|
172
|
+
|
|
173
|
+
while let Some(chunk) = self.rx.recv().await {
|
|
174
|
+
if let OutputChunk::Stdout(data) = chunk {
|
|
175
|
+
stdout.extend(data);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stdout
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// Run a streaming process and send output to the channel
|
|
184
|
+
async fn run_streaming_process(
|
|
185
|
+
command: String,
|
|
186
|
+
cwd: Option<PathBuf>,
|
|
187
|
+
env: Option<HashMap<String, String>>,
|
|
188
|
+
stdin_content: Option<String>,
|
|
189
|
+
tx: mpsc::Sender<OutputChunk>,
|
|
190
|
+
) -> Result<()> {
|
|
191
|
+
trace_lazy("StreamingRunner", || format!("Starting: {}", command));
|
|
192
|
+
|
|
193
|
+
let shell = find_available_shell();
|
|
194
|
+
let mut cmd = Command::new(&shell.cmd);
|
|
195
|
+
for arg in &shell.args {
|
|
196
|
+
cmd.arg(arg);
|
|
197
|
+
}
|
|
198
|
+
cmd.arg(&command);
|
|
199
|
+
|
|
200
|
+
// Configure stdio
|
|
201
|
+
if stdin_content.is_some() {
|
|
202
|
+
cmd.stdin(Stdio::piped());
|
|
203
|
+
} else {
|
|
204
|
+
cmd.stdin(Stdio::null());
|
|
205
|
+
}
|
|
206
|
+
cmd.stdout(Stdio::piped());
|
|
207
|
+
cmd.stderr(Stdio::piped());
|
|
208
|
+
|
|
209
|
+
// Set working directory
|
|
210
|
+
if let Some(ref cwd) = cwd {
|
|
211
|
+
cmd.current_dir(cwd);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Set environment
|
|
215
|
+
if let Some(ref env_vars) = env {
|
|
216
|
+
for (key, value) in env_vars {
|
|
217
|
+
cmd.env(key, value);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Spawn the process
|
|
222
|
+
let mut child = cmd.spawn()?;
|
|
223
|
+
|
|
224
|
+
// Write stdin if needed
|
|
225
|
+
if let Some(content) = stdin_content {
|
|
226
|
+
if let Some(mut stdin) = child.stdin.take() {
|
|
227
|
+
use tokio::io::AsyncWriteExt;
|
|
228
|
+
let _ = stdin.write_all(content.as_bytes()).await;
|
|
229
|
+
let _ = stdin.shutdown().await;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Spawn stdout reader
|
|
234
|
+
let stdout = child.stdout.take();
|
|
235
|
+
let tx_stdout = tx.clone();
|
|
236
|
+
let stdout_handle = if let Some(stdout) = stdout {
|
|
237
|
+
Some(tokio::spawn(async move {
|
|
238
|
+
let mut reader = BufReader::new(stdout);
|
|
239
|
+
let mut buf = vec![0u8; 8192];
|
|
240
|
+
loop {
|
|
241
|
+
use tokio::io::AsyncReadExt;
|
|
242
|
+
match reader.read(&mut buf).await {
|
|
243
|
+
Ok(0) => break,
|
|
244
|
+
Ok(n) => {
|
|
245
|
+
if tx_stdout.send(OutputChunk::Stdout(buf[..n].to_vec())).await.is_err() {
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
Err(_) => break,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}))
|
|
253
|
+
} else {
|
|
254
|
+
None
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// Spawn stderr reader
|
|
258
|
+
let stderr = child.stderr.take();
|
|
259
|
+
let tx_stderr = tx.clone();
|
|
260
|
+
let stderr_handle = if let Some(stderr) = stderr {
|
|
261
|
+
Some(tokio::spawn(async move {
|
|
262
|
+
let mut reader = BufReader::new(stderr);
|
|
263
|
+
let mut buf = vec![0u8; 8192];
|
|
264
|
+
loop {
|
|
265
|
+
use tokio::io::AsyncReadExt;
|
|
266
|
+
match reader.read(&mut buf).await {
|
|
267
|
+
Ok(0) => break,
|
|
268
|
+
Ok(n) => {
|
|
269
|
+
if tx_stderr.send(OutputChunk::Stderr(buf[..n].to_vec())).await.is_err() {
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
Err(_) => break,
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}))
|
|
277
|
+
} else {
|
|
278
|
+
None
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// Wait for readers to complete
|
|
282
|
+
if let Some(handle) = stdout_handle {
|
|
283
|
+
let _ = handle.await;
|
|
284
|
+
}
|
|
285
|
+
if let Some(handle) = stderr_handle {
|
|
286
|
+
let _ = handle.await;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Wait for process to exit
|
|
290
|
+
let status = child.wait().await?;
|
|
291
|
+
let code = status.code().unwrap_or(-1);
|
|
292
|
+
|
|
293
|
+
// Send exit code
|
|
294
|
+
let _ = tx.send(OutputChunk::Exit(code)).await;
|
|
295
|
+
|
|
296
|
+
trace_lazy("StreamingRunner", || format!("Exited with code: {}", code));
|
|
297
|
+
|
|
298
|
+
Ok(())
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/// Shell configuration
|
|
302
|
+
#[derive(Debug, Clone)]
|
|
303
|
+
struct ShellConfig {
|
|
304
|
+
cmd: String,
|
|
305
|
+
args: Vec<String>,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/// Find an available shell
|
|
309
|
+
fn find_available_shell() -> ShellConfig {
|
|
310
|
+
let is_windows = cfg!(windows);
|
|
311
|
+
|
|
312
|
+
if is_windows {
|
|
313
|
+
ShellConfig {
|
|
314
|
+
cmd: "cmd.exe".to_string(),
|
|
315
|
+
args: vec!["/c".to_string()],
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
let shells = [
|
|
319
|
+
("/bin/sh", "-c"),
|
|
320
|
+
("/usr/bin/sh", "-c"),
|
|
321
|
+
("/bin/bash", "-c"),
|
|
322
|
+
];
|
|
323
|
+
|
|
324
|
+
for (cmd, arg) in shells {
|
|
325
|
+
if std::path::Path::new(cmd).exists() {
|
|
326
|
+
return ShellConfig {
|
|
327
|
+
cmd: cmd.to_string(),
|
|
328
|
+
args: vec![arg.to_string()],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
ShellConfig {
|
|
334
|
+
cmd: "/bin/sh".to_string(),
|
|
335
|
+
args: vec!["-c".to_string()],
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/// Async iterator trait for output streams
|
|
341
|
+
#[async_trait::async_trait]
|
|
342
|
+
pub trait AsyncIterator {
|
|
343
|
+
type Item;
|
|
344
|
+
|
|
345
|
+
/// Get the next item from the iterator
|
|
346
|
+
async fn next(&mut self) -> Option<Self::Item>;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#[async_trait::async_trait]
|
|
350
|
+
impl AsyncIterator for OutputStream {
|
|
351
|
+
type Item = OutputChunk;
|
|
352
|
+
|
|
353
|
+
async fn next(&mut self) -> Option<Self::Item> {
|
|
354
|
+
self.rx.recv().await
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// Extension trait to convert ProcessRunner into a stream
|
|
359
|
+
pub trait IntoStream {
|
|
360
|
+
/// Convert into an output stream
|
|
361
|
+
fn into_stream(self) -> OutputStream;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
impl IntoStream for crate::ProcessRunner {
|
|
365
|
+
fn into_stream(self) -> OutputStream {
|
|
366
|
+
let streaming = StreamingRunner::new(self.command().to_string());
|
|
367
|
+
streaming.stream()
|
|
368
|
+
}
|
|
369
|
+
}
|