From 6b097345eda258706111c87e594076466d10af07 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Mon, 29 Jun 2020 15:39:39 -0300 Subject: [PATCH] refactor(tauri) execute_promise String/Serialize management (#724) --- .changes/execute-promise-refactor.md | 8 ++ cli/tauri.js/api-src/types/http.ts | 2 +- tauri-api/src/http.rs | 11 +-- tauri-api/src/notification.rs | 7 +- tauri-api/src/rpc.rs | 114 +++++++++++++-------------- tauri/src/endpoints.rs | 10 +-- tauri/src/endpoints/asset.rs | 2 +- tauri/src/endpoints/dialog.rs | 19 +++-- tauri/src/endpoints/file_system.rs | 39 +++------ tauri/src/endpoints/http.rs | 20 +---- tauri/src/endpoints/notification.rs | 14 ++-- tauri/src/endpoints/salt.rs | 9 +-- tauri/src/event.rs | 16 +++- tauri/src/lib.rs | 62 +++++++++------ 14 files changed, 157 insertions(+), 176 deletions(-) create mode 100644 .changes/execute-promise-refactor.md diff --git a/.changes/execute-promise-refactor.md b/.changes/execute-promise-refactor.md new file mode 100644 index 000000000..9bee5586b --- /dev/null +++ b/.changes/execute-promise-refactor.md @@ -0,0 +1,8 @@ +--- +"tauri-api": minor +"tauri": minor +--- + +The `execute_promise` and `execute_promise_sync` helpers now accepts any `tauri::Result` where `T: impl Serialize`. +This means that you do not need to serialize your response manually or deal with String quotes anymore. +As part of this refactor, the `event::emit` function also supports `impl Serialize` instead of `String`. diff --git a/cli/tauri.js/api-src/types/http.ts b/cli/tauri.js/api-src/types/http.ts index caa0bda5f..c814aa782 100644 --- a/cli/tauri.js/api-src/types/http.ts +++ b/cli/tauri.js/api-src/types/http.ts @@ -18,7 +18,7 @@ export interface HttpOptions { method: HttpVerb url: string headers?: Record - propertys?: Record + params?: Record body?: Body followRedirects: boolean maxRedirections: boolean diff --git a/tauri-api/src/http.rs b/tauri-api/src/http.rs index 6b9255acf..9e9d72a47 100644 --- a/tauri-api/src/http.rs +++ b/tauri-api/src/http.rs @@ -221,7 +221,7 @@ impl HttpRequestBuilder { /// /// The response will be transformed to String, /// If reading the response as binary, the byte array will be serialized using serde_json -pub fn make_request(options: HttpRequestOptions) -> crate::Result { +pub fn make_request(options: HttpRequestOptions) -> crate::Result { let method = Method::from_bytes(options.method.to_uppercase().as_bytes())?; let mut builder = RequestBuilder::new(method, options.url); if let Some(params) = options.params { @@ -291,12 +291,9 @@ pub fn make_request(options: HttpRequestOptions) -> crate::Result { let response = response?; if response.is_success() { let response_data = match options.response_type.unwrap_or(ResponseType::Json) { - ResponseType::Json => { - let result = response.json::()?; - serde_json::to_string(&result)? - } - ResponseType::Text => response.text()?, - ResponseType::Binary => serde_json::to_string(&response.bytes()?)?, + ResponseType::Json => response.json::()?, + ResponseType::Text => Value::String(response.text()?), + ResponseType::Binary => Value::String(serde_json::to_string(&response.bytes()?)?), }; Ok(response_data) } else { diff --git a/tauri-api/src/notification.rs b/tauri-api/src/notification.rs index b0f141263..4b1606f0a 100644 --- a/tauri-api/src/notification.rs +++ b/tauri-api/src/notification.rs @@ -16,6 +16,7 @@ use std::path::MAIN_SEPARATOR; /// .show(); /// ``` #[allow(dead_code)] +#[derive(Default)] pub struct Notification { /// The notification body. body: Option, @@ -28,11 +29,7 @@ pub struct Notification { impl Notification { /// Initializes a instance of a Notification. pub fn new() -> Self { - Self { - body: None, - title: None, - icon: None, - } + Default::default() } /// Sets the notification body. diff --git a/tauri-api/src/rpc.rs b/tauri-api/src/rpc.rs index 152ab2d82..497ed174c 100644 --- a/tauri-api/src/rpc.rs +++ b/tauri-api/src/rpc.rs @@ -1,12 +1,15 @@ -/// Formats a function to be evaluated as callback. -/// If the arg is a string literal, it needs the proper quotes. +use serde::Serialize; +use serde_json::Value as JsonValue; +use std::fmt::Display; + +/// Formats a function name and argument to be evaluated as callback. /// /// # Examples /// ``` /// use tauri_api::rpc::format_callback; /// // callback with a string argument -/// // returns `window["callback-function-name"]("the string response")` -/// format_callback("callback-function-name".to_string(), r#""the string response""#.to_string()); +/// let cb = format_callback("callback-function-name", "the string response"); +/// assert_eq!(cb, r#"window["callback-function-name"]("the string response")"#); /// ``` /// /// ``` @@ -17,39 +20,50 @@ /// struct MyResponse { /// value: String /// } -/// // this returns `window["callback-function-name"]({value: "some value"})` -/// format_callback("callback-function-name".to_string(), serde_json::to_string(&MyResponse { +/// let cb = format_callback("callback-function-name", serde_json::to_value(&MyResponse { /// value: "some value".to_string() -/// }).expect("failed to serialize type")); +/// }).expect("failed to serialize")); +/// assert_eq!(cb, r#"window["callback-function-name"]({"value":"some value"})"#); /// ``` -pub fn format_callback(function_name: String, arg: String) -> String { - let formatted_string = &format!("window[\"{}\"]({})", function_name, arg); - formatted_string.to_string() +pub fn format_callback, S: AsRef + Display>( + function_name: S, + arg: T, +) -> String { + format!(r#"window["{}"]({})"#, function_name, arg.into().to_string()) } -/// Formats a Result type to its callback version. +/// Formats a Result type to its Promise response. /// Useful for Promises handling. +/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. +/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. /// -/// If the Result is Ok, `format_callback` will be called directly. -/// If the result is an Err, we assume the error message is a string, and quote it. +/// * `result` the Result to check +/// * `success_callback` the function name of the Ok callback. Usually the `resolve` of the JS Promise. +/// * `error_callback` the function name of the Err callback. Usually the `reject` of the JS Promise. +/// +/// Note that the callback strings are automatically generated by the `promisified` helper. /// /// # Examples /// ``` /// use tauri_api::rpc::format_callback_result; -/// // returns `window["success_cb"](5)` -/// format_callback_result(Ok("5".to_string()), "success_cb".to_string(), "error_cb".to_string()); -/// // returns `window["error_cb"]("error message here")` -/// format_callback_result(Err("error message here".to_string()), "success_cb".to_string(), "error_cb".to_string()); +/// let res: Result = Ok(5); +/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format"); +/// assert_eq!(cb, r#"window["success_cb"](5)"#); +/// +/// let res: Result<&str, &str> = Err("error message here"); +/// let cb = format_callback_result(res, "success_cb".to_string(), "error_cb".to_string()).expect("failed to format"); +/// assert_eq!(cb, r#"window["error_cb"]("error message here")"#); /// ``` -pub fn format_callback_result( - result: Result, - callback: String, +pub fn format_callback_result( + result: Result, + success_callback: String, error_callback: String, -) -> String { - match result { - Ok(res) => format_callback(callback, res), - Err(err) => format_callback(error_callback, format!("\"{}\"", err)), - } +) -> crate::Result { + let rpc = match result { + Ok(res) => format_callback(success_callback, serde_json::to_value(res)?), + Err(err) => format_callback(error_callback, serde_json::to_value(err)?), + }; + Ok(rpc) } #[cfg(test)] @@ -62,16 +76,9 @@ mod test { fn qc_formating(f: String, a: String) -> bool { // can not accept empty strings if f != "" && a != "" { - // get length of function and argument - let alen = &a.len(); - let flen = &f.len(); // call format callback - let fc = format_callback(f, a); - // get length of the resulting string - let fclen = fc.len(); - - // if formatted string equals the length of the argument and the function plus 12 then its correct. - fclen == alen + flen + 12 + let fc = format_callback(f.clone(), a.clone()); + fc == format!(r#"window["{}"]({})"#, f, serde_json::Value::String(a)) } else { true } @@ -80,33 +87,18 @@ mod test { // check arbitrary strings in format_callback_result #[quickcheck] fn qc_format_res(result: Result, c: String, ec: String) -> bool { - // match on result to decide how to call the function. - match result { - // if ok, get length of result and callback strings. - Ok(r) => { - let rlen = r.len(); - let clen = c.len(); + let resp = format_callback_result(result.clone(), c.clone(), ec.clone()) + .expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; - // take the ok string from result and pass it into format_callback_result as an ok. - let resp = format_callback_result(Ok(r), c, ec); - // get response string length - let reslen = resp.len(); - - // if response string length equals result and callback length plus 12 characters then it is correct. - reslen == rlen + clen + 12 - } - // If Err, get length of Err and Error callback - Err(err) => { - let eclen = ec.len(); - let errlen = err.len(); - // pass err as Err into format_callback_result with callback and error callback - let resp = format_callback_result(Err(err), c, ec); - // get response string length - let reslen = resp.len(); - - // if length of response string equals the error length and the error callback length plus 14 characters then its is correct. - reslen == eclen + errlen + 14 - } - } + resp + == format!( + r#"window["{}"]({})"#, + function, + serde_json::Value::String(value), + ) } } diff --git a/tauri/src/endpoints.rs b/tauri/src/endpoints.rs index 8f4dd54e0..f68bf39da 100644 --- a/tauri/src/endpoints.rs +++ b/tauri/src/endpoints.rs @@ -179,7 +179,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra callback, error, } => { - salt::validate(webview, salt, callback, error); + salt::validate(webview, salt, callback, error)?; } Listen { event, @@ -206,7 +206,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra error, } => { #[cfg(open_dialog)] - dialog::open(webview, options, callback, error); + dialog::open(webview, options, callback, error)?; #[cfg(not(open_dialog))] whitelist_error(webview, error, "title"); } @@ -216,7 +216,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra error, } => { #[cfg(save_dialog)] - dialog::save(webview, options, callback, error); + dialog::save(webview, options, callback, error)?; #[cfg(not(save_dialog))] throw_whitelist_error(webview, "saveDialog"); } @@ -244,7 +244,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra crate::execute_promise( webview, move || match crate::cli::get_matches() { - Some(matches) => Ok(serde_json::to_string(matches)?), + Some(matches) => Ok(matches), None => Err(anyhow::anyhow!(r#""failed to get matches""#)), }, callback, @@ -271,7 +271,7 @@ pub(crate) fn handle(webview: &mut WebView<'_, T>, arg: &str) -> cra } RequestNotificationPermission { callback, error } => { #[cfg(notification)] - notification::request_permission(webview, callback, error); + notification::request_permission(webview, callback, error)?; #[cfg(not(notification))] whitelist_error(webview, error, "notification"); } diff --git a/tauri/src/endpoints/asset.rs b/tauri/src/endpoints/asset.rs index 8ad87784b..b910a10e8 100644 --- a/tauri/src/endpoints/asset.rs +++ b/tauri/src/endpoints/asset.rs @@ -70,7 +70,7 @@ pub fn load( } }) .map_err(|err| err.into()) - .map(|_| r#""Asset loaded successfully""#.to_string()) + .map(|_| "Asset loaded successfully".to_string()) } }, callback, diff --git a/tauri/src/endpoints/dialog.rs b/tauri/src/endpoints/dialog.rs index a2d568ca3..cd1cf46cf 100644 --- a/tauri/src/endpoints/dialog.rs +++ b/tauri/src/endpoints/dialog.rs @@ -1,13 +1,14 @@ use super::cmd::{OpenDialogOptions, SaveDialogOptions}; use crate::api::dialog::{pick_folder, save_file, select, select_multiple, Response}; +use serde_json::Value as JsonValue; use web_view::WebView; /// maps a dialog response to a JS value to eval -fn map_response(response: Response) -> String { +fn map_response(response: Response) -> JsonValue { match response { - Response::Okay(path) => format!(r#""{}""#, path).replace("\\", "\\\\"), - Response::OkayMultiple(paths) => format!("{:?}", paths), - Response::Cancel => panic!("unexpected response type"), + Response::Okay(path) => path.into(), + Response::OkayMultiple(paths) => paths.into(), + Response::Cancel => JsonValue::Null, } } @@ -18,7 +19,7 @@ pub fn open( options: OpenDialogOptions, callback: String, error: String, -) { +) -> crate::Result<()> { crate::execute_promise_sync( webview, move || { @@ -33,7 +34,8 @@ pub fn open( }, callback, error, - ); + )?; + Ok(()) } /// Shows a save dialog. @@ -43,11 +45,12 @@ pub fn save( options: SaveDialogOptions, callback: String, error: String, -) { +) -> crate::Result<()> { crate::execute_promise_sync( webview, move || save_file(options.filter, options.default_path).map(map_response), callback, error, - ); + )?; + Ok(()) } diff --git a/tauri/src/endpoints/file_system.rs b/tauri/src/endpoints/file_system.rs index 0a9342acf..440a7ac2d 100644 --- a/tauri/src/endpoints/file_system.rs +++ b/tauri/src/endpoints/file_system.rs @@ -28,7 +28,6 @@ pub fn read_dir( (false, None) }; dir::read_dir(resolve_path(path, dir)?, recursive) - .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into())) }, callback, error, @@ -55,9 +54,7 @@ pub fn copy_file( ), None => (source, destination), }; - fs::copy(src, dest) - .map_err(|e| e.into()) - .map(|_| "".to_string()) + fs::copy(src, dest).map_err(|e| e.into()) }, callback, error, @@ -88,7 +85,7 @@ pub fn create_dir( fs::create_dir(resolved_path) }; - response.map_err(|e| e.into()).map(|_| "".to_string()) + response.map_err(|e| e.into()) }, callback, error, @@ -119,7 +116,7 @@ pub fn remove_dir( fs::remove_dir(resolved_path) }; - response.map_err(|e| e.into()).map(|_| "".to_string()) + response.map_err(|e| e.into()) }, callback, error, @@ -139,9 +136,7 @@ pub fn remove_file( webview, move || { let resolved_path = resolve_path(path, options.and_then(|o| o.dir))?; - fs::remove_file(resolved_path) - .map_err(|e| e.into()) - .map(|_| "".to_string()) + fs::remove_file(resolved_path).map_err(|e| e.into()) }, callback, error, @@ -168,9 +163,7 @@ pub fn rename_file( ), None => (old_path, new_path), }; - fs::rename(old, new) - .map_err(|e| e.into()) - .map(|_| "".to_string()) + fs::rename(old, new).map_err(|e| e.into()) }, callback, error, @@ -192,11 +185,7 @@ pub fn write_file( move || { File::create(resolve_path(file, options.and_then(|o| o.dir))?) .map_err(|e| e.into()) - .and_then(|mut f| { - f.write_all(contents.as_bytes()) - .map_err(|err| err.into()) - .map(|_| "".to_string()) - }) + .and_then(|mut f| f.write_all(contents.as_bytes()).map_err(|err| err.into())) }, callback, error, @@ -221,11 +210,7 @@ pub fn write_binary_file( .and_then(|c| { File::create(resolve_path(file, options.and_then(|o| o.dir))?) .map_err(|e| e.into()) - .and_then(|mut f| { - f.write_all(&c) - .map_err(|err| err.into()) - .map(|_| "".to_string()) - }) + .and_then(|mut f| f.write_all(&c).map_err(|err| err.into())) }) }, callback, @@ -244,10 +229,7 @@ pub fn read_text_file( ) { crate::execute_promise( webview, - move || { - file::read_string(resolve_path(path, options.and_then(|o| o.dir))?) - .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into())) - }, + move || file::read_string(resolve_path(path, options.and_then(|o| o.dir))?), callback, error, ); @@ -264,10 +246,7 @@ pub fn read_binary_file( ) { crate::execute_promise( webview, - move || { - file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?) - .and_then(|f| serde_json::to_string(&f).map_err(|err| err.into())) - }, + move || file::read_binary(resolve_path(path, options.and_then(|o| o.dir))?), callback, error, ); diff --git a/tauri/src/endpoints/http.rs b/tauri/src/endpoints/http.rs index 2160c2cf9..e10133762 100644 --- a/tauri/src/endpoints/http.rs +++ b/tauri/src/endpoints/http.rs @@ -1,4 +1,4 @@ -use tauri_api::http::{make_request as request, HttpRequestOptions, ResponseType}; +use tauri_api::http::{make_request as request, HttpRequestOptions}; use web_view::WebView; /// Makes an HTTP request and resolves the response to the webview @@ -8,21 +8,5 @@ pub fn make_request( callback: String, error: String, ) { - crate::execute_promise( - webview, - move || { - let response_type = options.response_type.clone(); - request(options).map( - |response| match response_type.unwrap_or(ResponseType::Json) { - ResponseType::Text => format!( - r#""{}""#, - response.replace(r#"""#, r#"\""#).replace(r#"\\""#, r#"\""#) - ), - _ => response, - }, - ) - }, - callback, - error, - ); + crate::execute_promise(webview, move || request(options), callback, error); } diff --git a/tauri/src/endpoints/notification.rs b/tauri/src/endpoints/notification.rs index 6400c20aa..4885b7226 100644 --- a/tauri/src/endpoints/notification.rs +++ b/tauri/src/endpoints/notification.rs @@ -1,4 +1,5 @@ use super::cmd::NotificationOptions; +use serde_json::Value as JsonValue; use web_view::WebView; pub fn send( @@ -18,9 +19,7 @@ pub fn send( if let Some(icon) = options.icon { notification = notification.icon(icon); } - notification - .show() - .map_err(|e| anyhow::anyhow!(r#""{}""#, e.to_string()))?; + notification.show()?; Ok("".to_string()) }, callback, @@ -38,9 +37,9 @@ pub fn is_permission_granted( move || { let settings = crate::settings::read_settings()?; if let Some(allow_notification) = settings.allow_notification { - Ok(allow_notification.to_string()) + Ok(JsonValue::String(allow_notification.to_string())) } else { - Ok("null".to_string()) + Ok(JsonValue::Null) } }, callback, @@ -52,7 +51,7 @@ pub fn request_permission( webview: &mut WebView<'_, T>, callback: String, error: String, -) { +) -> crate::Result<()> { crate::execute_promise_sync( webview, move || { @@ -82,5 +81,6 @@ pub fn request_permission( }, callback, error, - ); + )?; + Ok(()) } diff --git a/tauri/src/endpoints/salt.rs b/tauri/src/endpoints/salt.rs index 19ec0a31f..81037c303 100644 --- a/tauri/src/endpoints/salt.rs +++ b/tauri/src/endpoints/salt.rs @@ -6,14 +6,13 @@ pub fn validate( salt: String, callback: String, error: String, -) { +) -> crate::Result<()> { let response = if crate::salt::is_valid(salt) { Ok("'VALID'".to_string()) } else { Err("'INVALID SALT'".to_string()) }; - let callback_string = crate::api::rpc::format_callback_result(response, callback, error); - webview - .eval(callback_string.as_str()) - .expect("Failed to eval JS from validate()"); + let callback_string = crate::api::rpc::format_callback_result(response, callback, error)?; + webview.eval(callback_string.as_str())?; + Ok(()) } diff --git a/tauri/src/event.rs b/tauri/src/event.rs index 4ba7c6df0..d1c3af2d4 100644 --- a/tauri/src/event.rs +++ b/tauri/src/event.rs @@ -4,6 +4,8 @@ use std::sync::{Arc, Mutex}; use lazy_static::lazy_static; use once_cell::sync::Lazy; +use serde::Serialize; +use serde_json::Value as JsonValue; use web_view::Handle; /// An event handler. @@ -55,13 +57,17 @@ pub fn listen) + Send + 'static>(id: String, handler: F) } /// Emits an event to JS. -pub fn emit(webview_handle: &Handle, event: String, payload: Option) { +pub fn emit( + webview_handle: &Handle, + event: String, + payload: Option, +) -> crate::Result<()> { let salt = crate::salt::generate(); - let js_payload = if let Some(payload_str) = payload { - payload_str + let js_payload = if let Some(payload_value) = payload { + serde_json::to_value(payload_value)? } else { - "void 0".to_string() + JsonValue::Null }; webview_handle @@ -75,6 +81,8 @@ pub fn emit(webview_handle: &Handle, event: String, payload: Opti )) }) .expect("Failed to dispatch JS from emit"); + + Ok(()) } /// Triggers the given event with its payload. diff --git a/tauri/src/lib.rs b/tauri/src/lib.rs index d78cfbedb..c740cc6d6 100644 --- a/tauri/src/lib.rs +++ b/tauri/src/lib.rs @@ -32,17 +32,19 @@ mod endpoints; /// The salt helpers. mod salt; -use std::process::Stdio; - /// Alias for a Result with error type anyhow::Error. pub use anyhow::Result; -use threadpool::ThreadPool; - -pub use web_view::Handle; -use web_view::WebView; - pub use app::*; pub use tauri_api as api; +pub use web_view::Handle; + +use std::process::Stdio; + +use api::rpc::{format_callback, format_callback_result}; +use serde::Serialize; +use threadpool::ThreadPool; +use web_view::WebView; + thread_local!(static POOL: ThreadPool = ThreadPool::new(4)); /// Executes the operation in the thread pool. @@ -56,33 +58,49 @@ pub fn spawn () + Send + 'static>(task: F) { /// Synchronously executes the given task /// and evaluates its Result to the JS promise described by the `callback` and `error` function names. -pub fn execute_promise_sync crate::Result + Send + 'static>( +pub fn execute_promise_sync< + T: 'static, + R: Serialize, + F: FnOnce() -> crate::Result + Send + 'static, +>( webview: &mut WebView<'_, T>, task: F, callback: String, error: String, -) { +) -> crate::Result<()> { let handle = webview.handle(); let callback_string = - api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error); - handle - .dispatch(move |_webview| _webview.eval(callback_string.as_str())) - .expect("Failed to dispatch promise callback"); + format_callback_result(task().map_err(|err| err.to_string()), callback, error)?; + handle.dispatch(move |_webview| _webview.eval(callback_string.as_str()))?; + Ok(()) } /// Asynchronously executes the given task -/// and evaluates its Result to the JS promise described by the `callback` and `error` function names. -pub fn execute_promise crate::Result + Send + 'static>( +/// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names. +/// +/// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. +/// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. +pub fn execute_promise< + T: 'static, + R: Serialize, + F: FnOnce() -> crate::Result + Send + 'static, +>( webview: &mut WebView<'_, T>, task: F, - callback: String, - error: String, + success_callback: String, + error_callback: String, ) { let handle = webview.handle(); POOL.with(|thread| { thread.execute(move || { - let callback_string = - api::rpc::format_callback_result(task().map_err(|err| err.to_string()), callback, error); + let callback_string = match format_callback_result( + task().map_err(|err| err.to_string()), + success_callback, + error_callback.clone(), + ) { + Ok(callback_string) => callback_string, + Err(e) => format_callback(error_callback, e.to_string()), + }; handle .dispatch(move |_webview| _webview.eval(callback_string.as_str())) .expect("Failed to dispatch promise callback") @@ -100,11 +118,7 @@ pub fn call( ) { execute_promise( webview, - || { - api::command::get_output(command, args, Stdio::piped()) - .map_err(|err| err) - .map(|output| format!(r#""{}""#, output)) - }, + || api::command::get_output(command, args, Stdio::piped()), callback, error, );