ferrous_actions/actions/
exec.rs1use super::noop_stream;
2use super::push_line_splitter::PushLineSplitter;
3use crate::node;
4use crate::node::path::Path;
5use js_sys::{JsString, Object};
6use parking_lot::Mutex;
7use std::sync::Arc;
8use wasm_bindgen::closure::Closure;
9use wasm_bindgen::JsValue;
10
11#[derive(Debug, Clone, Copy)]
12enum StdioEnum {
13 Inherit,
14 Null,
15}
16
17#[derive(Debug, Clone, Copy)]
19pub struct Stdio {
20 inner: StdioEnum,
21}
22
23impl Stdio {
24 pub fn null() -> Stdio {
26 Stdio { inner: StdioEnum::Null }
27 }
28
29 pub fn inherit() -> Stdio {
32 Stdio {
33 inner: StdioEnum::Inherit,
34 }
35 }
36}
37
38struct StreamToLines {
40 splitter: Arc<Mutex<PushLineSplitter>>,
41 #[allow(clippy::type_complexity)]
42 callback: Arc<Box<dyn Fn(&str)>>,
43 closure: Closure<dyn Fn(JsValue)>,
44}
45
46impl StreamToLines {
47 #[allow(clippy::type_complexity)]
48 pub fn new(callback: Arc<Box<dyn Fn(&str)>>) -> StreamToLines {
49 let splitter: Arc<Mutex<PushLineSplitter>> = Arc::default();
50 let closure = {
51 let splitter = splitter.clone();
52 let callback = callback.clone();
53 Closure::new(move |data: JsValue| {
54 let data: js_sys::Uint8Array = data.into();
55 let mut splitter = splitter.lock();
56 let mut write_buffer = splitter.write_via_buffer(data.length() as usize);
57 data.copy_to(write_buffer.as_mut());
58 drop(write_buffer);
59 while let Some(line) = splitter.next_line() {
60 callback(&line);
61 }
62 })
63 };
64 StreamToLines {
65 splitter,
66 callback,
67 closure,
68 }
69 }
70}
71
72impl Drop for StreamToLines {
73 fn drop(&mut self) {
74 let mut splitter = self.splitter.lock();
75 splitter.close();
76 while let Some(line) = splitter.next_line() {
77 (self.callback)(&line);
78 }
79 }
80}
81
82impl AsRef<JsValue> for StreamToLines {
83 fn as_ref(&self) -> &JsValue {
84 self.closure.as_ref()
85 }
86}
87
88pub struct Command {
90 path: Path,
91 args: Vec<JsString>,
92 #[allow(clippy::type_complexity)]
93 outline: Option<Arc<Box<dyn Fn(&str)>>>,
94 #[allow(clippy::type_complexity)]
95 errline: Option<Arc<Box<dyn Fn(&str)>>>,
96 stdout: Stdio,
97 stderr: Stdio,
98 cwd: Path,
99}
100
101impl Command {
102 pub fn args<I, S>(&mut self, args: I) -> &mut Command
104 where
105 I: IntoIterator<Item = S>,
106 S: Into<JsString>,
107 {
108 self.args.extend(args.into_iter().map(Into::into));
109 self
110 }
111
112 pub fn arg<S: Into<JsString>>(&mut self, arg: S) -> &mut Command {
114 self.args(std::iter::once(arg.into()));
115 self
116 }
117
118 pub async fn exec(&mut self) -> Result<i32, JsValue> {
120 let command = self.path.to_string();
121 let command = Self::escape_command(command.as_str());
122 let command: JsString = command.into();
123 let args: Vec<JsString> = self.args.iter().map(JsString::to_string).collect();
124 let options = js_sys::Map::new();
125 let listeners = js_sys::Map::new();
126
127 let outline_adapter = self.outline.clone().map(StreamToLines::new);
128 if let Some(callback) = &outline_adapter {
129 listeners.set(&"stdout".into(), callback.as_ref());
130 }
131 let errline_adapter = self.errline.clone().map(StreamToLines::new);
132 if let Some(callback) = &errline_adapter {
133 listeners.set(&"stderr".into(), callback.as_ref());
134 }
135
136 options.set(&"cwd".into(), &self.cwd.to_js_string());
137 let sink = noop_stream::Sink::default();
138 if let StdioEnum::Null = self.stdout.inner {
139 options.set(&"outStream".into(), sink.as_ref());
140 }
141 if let StdioEnum::Null = self.stderr.inner {
142 options.set(&"errStream".into(), sink.as_ref());
143 }
144
145 let listeners = Object::from_entries(&listeners).expect("Failed to convert listeners map to object");
146 options.set(&"listeners".into(), &listeners);
147 let options = Object::from_entries(&options).expect("Failed to convert options map to object");
148 let result = ffi::exec(&command, Some(args), &options).await.map(|r| {
149 #[allow(clippy::cast_possible_truncation)]
150 let code = r.as_f64().expect("exec didn't return a number") as i32;
151 code
152 });
153
154 drop(outline_adapter);
156 drop(errline_adapter);
157 result
158 }
159
160 pub fn outline<F: Fn(&str) + 'static + Sync + Send>(&mut self, callback: F) -> &mut Command {
165 self.outline = Some(Arc::new(Box::new(callback)));
166 self
167 }
168
169 pub fn errline<F: Fn(&str) + 'static + Sync + Send>(&mut self, callback: F) -> &mut Command {
174 self.errline = Some(Arc::new(Box::new(callback)));
175 self
176 }
177
178 pub fn stdout(&mut self, redirect: Stdio) -> &mut Command {
180 self.stdout = redirect;
181 self
182 }
183
184 pub fn stderr(&mut self, redirect: Stdio) -> &mut Command {
186 self.stderr = redirect;
187 self
188 }
189
190 pub fn current_dir(&mut self, path: &Path) -> &mut Command {
192 self.cwd = path.clone();
193 self
194 }
195
196 fn escape_command(command: &str) -> String {
203 let mut result = String::with_capacity(command.len());
204 for c in command.chars() {
211 match c {
212 ' ' => result.push_str("\" \""),
213 '\"' => result.push_str("\"\\\""),
214 _ => result.push(c),
215 }
216 }
217 result
218 }
219}
220
221impl<'a> From<&'a Path> for Command {
222 fn from(path: &'a Path) -> Command {
224 Command {
225 path: path.clone(),
226 args: Vec::new(),
227 outline: None,
228 errline: None,
229 stdout: Stdio::inherit(),
230 stderr: Stdio::inherit(),
231 cwd: node::process::cwd(),
232 }
233 }
234}
235
236pub mod ffi {
238 use js_sys::JsString;
239 use wasm_bindgen::prelude::*;
240
241 #[wasm_bindgen(module = "@actions/exec")]
242 extern "C" {
243 #[wasm_bindgen(catch)]
244 pub async fn exec(
245 comand_line: &JsString,
246 args: Option<Vec<JsString>>,
247 options: &JsValue,
248 ) -> Result<JsValue, JsValue>;
249 }
250}