Skip to content

Commit 93d12b7

Browse files
fix(core/shell): speedup Command.execute & fix extra new lines (#9706)
* fix(core/shell): speedup `Command.execute` & fix extra new lines The speed gains comes from running the Command in Rust fully and returning the result in one go instead of using events. The extra new lines was a regression from #6519 ref: #7684 (comment) * fix unix build * clippy * cleanup --------- Co-authored-by: Lucas Nogueira <[email protected]>
1 parent d78fa20 commit 93d12b7

File tree

5 files changed

+218
-155
lines changed

5 files changed

+218
-155
lines changed
+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@tauri-apps/api": "patch:bug"
3+
---
4+
5+
Fix The JS `Command.execute` API from `shell` module including extra new lines.
6+

.changes/shell-execute-performance.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"tauri": "patch:enhance"
3+
"@tauri-apps/api": "patch:enhance"
4+
---
5+
6+
Enhance the speed of The JS `Command.execute` API from `shell` module.
7+

core/tauri/scripts/bundle.global.js

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

core/tauri/src/endpoints/shell.rs

+153-82
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use super::InvokeContext;
88
use crate::{api::ipc::CallbackFn, Runtime};
99
#[cfg(shell_scope)]
1010
use crate::{Manager, Scopes};
11-
use serde::Deserialize;
11+
use serde::{Deserialize, Serialize};
1212
use tauri_macros::{command_enum, module_command_handler, CommandModule};
1313

1414
#[cfg(shell_scope)]
@@ -63,6 +63,15 @@ pub struct CommandOptions {
6363
#[derive(Deserialize, CommandModule)]
6464
#[serde(tag = "cmd", rename_all = "camelCase")]
6565
pub enum Cmd {
66+
/// The execute and return script API.
67+
#[cmd(shell_script, "shell > execute or shell > sidecar")]
68+
#[serde(rename_all = "camelCase")]
69+
ExecuteAndReturn {
70+
program: String,
71+
args: ExecuteArgs,
72+
#[serde(default)]
73+
options: CommandOptions,
74+
},
6675
/// The execute script API.
6776
#[cmd(shell_script, "shell > execute or shell > sidecar")]
6877
#[serde(rename_all = "camelCase")]
@@ -81,101 +90,88 @@ pub enum Cmd {
8190
Open { path: String, with: Option<String> },
8291
}
8392

93+
#[derive(Serialize)]
94+
#[cfg(any(shell_execute, shell_sidecar))]
95+
struct ChildProcessReturn {
96+
code: Option<i32>,
97+
signal: Option<i32>,
98+
stdout: String,
99+
stderr: String,
100+
}
101+
84102
impl Cmd {
85103
#[module_command_handler(shell_script)]
86-
#[allow(unused_variables)]
104+
fn execute_and_return<R: Runtime>(
105+
context: InvokeContext<R>,
106+
program: String,
107+
args: ExecuteArgs,
108+
options: CommandOptions,
109+
) -> super::Result<ChildProcessReturn> {
110+
let encoding = options
111+
.encoding
112+
.as_ref()
113+
.and_then(|encoding| crate::api::process::Encoding::for_label(encoding.as_bytes()));
114+
let command = prepare_cmd(&context, &program, args, options)?;
115+
116+
let mut command: std::process::Command = command.into();
117+
let output = command.output()?;
118+
119+
let (stdout, stderr) = match encoding {
120+
Some(encoding) => (
121+
encoding.decode_with_bom_removal(&output.stdout).0.into(),
122+
encoding.decode_with_bom_removal(&output.stderr).0.into(),
123+
),
124+
None => (
125+
String::from_utf8(output.stdout)?,
126+
String::from_utf8(output.stderr)?,
127+
),
128+
};
129+
130+
#[cfg(unix)]
131+
use std::os::unix::process::ExitStatusExt;
132+
133+
Ok(ChildProcessReturn {
134+
code: output.status.code(),
135+
#[cfg(windows)]
136+
signal: None,
137+
#[cfg(unix)]
138+
signal: output.status.signal(),
139+
stdout,
140+
stderr,
141+
})
142+
}
143+
144+
#[module_command_handler(shell_script)]
87145
fn execute<R: Runtime>(
88146
context: InvokeContext<R>,
89147
program: String,
90148
args: ExecuteArgs,
91149
on_event_fn: CallbackFn,
92150
options: CommandOptions,
93151
) -> super::Result<ChildId> {
94-
let mut command = if options.sidecar {
95-
#[cfg(not(shell_sidecar))]
96-
return Err(crate::Error::ApiNotAllowlisted("shell > sidecar".to_string()).into_anyhow());
97-
#[cfg(shell_sidecar)]
98-
{
99-
let program = PathBuf::from(program);
100-
let program_as_string = program.display().to_string();
101-
let program_no_ext_as_string = program.with_extension("").display().to_string();
102-
let configured_sidecar = context
103-
.config
104-
.tauri
105-
.bundle
106-
.external_bin
107-
.as_ref()
108-
.map(|bins| {
109-
bins
110-
.iter()
111-
.find(|b| b == &&program_as_string || b == &&program_no_ext_as_string)
112-
})
113-
.unwrap_or_default();
114-
if let Some(sidecar) = configured_sidecar {
115-
context
116-
.window
117-
.state::<Scopes>()
118-
.shell
119-
.prepare_sidecar(&program.to_string_lossy(), sidecar, args)
120-
.map_err(crate::error::into_anyhow)?
121-
} else {
122-
return Err(crate::Error::SidecarNotAllowed(program).into_anyhow());
123-
}
124-
}
125-
} else {
126-
#[cfg(not(shell_execute))]
127-
return Err(crate::Error::ApiNotAllowlisted("shell > execute".to_string()).into_anyhow());
128-
#[cfg(shell_execute)]
129-
match context
130-
.window
131-
.state::<Scopes>()
132-
.shell
133-
.prepare(&program, args)
134-
{
135-
Ok(cmd) => cmd,
136-
Err(e) => {
137-
#[cfg(debug_assertions)]
138-
eprintln!("{e}");
139-
return Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)).into_anyhow());
140-
}
141-
}
142-
};
143-
#[cfg(any(shell_execute, shell_sidecar))]
144-
{
145-
if let Some(cwd) = options.cwd {
146-
command = command.current_dir(cwd);
147-
}
148-
if let Some(env) = options.env {
149-
command = command.envs(env);
150-
} else {
151-
command = command.env_clear();
152-
}
153-
if let Some(encoding) = options.encoding {
154-
if let Some(encoding) = crate::api::process::Encoding::for_label(encoding.as_bytes()) {
155-
command = command.encoding(encoding);
156-
} else {
157-
return Err(anyhow::anyhow!(format!("unknown encoding {encoding}")));
158-
}
159-
}
160-
let (mut rx, child) = command.spawn()?;
152+
use std::future::Future;
153+
use std::pin::Pin;
161154

162-
let pid = child.pid();
163-
command_child_store().lock().unwrap().insert(pid, child);
155+
let command = prepare_cmd(&context, &program, args, options)?;
164156

165-
crate::async_runtime::spawn(async move {
166-
while let Some(event) = rx.recv().await {
167-
if matches!(event, crate::api::process::CommandEvent::Terminated(_)) {
168-
command_child_store().lock().unwrap().remove(&pid);
169-
}
170-
let js = crate::api::ipc::format_callback(on_event_fn, &event)
171-
.expect("unable to serialize CommandEvent");
157+
let (mut rx, child) = command.spawn()?;
172158

173-
let _ = context.window.eval(js.as_str());
159+
let pid = child.pid();
160+
command_child_store().lock().unwrap().insert(pid, child);
161+
162+
crate::async_runtime::spawn(async move {
163+
while let Some(event) = rx.recv().await {
164+
if matches!(event, crate::api::process::CommandEvent::Terminated(_)) {
165+
command_child_store().lock().unwrap().remove(&pid);
174166
}
175-
});
167+
let js = crate::api::ipc::format_callback(on_event_fn, &event)
168+
.expect("unable to serialize CommandEvent");
176169

177-
Ok(pid)
178-
}
170+
let _ = context.window.eval(js.as_str());
171+
}
172+
});
173+
174+
Ok(pid)
179175
}
180176

181177
#[module_command_handler(shell_script)]
@@ -226,6 +222,81 @@ impl Cmd {
226222
}
227223
}
228224

225+
fn prepare_cmd<R: Runtime>(
226+
context: &InvokeContext<R>,
227+
program: &String,
228+
args: ExecuteArgs,
229+
options: CommandOptions,
230+
) -> super::Result<crate::api::process::Command> {
231+
let mut command = if options.sidecar {
232+
#[cfg(not(shell_sidecar))]
233+
return Err(crate::Error::ApiNotAllowlisted("shell > sidecar".to_string()).into_anyhow());
234+
#[cfg(shell_sidecar)]
235+
{
236+
let program = PathBuf::from(program);
237+
let program_as_string = program.display().to_string();
238+
let program_no_ext_as_string = program.with_extension("").display().to_string();
239+
let configured_sidecar = context
240+
.config
241+
.tauri
242+
.bundle
243+
.external_bin
244+
.as_ref()
245+
.map(|bins| {
246+
bins
247+
.iter()
248+
.find(|b| b == &&program_as_string || b == &&program_no_ext_as_string)
249+
})
250+
.unwrap_or_default();
251+
if let Some(sidecar) = configured_sidecar {
252+
context
253+
.window
254+
.state::<Scopes>()
255+
.shell
256+
.prepare_sidecar(&program.to_string_lossy(), sidecar, args)
257+
.map_err(crate::error::into_anyhow)
258+
} else {
259+
Err(crate::Error::SidecarNotAllowed(program).into_anyhow())
260+
}
261+
}
262+
} else {
263+
#[cfg(not(shell_execute))]
264+
return Err(crate::Error::ApiNotAllowlisted("shell > execute".to_string()).into_anyhow());
265+
#[cfg(shell_execute)]
266+
match context
267+
.window
268+
.state::<Scopes>()
269+
.shell
270+
.prepare(program, args)
271+
{
272+
Ok(cmd) => Ok(cmd),
273+
Err(e) => {
274+
#[cfg(debug_assertions)]
275+
eprintln!("{e}");
276+
Err(crate::Error::ProgramNotAllowed(PathBuf::from(program)).into_anyhow())
277+
}
278+
}
279+
}?;
280+
281+
if let Some(cwd) = options.cwd {
282+
command = command.current_dir(cwd);
283+
}
284+
if let Some(env) = options.env {
285+
command = command.envs(env);
286+
} else {
287+
command = command.env_clear();
288+
}
289+
if let Some(encoding) = &options.encoding {
290+
if let Some(encoding) = crate::api::process::Encoding::for_label(encoding.as_bytes()) {
291+
command = command.encoding(encoding);
292+
} else {
293+
return Err(anyhow::anyhow!(format!("unknown encoding {encoding}")));
294+
}
295+
}
296+
297+
Ok(command)
298+
}
299+
229300
#[cfg(test)]
230301
mod tests {
231302
use super::{Buffer, ChildId, CommandOptions, ExecuteArgs};

0 commit comments

Comments
 (0)