use crate::{child_process, io, sync_process};
use rong::function::{Optional, Rest};
use rong::*;
use rong_abort::AbortSignal;
use rong_buffer::Blob;
use rong_stream::{ReadableStream, WritableStream, WritableStreamDefaultWriter};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
fn type_error(message: impl Into<String>) -> HostError {
HostError::new(rong::error::E_TYPE, message).with_name("TypeError")
}
#[derive(Clone, Default)]
struct ShellDefaults {
cwd: Option<String>,
env: Option<HashMap<String, String>>,
throws: bool,
quiet: bool,
}
impl JSContextService for ShellDefaults {}
impl ShellDefaults {
fn ensure(ctx: &JSContext) -> Self {
if let Some(state) = ctx.get_service::<Self>() {
return state.clone();
}
let state = Self {
throws: true,
..Default::default()
};
ctx.set_service(state.clone());
state
}
}
#[derive(Clone)]
enum SpawnStdinMode {
AutoClose,
Pipe,
Payload(Vec<u8>),
}
#[derive(Clone, Copy, Eq, PartialEq)]
enum ProcessStreamMode {
Pipe,
Ignore,
Inherit,
}
#[derive(Clone)]
struct NormalizedSpawn {
cmd: Vec<String>,
cwd: Option<String>,
env: Option<HashMap<String, String>>,
shell: Option<bool>,
timeout: Option<u64>,
stdin: SpawnStdinMode,
stdout: ProcessStreamMode,
stderr: ProcessStreamMode,
kill_signal: Option<String>,
signal: Option<AbortSignal>,
on_exit: Option<JSFunc>,
}
#[derive(Clone)]
struct ShellResultData {
stdout: Vec<u8>,
stderr: Vec<u8>,
exit_code: Option<i32>,
}
#[js_export(clone)]
struct ShellCommand {
command: String,
cwd: Option<String>,
env: Option<HashMap<String, String>>,
throws: bool,
quiet: bool,
}
#[js_export(clone)]
struct ShellError {
message: String,
command: String,
exit_code: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
}
fn js_array_to_strings(array: &JSArray, label: &str) -> JSResult<Vec<String>> {
let mut values = Vec::with_capacity(array.len()? as usize);
for index in 0..array.len()? {
let value = array
.get_opt::<String>(index)?
.ok_or_else(|| type_error(format!("{label} must be an array of strings")))?;
values.push(value);
}
Ok(values)
}
fn js_value_to_command_vec(value: JSValue, label: &str) -> JSResult<Vec<String>> {
let Some(obj) = value.into_object() else {
return Err(type_error(format!("{label} must be an array of strings")).into());
};
let Some(array) = JSArray::from_object(obj) else {
return Err(type_error(format!("{label} must be an array of strings")).into());
};
js_array_to_strings(&array, label)
}
fn js_value_to_bytes(value: &JSValue, label: &str) -> JSResult<Option<Vec<u8>>> {
if value.is_null() || value.is_undefined() {
return Ok(None);
}
if let Some(obj) = value.clone().into_object() {
if let Some(typed_array) = AnyJSTypedArray::from_object(obj.clone()) {
let bytes = typed_array
.byte_view()
.ok_or_else(|| type_error(format!("{label} contains an invalid TypedArray")))?;
return Ok(Some(bytes.to_vec()));
}
if let Some(array_buffer) = JSArrayBuffer::from_object(obj) {
return Ok(Some(array_buffer.to_vec()));
}
}
let text = value.clone().to_rust::<String>().map_err(|_| {
type_error(format!(
"{label} must be a string, ArrayBuffer, or TypedArray"
))
})?;
Ok(Some(text.into_bytes()))
}
fn normalize_env_map(env_obj: &JSObject) -> JSResult<HashMap<String, String>> {
let mut next = HashMap::new();
for (key, value) in env_obj.entries_as::<String, JSValue>()? {
if value.is_null() || value.is_undefined() {
continue;
}
next.insert(key, value.to_string());
}
Ok(next)
}
fn parse_stream_mode(obj: &JSObject, key: &str) -> JSResult<ProcessStreamMode> {
if !obj.has_property(key)? {
return Ok(ProcessStreamMode::Pipe);
}
let value: JSValue = obj.get(key)?;
if value.is_null() || value.is_undefined() {
return Ok(ProcessStreamMode::Pipe);
}
let mode = value.to_rust::<String>().map_err(|_| {
type_error(format!(
"options.{key} must be \"pipe\", \"ignore\", or \"inherit\""
))
})?;
match mode.as_str() {
"pipe" => Ok(ProcessStreamMode::Pipe),
"ignore" => Ok(ProcessStreamMode::Ignore),
"inherit" => Ok(ProcessStreamMode::Inherit),
_ => Err(type_error(format!(
"options.{key} must be \"pipe\", \"ignore\", or \"inherit\""
))
.into()),
}
}
fn parse_spawn_options(
cmd_or_options: JSValue,
options: Optional<JSObject>,
) -> JSResult<NormalizedSpawn> {
let (cmd, opts_obj) = if let Some(obj) = cmd_or_options.clone().into_object() {
if let Some(array) = JSArray::from_object(obj.clone()) {
(js_array_to_strings(&array, "cmd")?, options.0)
} else {
let cmd_value: JSValue = obj.get("cmd")?;
(
js_value_to_command_vec(cmd_value, "options.cmd")?,
Some(obj),
)
}
} else {
return Err(type_error(
"Rong.spawn() expects a command array or an options object with cmd",
)
.into());
};
if cmd.is_empty() {
return Err(type_error("Rong.spawn() requires a non-empty command array").into());
}
let mut normalized = NormalizedSpawn {
cmd,
cwd: None,
env: None,
shell: None,
timeout: None,
stdin: SpawnStdinMode::AutoClose,
stdout: ProcessStreamMode::Pipe,
stderr: ProcessStreamMode::Pipe,
kill_signal: None,
signal: None,
on_exit: None,
};
if let Some(obj) = opts_obj {
if obj.has_property("cwd")? {
normalized.cwd = Some(
obj.get::<_, String>("cwd")
.map_err(|_| type_error("options.cwd must be a string"))?,
);
}
if obj.has_property("env")? {
let env_obj = obj
.get::<_, JSObject>("env")
.map_err(|_| type_error("options.env must be an object"))?;
normalized.env = Some(normalize_env_map(&env_obj)?);
}
if obj.has_property("shell")? {
normalized.shell = Some(
obj.get::<_, bool>("shell")
.map_err(|_| type_error("options.shell must be a boolean"))?,
);
}
if obj.has_property("timeout")? {
let timeout = obj
.get::<_, f64>("timeout")
.map_err(|_| type_error("options.timeout must be a non-negative number"))?;
if !timeout.is_finite() || timeout < 0.0 {
return Err(type_error("options.timeout must be a non-negative number").into());
}
normalized.timeout = Some(timeout as u64);
}
if obj.has_property("stdin")? {
let stdin_value: JSValue = obj.get("stdin")?;
if let Ok(mode) = stdin_value.clone().to_rust::<String>() {
match mode.as_str() {
"pipe" => normalized.stdin = SpawnStdinMode::Pipe,
"inherit" => {
return Err(type_error(
"Rong.spawn() does not support stdin: \"inherit\" yet",
)
.into());
}
_ => {
normalized.stdin = SpawnStdinMode::Payload(mode.into_bytes());
}
}
} else if let Some(bytes) = js_value_to_bytes(&stdin_value, "options.stdin")? {
normalized.stdin = SpawnStdinMode::Payload(bytes);
}
}
normalized.stdout = parse_stream_mode(&obj, "stdout")?;
normalized.stderr = parse_stream_mode(&obj, "stderr")?;
if obj.has_property("killSignal")? {
let signal_value: JSValue = obj.get("killSignal")?;
normalized.kill_signal = Some(signal_value.to_string());
}
if obj.has_property("signal")? {
normalized.signal = Some(
obj.get::<_, AbortSignal>("signal")
.map_err(|_| type_error("options.signal must be an AbortSignal"))?,
);
}
if obj.has_property("onExit")? {
normalized.on_exit = Some(
obj.get::<_, JSFunc>("onExit")
.map_err(|_| type_error("options.onExit must be a function"))?,
);
}
}
Ok(normalized)
}
fn build_native_spawn_options(
ctx: &JSContext,
options: &NormalizedSpawn,
) -> JSResult<Option<JSObject>> {
let obj = JSObject::new(ctx);
let mut has_any = false;
if let Some(cwd) = &options.cwd {
obj.set("cwd", cwd.clone())?;
has_any = true;
}
if let Some(env) = &options.env {
let env_obj = JSObject::new(ctx);
for (key, value) in env {
env_obj.set(key.as_str(), value.clone())?;
}
obj.set("env", env_obj)?;
has_any = true;
}
if let Some(shell) = options.shell {
obj.set("shell", shell)?;
has_any = true;
}
if let Some(timeout) = options.timeout {
obj.set("timeout", timeout as f64)?;
has_any = true;
}
Ok(has_any.then_some(obj))
}
fn vec_to_js_array(ctx: &JSContext, values: &[String]) -> JSResult<JSArray> {
let array = JSArray::new(ctx)?;
for (index, value) in values.iter().enumerate() {
array.set(index as u32, value.clone())?;
}
Ok(array)
}
fn bytes_to_uint8_array(ctx: &JSContext, bytes: Vec<u8>) -> JSResult<JSTypedArray<u8>> {
let len = bytes.len();
let buffer = JSArrayBuffer::from_bytes_owned(ctx, bytes)?;
JSTypedArray::<u8>::from_array_buffer(ctx, buffer, 0, Some(len))
}
fn define_hidden_value(obj: &JSObject, key: &str, value: JSValue) -> JSResult<()> {
obj.define_property(
key,
PropertyDescriptor::from_value(value)
.hidden()
.configurable()
.writable(),
)?;
Ok(())
}
async fn collect_stream_bytes(stream_obj: JSObject) -> JSResult<Vec<u8>> {
let ctx = stream_obj.context();
let reader = {
let stream = stream_obj.borrow::<ReadableStream>()?;
stream.get_reader()?
};
let mut out = Vec::new();
let result = async {
loop {
let chunk: JSObject = reader.read(ctx.clone()).await?;
let done = chunk.get::<_, bool>("done")?;
if done {
break;
}
let value = chunk.get::<_, JSValue>("value")?;
if let Some(bytes) = js_value_to_bytes(&value, "stream chunk")? {
out.extend_from_slice(&bytes);
}
}
Ok::<(), RongJSError>(())
}
.await;
let _ = reader.release_lock();
result?;
Ok(out)
}
fn make_lines_iterator(ctx: &JSContext, text: String) -> JSResult<JSObject> {
let normalized = text.replace('\r', "");
let mut lines: VecDeque<String> = normalized
.split('\n')
.map(|line| line.to_string())
.collect();
if matches!(lines.back(), Some(last) if last.is_empty()) {
lines.pop_back();
}
let queue = Rc::new(RefCell::new(lines));
let iter = JSObject::new(ctx);
let queue_for_next = queue.clone();
iter.set(
"next",
JSFunc::new(ctx, move |ctx: JSContext| {
let queue = queue_for_next.clone();
async move {
let result = JSObject::new(&ctx);
if let Some(line) = queue.borrow_mut().pop_front() {
result.set("done", false)?;
result.set("value", line)?;
} else {
result.set("done", true)?;
result.set("value", JSValue::undefined(&ctx))?;
}
Ok(result)
}
})?,
)?;
rong::install_async_iterator_symbol(ctx, &iter)?;
Ok(iter)
}
pub(crate) fn decorate_readable(stream_obj: &JSObject) -> JSResult<()> {
if stream_obj.has_property("__rongProcessReadable")? {
return Ok(());
}
let ctx = stream_obj.context();
define_hidden_value(
stream_obj,
"__rongProcessReadable",
JSValue::from_rust(&ctx, true),
)?;
let bytes_stream = stream_obj.clone();
define_hidden_value(
stream_obj,
"bytes",
JSFunc::new(&ctx, move |ctx: JSContext| {
let bytes_stream = bytes_stream.clone();
async move { bytes_to_uint8_array(&ctx, collect_stream_bytes(bytes_stream).await?) }
})?
.name("bytes")?
.into_js_value(&ctx),
)?;
let text_stream = stream_obj.clone();
define_hidden_value(
stream_obj,
"text",
JSFunc::new(&ctx, move || {
let text_stream = text_stream.clone();
async move {
let bytes = collect_stream_bytes(text_stream).await?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
})?
.name("text")?
.into_js_value(&ctx),
)?;
let json_stream = stream_obj.clone();
define_hidden_value(
stream_obj,
"json",
JSFunc::new(&ctx, move |ctx: JSContext| {
let json_stream = json_stream.clone();
async move {
let bytes = collect_stream_bytes(json_stream).await?;
let text = String::from_utf8_lossy(&bytes).to_string();
let json = ctx.global().get::<_, JSObject>("JSON")?;
let parse = json.get::<_, JSFunc>("parse")?;
parse.call::<_, JSValue>(Some(json), (text,))
}
})?
.name("json")?
.into_js_value(&ctx),
)?;
let blob_stream = stream_obj.clone();
define_hidden_value(
stream_obj,
"blob",
JSFunc::new(&ctx, move || {
let blob_stream = blob_stream.clone();
async move {
let bytes = collect_stream_bytes(blob_stream).await?;
Ok(Blob::from_parts(String::new(), bytes))
}
})?
.name("blob")?
.into_js_value(&ctx),
)?;
let lines_stream = stream_obj.clone();
define_hidden_value(
stream_obj,
"lines",
JSFunc::new(&ctx, move |ctx: JSContext| {
let lines_stream = lines_stream.clone();
async move {
let bytes = collect_stream_bytes(lines_stream).await?;
let text = String::from_utf8_lossy(&bytes).to_string();
make_lines_iterator(&ctx, text)
}
})?
.name("lines")?
.into_js_value(&ctx),
)?;
Ok(())
}
fn decorate_writable(stream_obj: &JSObject) -> JSResult<()> {
if stream_obj.has_property("__rongProcessWritable")? {
return Ok(());
}
let ctx = stream_obj.context();
define_hidden_value(
stream_obj,
"__rongProcessWritable",
JSValue::from_rust(&ctx, true),
)?;
let writer_slot: Rc<RefCell<Option<Rc<WritableStreamDefaultWriter>>>> =
Rc::new(RefCell::new(None));
let write_stream = stream_obj.clone();
let write_writer_slot = writer_slot.clone();
define_hidden_value(
stream_obj,
"write",
JSFunc::new(&ctx, move |value: JSValue| {
let write_stream = write_stream.clone();
let write_writer_slot = write_writer_slot.clone();
async move {
if write_writer_slot.borrow().is_none() {
let writer = {
let stream = write_stream.borrow::<WritableStream>()?;
stream.get_writer()?
};
*write_writer_slot.borrow_mut() = Some(Rc::new(writer));
}
let payload = js_value_to_bytes(&value, "stdin")?.ok_or_else(|| {
type_error("stdin must be a string, ArrayBuffer, or TypedArray")
})?;
let typed = bytes_to_uint8_array(&write_stream.context(), payload)?;
let writer = write_writer_slot.borrow().as_ref().cloned();
if let Some(writer) = writer {
writer
.write(typed.into_any().into_js_value(&write_stream.context()))
.await?;
}
Ok(write_stream.clone())
}
})?
.name("write")?
.into_js_value(&ctx),
)?;
define_hidden_value(
stream_obj,
"flush",
JSFunc::new(&ctx, || async move { Ok::<(), RongJSError>(()) })?
.name("flush")?
.into_js_value(&ctx),
)?;
let end_stream = stream_obj.clone();
let end_writer_slot = writer_slot;
define_hidden_value(
stream_obj,
"end",
JSFunc::new(&ctx, move || {
let end_stream = end_stream.clone();
let end_writer_slot = end_writer_slot.clone();
async move {
if end_writer_slot.borrow().is_none() {
let writer = {
let stream = end_stream.borrow::<WritableStream>()?;
stream.get_writer()?
};
*end_writer_slot.borrow_mut() = Some(Rc::new(writer));
}
let writer = end_writer_slot.borrow().as_ref().cloned();
if let Some(writer) = writer {
writer.close().await?;
}
Ok(())
}
})?
.name("end")?
.into_js_value(&ctx),
)?;
Ok(())
}
async fn drain_stream(stream_obj: JSObject, target: ProcessStreamMode) -> JSResult<()> {
let bytes = collect_stream_bytes(stream_obj).await?;
if target == ProcessStreamMode::Inherit && !bytes.is_empty() {
io::write_stdout_bytes_native(&bytes);
}
Ok(())
}
async fn drain_stream_to_stderr(stream_obj: JSObject, target: ProcessStreamMode) -> JSResult<()> {
let bytes = collect_stream_bytes(stream_obj).await?;
if target == ProcessStreamMode::Inherit && !bytes.is_empty() {
io::write_stderr_bytes_native(&bytes);
}
Ok(())
}
fn setup_abort(child_obj: &JSObject, signal: Option<AbortSignal>, kill_signal: Option<String>) {
let Some(signal) = signal else {
return;
};
let child_obj = child_obj.clone();
let signal_name = kill_signal.unwrap_or_else(|| "SIGTERM".to_string());
if signal.aborted() {
if let Ok(child) = child_obj.borrow::<child_process::ChildProcess>() {
let _ = child.kill(Optional(Some(signal_name)));
}
return;
}
let mut abort_rx = signal.subscribe();
rong::spawn_local(async move {
let _ = abort_rx.recv().await;
if let Ok(child) = child_obj.borrow::<child_process::ChildProcess>() {
let _ = child.kill(Optional(Some(signal_name)));
}
});
}
fn spawn(
ctx: JSContext,
cmd_or_options: JSValue,
options: Optional<JSObject>,
) -> JSResult<JSObject> {
let normalized = parse_spawn_options(cmd_or_options, options)?;
let native_options = build_native_spawn_options(&ctx, &normalized)?;
let args = vec_to_js_array(&ctx, &normalized.cmd[1..])?;
let child = child_process::spawn_native(
ctx.clone(),
normalized.cmd[0].clone(),
Optional(Some(args.into_js_value(&ctx))),
Optional(native_options),
)?;
if let Ok(stdin) = child.get::<_, JSObject>("stdin") {
match normalized.stdin.clone() {
SpawnStdinMode::Pipe => {
decorate_writable(&stdin)?;
}
SpawnStdinMode::AutoClose => {
let stdin_obj = stdin.clone();
rong::spawn_local(async move {
let _ = decorate_writable(&stdin_obj);
let end = stdin_obj.get::<_, JSFunc>("end");
if let Ok(end) = end {
let _ = end.call_async::<_, ()>(Some(stdin_obj.clone()), ()).await;
}
});
child.set("stdin", JSValue::null(&ctx))?;
}
SpawnStdinMode::Payload(payload) => {
let stdin_obj = stdin.clone();
let ctx_for_payload = ctx.clone();
rong::spawn_local(async move {
let _ = decorate_writable(&stdin_obj);
if let Ok(write) = stdin_obj.get::<_, JSFunc>("write") {
let typed = match bytes_to_uint8_array(&ctx_for_payload, payload) {
Ok(typed) => typed,
Err(_) => return,
};
let _ = write
.call_async::<_, JSObject>(Some(stdin_obj.clone()), (typed,))
.await;
}
if let Ok(end) = stdin_obj.get::<_, JSFunc>("end") {
let _ = end.call_async::<_, ()>(Some(stdin_obj.clone()), ()).await;
}
});
child.set("stdin", JSValue::null(&ctx))?;
}
}
} else {
child.set("stdin", JSValue::null(&ctx))?;
}
if let Ok(stdout) = child.get::<_, JSObject>("stdout") {
match normalized.stdout {
ProcessStreamMode::Pipe => decorate_readable(&stdout)?,
ProcessStreamMode::Ignore | ProcessStreamMode::Inherit => {
let stdout_obj = stdout.clone();
let mode = normalized.stdout;
rong::spawn_local(async move {
let _ = drain_stream(stdout_obj, mode).await;
});
child.set("stdout", JSValue::null(&ctx))?;
}
}
}
if let Ok(stderr) = child.get::<_, JSObject>("stderr") {
match normalized.stderr {
ProcessStreamMode::Pipe => decorate_readable(&stderr)?,
ProcessStreamMode::Ignore | ProcessStreamMode::Inherit => {
let stderr_obj = stderr.clone();
let mode = normalized.stderr;
rong::spawn_local(async move {
let _ = drain_stream_to_stderr(stderr_obj, mode).await;
});
child.set("stderr", JSValue::null(&ctx))?;
}
}
}
if let Some(on_exit) = normalized.on_exit {
let child_obj = child.clone();
rong::spawn_local(async move {
let proc = if let Ok(proc) = child_obj.borrow::<child_process::ChildProcess>() {
proc.clone()
} else {
let error: RongJSError =
HostError::new(rong::error::E_INTERNAL, "ChildProcess missing").into();
let _ = on_exit
.call_async::<_, JSValue>(
None,
(
child_obj.clone(),
JSValue::null(&child_obj.context()),
JSValue::null(&child_obj.context()),
error.into_catch_value(&child_obj.context()),
),
)
.await;
return;
};
let wait_result = proc.wait().await;
match wait_result {
Ok(code) => {
let _ = on_exit
.call_async::<_, JSValue>(
None,
(
child_obj.clone(),
code,
JSValue::null(&child_obj.context()),
JSValue::undefined(&child_obj.context()),
),
)
.await;
}
Err(error) => {
let _ = on_exit
.call_async::<_, JSValue>(
None,
(
child_obj.clone(),
JSValue::null(&child_obj.context()),
JSValue::null(&child_obj.context()),
error.into_catch_value(&child_obj.context()),
),
)
.await;
}
}
});
}
setup_abort(&child, normalized.signal, normalized.kill_signal);
Ok(child)
}
fn spawn_sync(
ctx: JSContext,
cmd_or_options: JSValue,
options: Optional<JSObject>,
) -> JSResult<JSObject> {
let normalized = parse_spawn_options(cmd_or_options, options)?;
let native = JSObject::new(&ctx);
native.set("cmd", vec_to_js_array(&ctx, &normalized.cmd)?)?;
if let Some(cwd) = normalized.cwd {
native.set("cwd", cwd)?;
}
if let Some(env) = normalized.env {
let env_obj = JSObject::new(&ctx);
for (key, value) in env {
env_obj.set(key.as_str(), value)?;
}
native.set("env", env_obj)?;
}
if let Some(shell) = normalized.shell {
native.set("shell", shell)?;
}
if let Some(timeout) = normalized.timeout {
native.set("timeout", timeout as f64)?;
}
match normalized.stdin {
SpawnStdinMode::Pipe | SpawnStdinMode::AutoClose => {
native.set("stdin", JSValue::null(&ctx))?;
}
SpawnStdinMode::Payload(payload) => {
native.set("stdin", bytes_to_uint8_array(&ctx, payload)?)?;
}
}
let stdout_mode = match normalized.stdout {
ProcessStreamMode::Pipe => "pipe",
ProcessStreamMode::Ignore => "ignore",
ProcessStreamMode::Inherit => "inherit",
};
let stderr_mode = match normalized.stderr {
ProcessStreamMode::Pipe => "pipe",
ProcessStreamMode::Ignore => "ignore",
ProcessStreamMode::Inherit => "inherit",
};
native.set("stdout", stdout_mode)?;
native.set("stderr", stderr_mode)?;
let result = sync_process::spawn_sync_native(ctx.clone(), native)?;
let stdout = result.get::<_, JSArrayBuffer>("stdout")?.to_vec();
let stderr = result.get::<_, JSArrayBuffer>("stderr")?.to_vec();
result.set("stdout", bytes_to_uint8_array(&ctx, stdout)?)?;
result.set("stderr", bytes_to_uint8_array(&ctx, stderr)?)?;
Ok(result)
}
#[cfg(target_os = "windows")]
fn escape_cmd_literal(text: &str) -> String {
if text.is_empty() {
return "\"\"".to_string();
}
let needs_wrapping =
text.contains(char::is_whitespace) || text.contains(|c: char| "^&|<>()%".contains(c));
if !needs_wrapping {
return text.to_string();
}
let escaped = text.replace('%', "%%").replace('"', "\"\"");
format!("\"{escaped}\"")
}
#[cfg(not(target_os = "windows"))]
fn escape_cmd_literal(text: &str) -> String {
if text.is_empty() {
return "''".to_string();
}
format!("'{}'", text.replace('\'', "'\\''"))
}
fn shell_escape_value(value: JSValue) -> JSResult<String> {
if let Some(obj) = value.clone().into_object() {
if let Some(array) = JSArray::from_object(obj.clone()) {
let mut parts = Vec::with_capacity(array.len()? as usize);
for item in array.iter_values()? {
parts.push(shell_escape_value(item?)?);
}
return Ok(parts.join(" "));
}
if obj.has_property("raw")? {
let raw: JSValue = obj.get("raw")?;
return Ok(raw.to_string());
}
}
let text = value.to_string();
Ok(escape_cmd_literal(&text))
}
fn compose_shell_command(strings: JSArray, values: Rest<JSValue>) -> JSResult<String> {
let raw_candidate: JSValue = strings.get("raw")?;
let raw_strings = if let Some(obj) = raw_candidate.into_object() {
JSArray::from_object(obj).unwrap_or(strings.clone())
} else {
strings.clone()
};
let mut command = String::new();
let cooked = js_array_to_strings(&raw_strings, "template strings")?;
for (index, chunk) in cooked.iter().enumerate() {
command.push_str(chunk);
if let Some(value) = values.0.get(index) {
command.push_str(&shell_escape_value(value.clone())?);
}
}
Ok(command)
}
fn shell_defaults_snapshot(ctx: &JSContext) -> ShellDefaults {
ShellDefaults::ensure(ctx)
}
fn shell_default_cwd(ctx: JSContext, path: Optional<String>) -> JSResult<JSValue> {
let mut state = ShellDefaults::ensure(&ctx);
if let Some(path) = path.0 {
state.cwd = Some(path);
ctx.set_service(state);
return ctx.host_namespace().get::<_, JSValue>("$");
}
Ok(match state.cwd {
Some(path) => JSValue::from_rust(&ctx, path),
None => JSValue::undefined(&ctx),
})
}
fn shell_default_env(ctx: JSContext, env: Optional<JSObject>) -> JSResult<JSValue> {
let mut state = ShellDefaults::ensure(&ctx);
if let Some(env_obj) = env.0 {
state.env = Some(normalize_env_map(&env_obj)?);
ctx.set_service(state);
return ctx.host_namespace().get::<_, JSValue>("$");
}
if let Some(env_map) = state.env {
let obj = JSObject::new(&ctx);
for (key, value) in env_map {
obj.set(key.as_str(), value)?;
}
return Ok(obj.into_js_value());
}
Ok(JSValue::undefined(&ctx))
}
fn shell_default_throws(ctx: JSContext, value: Optional<bool>) -> JSResult<JSObject> {
let mut state = ShellDefaults::ensure(&ctx);
state.throws = value.0.unwrap_or(true);
ctx.set_service(state);
ctx.host_namespace().get("$")
}
fn shell_default_nothrow(ctx: JSContext) -> JSResult<JSObject> {
let mut state = ShellDefaults::ensure(&ctx);
state.throws = false;
ctx.set_service(state);
ctx.host_namespace().get("$")
}
fn shell_default_quiet(ctx: JSContext) -> JSResult<JSObject> {
let mut state = ShellDefaults::ensure(&ctx);
state.quiet = true;
ctx.set_service(state);
ctx.host_namespace().get("$")
}
fn shell_escape(value: JSValue) -> JSResult<String> {
shell_escape_value(value)
}
fn shell_tag(ctx: JSContext, first: JSValue, rest: Rest<JSValue>) -> JSResult<ShellCommand> {
let command = if let Some(obj) = first.clone().into_object() {
if let Some(array) = JSArray::from_object(obj) {
compose_shell_command(array, rest)?
} else {
first.to_string()
}
} else {
first.to_string()
};
let state = shell_defaults_snapshot(&ctx);
Ok(ShellCommand {
command,
cwd: state.cwd,
env: state.env,
throws: state.throws,
quiet: state.quiet,
})
}
fn shell_result_to_object(ctx: &JSContext, data: ShellResultData) -> JSResult<JSObject> {
let result = JSObject::new(ctx);
result.set("stdout", bytes_to_uint8_array(ctx, data.stdout)?)?;
result.set("stderr", bytes_to_uint8_array(ctx, data.stderr)?)?;
result.set("exitCode", data.exit_code)?;
result.set("success", data.exit_code == Some(0))?;
Ok(result)
}
impl ShellCommand {
async fn run_internal(&self, ctx: JSContext) -> JSResult<ShellResultData> {
let options = JSObject::new(&ctx);
let mut has_options = false;
if let Some(cwd) = &self.cwd {
options.set("cwd", cwd.clone())?;
has_options = true;
}
if let Some(env) = &self.env {
let env_obj = JSObject::new(&ctx);
for (key, value) in env {
env_obj.set(key.as_str(), value.clone())?;
}
options.set("env", env_obj)?;
has_options = true;
}
let exec = child_process::exec_native(
ctx.clone(),
self.command.clone(),
Optional(has_options.then_some(options)),
)?;
let result = exec.into_future::<JSObject>().await?;
let stdout = result.get::<_, String>("stdout")?.into_bytes();
let stderr = result.get::<_, String>("stderr")?.into_bytes();
let exit_code = result.get::<_, Option<i32>>("code")?;
let payload = ShellResultData {
stdout,
stderr,
exit_code,
};
if payload.exit_code != Some(0) && self.throws {
return Err(ShellError::new_internal(
&ctx,
self.command.clone(),
payload.exit_code,
payload.stdout.clone(),
payload.stderr.clone(),
)?);
}
if !self.quiet && !payload.stderr.is_empty() {
io::write_stderr_bytes_native(&payload.stderr);
}
Ok(payload)
}
fn with_overrides(
&self,
cwd: Option<Option<String>>,
env: Option<HashMap<String, String>>,
throws: Option<bool>,
quiet: Option<bool>,
) -> Self {
Self {
command: self.command.clone(),
cwd: cwd.unwrap_or_else(|| self.cwd.clone()),
env: env.or_else(|| self.env.clone()),
throws: throws.unwrap_or(self.throws),
quiet: quiet.unwrap_or(self.quiet),
}
}
}
impl ShellError {
fn new_internal(
ctx: &JSContext,
command: String,
exit_code: Option<i32>,
stdout: Vec<u8>,
stderr: Vec<u8>,
) -> JSResult<RongJSError> {
let message = format!(
"Shell command failed with exit code {}: {}",
exit_code
.map(|v| v.to_string())
.unwrap_or_else(|| "null".to_string()),
command
);
let obj = Class::lookup::<ShellError>(ctx)?.instance(ShellError {
message,
command,
exit_code,
stdout,
stderr,
});
Ok(RongJSError::from_thrown_value(obj.into_js_value()))
}
}
#[js_class]
impl ShellError {
#[js_method(constructor)]
fn constructor(message: String) -> JSResult<Self> {
Ok(Self {
message,
command: String::new(),
exit_code: None,
stdout: Vec::new(),
stderr: Vec::new(),
})
}
#[js_method(getter)]
fn name(&self) -> String {
"ShellError".to_string()
}
#[js_method(getter)]
fn message(&self) -> String {
self.message.clone()
}
#[js_method(getter)]
fn command(&self) -> String {
self.command.clone()
}
#[js_method(getter, rename = "exitCode")]
fn exit_code(&self) -> Option<i32> {
self.exit_code
}
#[js_method(getter)]
fn stdout(&self, ctx: JSContext) -> JSResult<JSTypedArray<u8>> {
bytes_to_uint8_array(&ctx, self.stdout.clone())
}
#[js_method(getter)]
fn stderr(&self, ctx: JSContext) -> JSResult<JSTypedArray<u8>> {
bytes_to_uint8_array(&ctx, self.stderr.clone())
}
#[js_method(gc_mark)]
fn gc_mark_with<F>(&self, _mark_fn: F)
where
F: FnMut(&JSValue),
{
}
}
#[js_class]
impl ShellCommand {
#[js_method(constructor)]
fn constructor() -> JSResult<Self> {
rong::illegal_constructor("ShellCommand cannot be constructed directly. Use Rong.$.")
}
#[js_method]
fn cwd(&self, path: String) -> Self {
self.with_overrides(Some(Some(path)), None, None, None)
}
#[js_method]
fn env(&self, values: JSObject) -> JSResult<Self> {
let mut next = self.env.clone().unwrap_or_default();
next.extend(normalize_env_map(&values)?);
Ok(self.with_overrides(None, Some(next), None, None))
}
#[js_method]
fn quiet(&self) -> Self {
self.with_overrides(None, None, None, Some(true))
}
#[js_method]
fn nothrow(&self) -> Self {
self.with_overrides(None, None, Some(false), None)
}
#[js_method]
fn throws(&self, value: Optional<bool>) -> Self {
self.with_overrides(None, None, Some(value.0.unwrap_or(true)), None)
}
#[js_method]
async fn run(&self, ctx: JSContext) -> JSResult<JSObject> {
shell_result_to_object(&ctx, self.run_internal(ctx.clone()).await?)
}
#[js_method]
async fn text(&self, ctx: JSContext) -> JSResult<String> {
let result = self.run_internal(ctx).await?;
Ok(String::from_utf8_lossy(&result.stdout).to_string())
}
#[js_method]
async fn json(&self, ctx: JSContext) -> JSResult<JSValue> {
let text = self.text(ctx.clone()).await?;
let json = ctx.global().get::<_, JSObject>("JSON")?;
let parse = json.get::<_, JSFunc>("parse")?;
parse.call::<_, JSValue>(Some(json), (text,))
}
#[js_method]
async fn blob(&self, ctx: JSContext) -> JSResult<Blob> {
let bytes = self.run_internal(ctx).await?.stdout;
Ok(Blob::from_parts(String::new(), bytes))
}
#[js_method]
async fn lines(&self, ctx: JSContext) -> JSResult<JSObject> {
make_lines_iterator(&ctx, self.text(ctx.clone()).await?)
}
#[js_method]
fn then(
&self,
ctx: JSContext,
on_fulfilled: Optional<JSFunc>,
on_rejected: Optional<JSFunc>,
) -> JSResult<Promise> {
let this = self.clone();
let promise_ctx = ctx.clone();
Promise::from_future(&ctx, None, async move {
match this.run_internal(promise_ctx.clone()).await {
Ok(data) => {
let result = shell_result_to_object(&promise_ctx, data)?;
if let Some(handler) = on_fulfilled.0 {
handler.call_async::<_, JSValue>(None, (result,)).await
} else {
Ok(result.into_js_value())
}
}
Err(error) => {
if let Some(handler) = on_rejected.0 {
handler
.call_async::<_, JSValue>(None, (error.into_catch_value(&promise_ctx),))
.await
} else {
Err(error)
}
}
}
})
}
#[js_method(gc_mark)]
fn gc_mark_with<F>(&self, _mark_fn: F)
where
F: FnMut(&JSValue),
{
}
}
pub fn init(ctx: &JSContext) -> JSResult<()> {
let _ = ShellDefaults::ensure(ctx);
ctx.register_hidden_class::<ShellCommand>()?;
ctx.register_class::<ShellError>()?;
let rong = ctx.host_namespace();
rong.set("spawn", JSFunc::new(ctx, spawn)?.name("spawn")?)?;
rong.set(
"spawnSync",
JSFunc::new(ctx, spawn_sync)?.name("spawnSync")?,
)?;
let shell = JSFunc::new(ctx, shell_tag)?.name("$")?;
shell.set("cwd", JSFunc::new(ctx, shell_default_cwd)?.name("cwd")?)?;
shell.set("env", JSFunc::new(ctx, shell_default_env)?.name("env")?)?;
shell.set(
"throws",
JSFunc::new(ctx, shell_default_throws)?.name("throws")?,
)?;
shell.set(
"nothrow",
JSFunc::new(ctx, shell_default_nothrow)?.name("nothrow")?,
)?;
shell.set(
"quiet",
JSFunc::new(ctx, shell_default_quiet)?.name("quiet")?,
)?;
shell.set("escape", JSFunc::new(ctx, shell_escape)?.name("escape")?)?;
rong.set("$", shell)?;
let shell_error_ctor: JSObject = ctx.global().get("ShellError")?;
rong.set("ShellError", shell_error_ctor)?;
Ok(())
}