tauri_plugin_shell/process/
mod.rs1use std::{
6 ffi::OsStr,
7 io::{BufRead, BufReader, Write},
8 path::{Path, PathBuf},
9 process::{Command as StdCommand, Stdio},
10 sync::{Arc, RwLock},
11 thread::spawn,
12};
13
14#[cfg(unix)]
15use std::os::unix::process::ExitStatusExt;
16#[cfg(windows)]
17use std::os::windows::process::CommandExt;
18
19#[cfg(windows)]
20const CREATE_NO_WINDOW: u32 = 0x0800_0000;
21const NEWLINE_BYTE: u8 = b'\n';
22
23use tauri::async_runtime::{block_on as block_on_task, channel, Receiver, Sender};
24
25pub use encoding_rs::Encoding;
26use os_pipe::{pipe, PipeReader, PipeWriter};
27use serde::Serialize;
28use shared_child::SharedChild;
29use tauri::utils::platform;
30
31#[derive(Debug, Clone, Serialize)]
33pub struct TerminatedPayload {
34 pub code: Option<i32>,
36 pub signal: Option<i32>,
38}
39
40#[derive(Debug, Clone)]
42#[non_exhaustive]
43pub enum CommandEvent {
44 Stderr(Vec<u8>),
47 Stdout(Vec<u8>),
50 Error(String),
52 Terminated(TerminatedPayload),
54}
55
56#[derive(Debug)]
58pub struct Command {
59 cmd: StdCommand,
60 raw_out: bool,
61}
62
63#[derive(Debug)]
65pub struct CommandChild {
66 inner: Arc<SharedChild>,
67 stdin_writer: PipeWriter,
68}
69
70impl CommandChild {
71 pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
73 self.stdin_writer.write_all(buf)?;
74 Ok(())
75 }
76
77 pub fn kill(self) -> crate::Result<()> {
79 self.inner.kill()?;
80 Ok(())
81 }
82
83 pub fn pid(&self) -> u32 {
85 self.inner.id()
86 }
87}
88
89#[derive(Debug)]
91pub struct ExitStatus {
92 code: Option<i32>,
93}
94
95impl ExitStatus {
96 pub fn code(&self) -> Option<i32> {
98 self.code
99 }
100
101 pub fn success(&self) -> bool {
103 self.code == Some(0)
104 }
105}
106
107#[derive(Debug)]
109pub struct Output {
110 pub status: ExitStatus,
112 pub stdout: Vec<u8>,
114 pub stderr: Vec<u8>,
116}
117
118fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
119 match platform::current_exe()?.parent() {
120 #[cfg(windows)]
121 Some(exe_dir) => {
122 let mut command_path = exe_dir.join(command);
123 let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
124 if !already_exe {
125 command_path.as_mut_os_string().push(".exe");
127 }
128 Ok(command_path)
129 }
130 #[cfg(not(windows))]
131 Some(exe_dir) => {
132 let mut command_path = exe_dir.join(command);
133 if command_path.extension().is_some_and(|ext| ext == "exe") {
134 command_path.set_extension("");
135 }
136 Ok(command_path)
137 }
138 None => Err(crate::Error::CurrentExeHasNoParent),
139 }
140}
141
142impl From<Command> for StdCommand {
143 fn from(cmd: Command) -> StdCommand {
144 cmd.cmd
145 }
146}
147
148impl Command {
149 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
150 log::debug!(
151 "Creating sidecar {}",
152 program.as_ref().to_str().unwrap_or("")
153 );
154 let mut command = StdCommand::new(program);
155
156 command.stdout(Stdio::piped());
157 command.stdin(Stdio::piped());
158 command.stderr(Stdio::piped());
159 #[cfg(windows)]
160 command.creation_flags(CREATE_NO_WINDOW);
161
162 Self {
163 cmd: command,
164 raw_out: false,
165 }
166 }
167
168 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
169 Ok(Self::new(relative_command_path(program.as_ref())?))
170 }
171
172 #[must_use]
174 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
175 self.cmd.arg(arg);
176 self
177 }
178
179 #[must_use]
181 pub fn args<I, S>(mut self, args: I) -> Self
182 where
183 I: IntoIterator<Item = S>,
184 S: AsRef<OsStr>,
185 {
186 self.cmd.args(args);
187 self
188 }
189
190 #[must_use]
192 pub fn env_clear(mut self) -> Self {
193 self.cmd.env_clear();
194 self
195 }
196
197 #[must_use]
199 pub fn env<K, V>(mut self, key: K, value: V) -> Self
200 where
201 K: AsRef<OsStr>,
202 V: AsRef<OsStr>,
203 {
204 self.cmd.env(key, value);
205 self
206 }
207
208 #[must_use]
210 pub fn envs<I, K, V>(mut self, envs: I) -> Self
211 where
212 I: IntoIterator<Item = (K, V)>,
213 K: AsRef<OsStr>,
214 V: AsRef<OsStr>,
215 {
216 self.cmd.envs(envs);
217 self
218 }
219
220 #[must_use]
222 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
223 self.cmd.current_dir(current_dir);
224 self
225 }
226
227 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
229 self.raw_out = raw_out;
230 self
231 }
232
233 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
264 let raw = self.raw_out;
265 let mut command: StdCommand = self.into();
266 let (stdout_reader, stdout_writer) = pipe()?;
267 let (stderr_reader, stderr_writer) = pipe()?;
268 let (stdin_reader, stdin_writer) = pipe()?;
269 command.stdout(stdout_writer);
270 command.stderr(stderr_writer);
271 command.stdin(stdin_reader);
272
273 let shared_child = SharedChild::spawn(&mut command)?;
274 let child = Arc::new(shared_child);
275 let child_ = child.clone();
276 let guard = Arc::new(RwLock::new(()));
277
278 let (tx, rx) = channel(1);
279
280 spawn_pipe_reader(
281 tx.clone(),
282 guard.clone(),
283 stdout_reader,
284 CommandEvent::Stdout,
285 raw,
286 );
287 spawn_pipe_reader(
288 tx.clone(),
289 guard.clone(),
290 stderr_reader,
291 CommandEvent::Stderr,
292 raw,
293 );
294
295 spawn(move || {
296 let _ = match child_.wait() {
297 Ok(status) => {
298 let _l = guard.write().unwrap();
299 block_on_task(async move {
300 tx.send(CommandEvent::Terminated(TerminatedPayload {
301 code: status.code(),
302 #[cfg(windows)]
303 signal: None,
304 #[cfg(unix)]
305 signal: status.signal(),
306 }))
307 .await
308 })
309 }
310 Err(e) => {
311 let _l = guard.write().unwrap();
312 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
313 }
314 };
315 });
316
317 Ok((
318 rx,
319 CommandChild {
320 inner: child,
321 stdin_writer,
322 },
323 ))
324 }
325
326 pub async fn status(self) -> crate::Result<ExitStatus> {
340 let (mut rx, _child) = self.spawn()?;
341 let mut code = None;
342 #[allow(clippy::collapsible_match)]
343 while let Some(event) = rx.recv().await {
344 if let CommandEvent::Terminated(payload) = event {
345 code = payload.code;
346 }
347 }
348 Ok(ExitStatus { code })
349 }
350
351 pub async fn output(self) -> crate::Result<Output> {
367 let (mut rx, _child) = self.spawn()?;
368
369 let mut code = None;
370 let mut stdout = Vec::new();
371 let mut stderr = Vec::new();
372
373 while let Some(event) = rx.recv().await {
374 match event {
375 CommandEvent::Terminated(payload) => {
376 code = payload.code;
377 }
378 CommandEvent::Stdout(line) => {
379 stdout.extend(line);
380 stdout.push(NEWLINE_BYTE);
381 }
382 CommandEvent::Stderr(line) => {
383 stderr.extend(line);
384 stderr.push(NEWLINE_BYTE);
385 }
386 CommandEvent::Error(_) => {}
387 }
388 }
389 Ok(Output {
390 status: ExitStatus { code },
391 stdout,
392 stderr,
393 })
394 }
395}
396
397fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
398 mut reader: BufReader<PipeReader>,
399 tx: Sender<CommandEvent>,
400 wrapper: F,
401) {
402 loop {
403 let result = reader.fill_buf();
404 match result {
405 Ok(buf) => {
406 let length = buf.len();
407 if length == 0 {
408 break;
409 }
410 let tx_ = tx.clone();
411 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
412 reader.consume(length);
413 }
414 Err(e) => {
415 let tx_ = tx.clone();
416 let _ = block_on_task(
417 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
418 );
419 }
420 }
421 }
422}
423
424fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
425 mut reader: BufReader<PipeReader>,
426 tx: Sender<CommandEvent>,
427 wrapper: F,
428) {
429 loop {
430 let mut buf = Vec::new();
431 match tauri::utils::io::read_line(&mut reader, &mut buf) {
432 Ok(n) => {
433 if n == 0 {
434 break;
435 }
436 let tx_ = tx.clone();
437 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
438 }
439 Err(e) => {
440 let tx_ = tx.clone();
441 let _ = block_on_task(
442 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
443 );
444 break;
445 }
446 }
447 }
448}
449
450fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
451 tx: Sender<CommandEvent>,
452 guard: Arc<RwLock<()>>,
453 pipe_reader: PipeReader,
454 wrapper: F,
455 raw_out: bool,
456) {
457 spawn(move || {
458 let _lock = guard.read().unwrap();
459 let reader = BufReader::new(pipe_reader);
460
461 if raw_out {
462 read_raw_bytes(reader, tx, wrapper);
463 } else {
464 read_line(reader, tx, wrapper);
465 }
466 });
467}
468
469#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn relative_command_path_resolves() {
476 let cwd_parent = platform::current_exe()
477 .unwrap()
478 .parent()
479 .unwrap()
480 .to_owned();
481 assert_eq!(
482 relative_command_path(Path::new("Tauri.Example")).unwrap(),
483 cwd_parent.join(if cfg!(windows) {
484 "Tauri.Example.exe"
485 } else {
486 "Tauri.Example"
487 })
488 );
489 assert_eq!(
490 relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
491 cwd_parent.join(if cfg!(windows) {
492 "Tauri.Example.exe"
493 } else {
494 "Tauri.Example"
495 })
496 );
497 }
498
499 #[cfg(not(windows))]
500 #[test]
501 fn test_cmd_spawn_output() {
502 let cmd = Command::new("cat").args(["test/test.txt"]);
503 let (mut rx, _) = cmd.spawn().unwrap();
504
505 tauri::async_runtime::block_on(async move {
506 while let Some(event) = rx.recv().await {
507 match event {
508 CommandEvent::Terminated(payload) => {
509 assert_eq!(payload.code, Some(0));
510 }
511 CommandEvent::Stdout(line) => {
512 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
513 }
514 _ => {}
515 }
516 }
517 });
518 }
519
520 #[cfg(not(windows))]
521 #[test]
522 fn test_cmd_spawn_raw_output() {
523 let cmd = Command::new("cat").args(["test/test.txt"]);
524 let (mut rx, _) = cmd.spawn().unwrap();
525
526 tauri::async_runtime::block_on(async move {
527 while let Some(event) = rx.recv().await {
528 match event {
529 CommandEvent::Terminated(payload) => {
530 assert_eq!(payload.code, Some(0));
531 }
532 CommandEvent::Stdout(line) => {
533 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
534 }
535 _ => {}
536 }
537 }
538 });
539 }
540
541 #[cfg(not(windows))]
542 #[test]
543 fn test_cmd_spawn_fail() {
545 let cmd = Command::new("cat").args(["test/"]);
546 let (mut rx, _) = cmd.spawn().unwrap();
547
548 tauri::async_runtime::block_on(async move {
549 while let Some(event) = rx.recv().await {
550 match event {
551 CommandEvent::Terminated(payload) => {
552 assert_eq!(payload.code, Some(1));
553 }
554 CommandEvent::Stderr(line) => {
555 assert_eq!(
556 String::from_utf8(line).unwrap(),
557 "cat: test/: Is a directory\n"
558 );
559 }
560 _ => {}
561 }
562 }
563 });
564 }
565
566 #[cfg(not(windows))]
567 #[test]
568 fn test_cmd_spawn_raw_fail() {
570 let cmd = Command::new("cat").args(["test/"]);
571 let (mut rx, _) = cmd.spawn().unwrap();
572
573 tauri::async_runtime::block_on(async move {
574 while let Some(event) = rx.recv().await {
575 match event {
576 CommandEvent::Terminated(payload) => {
577 assert_eq!(payload.code, Some(1));
578 }
579 CommandEvent::Stderr(line) => {
580 assert_eq!(
581 String::from_utf8(line).unwrap(),
582 "cat: test/: Is a directory\n"
583 );
584 }
585 _ => {}
586 }
587 }
588 });
589 }
590
591 #[cfg(not(windows))]
592 #[test]
593 fn test_cmd_output_output() {
594 let cmd = Command::new("cat").args(["test/test.txt"]);
595 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
596
597 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
598 assert_eq!(
599 String::from_utf8(output.stdout).unwrap(),
600 "This is a test doc!\n"
601 );
602 }
603
604 #[cfg(not(windows))]
605 #[test]
606 fn test_cmd_output_output_fail() {
607 let cmd = Command::new("cat").args(["test/"]);
608 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
609
610 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
611 assert_eq!(
612 String::from_utf8(output.stderr).unwrap(),
613 "cat: test/: Is a directory\n\n"
614 );
615 }
616}