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) => Ok(exe_dir.join(command).with_extension("exe")),
122 #[cfg(not(windows))]
123 Some(exe_dir) => Ok(exe_dir.join(command)),
124 None => Err(crate::Error::CurrentExeHasNoParent),
125 }
126}
127
128impl From<Command> for StdCommand {
129 fn from(cmd: Command) -> StdCommand {
130 cmd.cmd
131 }
132}
133
134impl Command {
135 pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
136 let mut command = StdCommand::new(program);
137
138 command.stdout(Stdio::piped());
139 command.stdin(Stdio::piped());
140 command.stderr(Stdio::piped());
141 #[cfg(windows)]
142 command.creation_flags(CREATE_NO_WINDOW);
143
144 Self {
145 cmd: command,
146 raw_out: false,
147 }
148 }
149
150 pub(crate) fn new_sidecar<S: AsRef<Path>>(program: S) -> crate::Result<Self> {
151 Ok(Self::new(relative_command_path(program.as_ref())?))
152 }
153
154 #[must_use]
156 pub fn arg<S: AsRef<OsStr>>(mut self, arg: S) -> Self {
157 self.cmd.arg(arg);
158 self
159 }
160
161 #[must_use]
163 pub fn args<I, S>(mut self, args: I) -> Self
164 where
165 I: IntoIterator<Item = S>,
166 S: AsRef<OsStr>,
167 {
168 self.cmd.args(args);
169 self
170 }
171
172 #[must_use]
174 pub fn env_clear(mut self) -> Self {
175 self.cmd.env_clear();
176 self
177 }
178
179 #[must_use]
181 pub fn env<K, V>(mut self, key: K, value: V) -> Self
182 where
183 K: AsRef<OsStr>,
184 V: AsRef<OsStr>,
185 {
186 self.cmd.env(key, value);
187 self
188 }
189
190 #[must_use]
192 pub fn envs<I, K, V>(mut self, envs: I) -> Self
193 where
194 I: IntoIterator<Item = (K, V)>,
195 K: AsRef<OsStr>,
196 V: AsRef<OsStr>,
197 {
198 self.cmd.envs(envs);
199 self
200 }
201
202 #[must_use]
204 pub fn current_dir<P: AsRef<Path>>(mut self, current_dir: P) -> Self {
205 self.cmd.current_dir(current_dir);
206 self
207 }
208
209 pub fn set_raw_out(mut self, raw_out: bool) -> Self {
211 self.raw_out = raw_out;
212 self
213 }
214
215 pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
246 let raw = self.raw_out;
247 let mut command: StdCommand = self.into();
248 let (stdout_reader, stdout_writer) = pipe()?;
249 let (stderr_reader, stderr_writer) = pipe()?;
250 let (stdin_reader, stdin_writer) = pipe()?;
251 command.stdout(stdout_writer);
252 command.stderr(stderr_writer);
253 command.stdin(stdin_reader);
254
255 let shared_child = SharedChild::spawn(&mut command)?;
256 let child = Arc::new(shared_child);
257 let child_ = child.clone();
258 let guard = Arc::new(RwLock::new(()));
259
260 let (tx, rx) = channel(1);
261
262 spawn_pipe_reader(
263 tx.clone(),
264 guard.clone(),
265 stdout_reader,
266 CommandEvent::Stdout,
267 raw,
268 );
269 spawn_pipe_reader(
270 tx.clone(),
271 guard.clone(),
272 stderr_reader,
273 CommandEvent::Stderr,
274 raw,
275 );
276
277 spawn(move || {
278 let _ = match child_.wait() {
279 Ok(status) => {
280 let _l = guard.write().unwrap();
281 block_on_task(async move {
282 tx.send(CommandEvent::Terminated(TerminatedPayload {
283 code: status.code(),
284 #[cfg(windows)]
285 signal: None,
286 #[cfg(unix)]
287 signal: status.signal(),
288 }))
289 .await
290 })
291 }
292 Err(e) => {
293 let _l = guard.write().unwrap();
294 block_on_task(async move { tx.send(CommandEvent::Error(e.to_string())).await })
295 }
296 };
297 });
298
299 Ok((
300 rx,
301 CommandChild {
302 inner: child,
303 stdin_writer,
304 },
305 ))
306 }
307
308 pub async fn status(self) -> crate::Result<ExitStatus> {
322 let (mut rx, _child) = self.spawn()?;
323 let mut code = None;
324 #[allow(clippy::collapsible_match)]
325 while let Some(event) = rx.recv().await {
326 if let CommandEvent::Terminated(payload) = event {
327 code = payload.code;
328 }
329 }
330 Ok(ExitStatus { code })
331 }
332
333 pub async fn output(self) -> crate::Result<Output> {
349 let (mut rx, _child) = self.spawn()?;
350
351 let mut code = None;
352 let mut stdout = Vec::new();
353 let mut stderr = Vec::new();
354
355 while let Some(event) = rx.recv().await {
356 match event {
357 CommandEvent::Terminated(payload) => {
358 code = payload.code;
359 }
360 CommandEvent::Stdout(line) => {
361 stdout.extend(line);
362 stdout.push(NEWLINE_BYTE);
363 }
364 CommandEvent::Stderr(line) => {
365 stderr.extend(line);
366 stderr.push(NEWLINE_BYTE);
367 }
368 CommandEvent::Error(_) => {}
369 }
370 }
371 Ok(Output {
372 status: ExitStatus { code },
373 stdout,
374 stderr,
375 })
376 }
377}
378
379fn read_raw_bytes<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
380 mut reader: BufReader<PipeReader>,
381 tx: Sender<CommandEvent>,
382 wrapper: F,
383) {
384 loop {
385 let result = reader.fill_buf();
386 match result {
387 Ok(buf) => {
388 let length = buf.len();
389 if length == 0 {
390 break;
391 }
392 let tx_ = tx.clone();
393 let _ = block_on_task(async move { tx_.send(wrapper(buf.to_vec())).await });
394 reader.consume(length);
395 }
396 Err(e) => {
397 let tx_ = tx.clone();
398 let _ = block_on_task(
399 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
400 );
401 }
402 }
403 }
404}
405
406fn read_line<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
407 mut reader: BufReader<PipeReader>,
408 tx: Sender<CommandEvent>,
409 wrapper: F,
410) {
411 loop {
412 let mut buf = Vec::new();
413 match tauri::utils::io::read_line(&mut reader, &mut buf) {
414 Ok(n) => {
415 if n == 0 {
416 break;
417 }
418 let tx_ = tx.clone();
419 let _ = block_on_task(async move { tx_.send(wrapper(buf)).await });
420 }
421 Err(e) => {
422 let tx_ = tx.clone();
423 let _ = block_on_task(
424 async move { tx_.send(CommandEvent::Error(e.to_string())).await },
425 );
426 break;
427 }
428 }
429 }
430}
431
432fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
433 tx: Sender<CommandEvent>,
434 guard: Arc<RwLock<()>>,
435 pipe_reader: PipeReader,
436 wrapper: F,
437 raw_out: bool,
438) {
439 spawn(move || {
440 let _lock = guard.read().unwrap();
441 let reader = BufReader::new(pipe_reader);
442
443 if raw_out {
444 read_raw_bytes(reader, tx, wrapper);
445 } else {
446 read_line(reader, tx, wrapper);
447 }
448 });
449}
450
451#[cfg(test)]
453mod tests {
454 #[cfg(not(windows))]
455 use super::*;
456
457 #[cfg(not(windows))]
458 #[test]
459 fn test_cmd_spawn_output() {
460 let cmd = Command::new("cat").args(["test/test.txt"]);
461 let (mut rx, _) = cmd.spawn().unwrap();
462
463 tauri::async_runtime::block_on(async move {
464 while let Some(event) = rx.recv().await {
465 match event {
466 CommandEvent::Terminated(payload) => {
467 assert_eq!(payload.code, Some(0));
468 }
469 CommandEvent::Stdout(line) => {
470 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
471 }
472 _ => {}
473 }
474 }
475 });
476 }
477
478 #[cfg(not(windows))]
479 #[test]
480 fn test_cmd_spawn_raw_output() {
481 let cmd = Command::new("cat").args(["test/test.txt"]);
482 let (mut rx, _) = cmd.spawn().unwrap();
483
484 tauri::async_runtime::block_on(async move {
485 while let Some(event) = rx.recv().await {
486 match event {
487 CommandEvent::Terminated(payload) => {
488 assert_eq!(payload.code, Some(0));
489 }
490 CommandEvent::Stdout(line) => {
491 assert_eq!(String::from_utf8(line).unwrap(), "This is a test doc!");
492 }
493 _ => {}
494 }
495 }
496 });
497 }
498
499 #[cfg(not(windows))]
500 #[test]
501 fn test_cmd_spawn_fail() {
503 let cmd = Command::new("cat").args(["test/"]);
504 let (mut rx, _) = cmd.spawn().unwrap();
505
506 tauri::async_runtime::block_on(async move {
507 while let Some(event) = rx.recv().await {
508 match event {
509 CommandEvent::Terminated(payload) => {
510 assert_eq!(payload.code, Some(1));
511 }
512 CommandEvent::Stderr(line) => {
513 assert_eq!(
514 String::from_utf8(line).unwrap(),
515 "cat: test/: Is a directory\n"
516 );
517 }
518 _ => {}
519 }
520 }
521 });
522 }
523
524 #[cfg(not(windows))]
525 #[test]
526 fn test_cmd_spawn_raw_fail() {
528 let cmd = Command::new("cat").args(["test/"]);
529 let (mut rx, _) = cmd.spawn().unwrap();
530
531 tauri::async_runtime::block_on(async move {
532 while let Some(event) = rx.recv().await {
533 match event {
534 CommandEvent::Terminated(payload) => {
535 assert_eq!(payload.code, Some(1));
536 }
537 CommandEvent::Stderr(line) => {
538 assert_eq!(
539 String::from_utf8(line).unwrap(),
540 "cat: test/: Is a directory\n"
541 );
542 }
543 _ => {}
544 }
545 }
546 });
547 }
548
549 #[cfg(not(windows))]
550 #[test]
551 fn test_cmd_output_output() {
552 let cmd = Command::new("cat").args(["test/test.txt"]);
553 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
554
555 assert_eq!(String::from_utf8(output.stderr).unwrap(), "");
556 assert_eq!(
557 String::from_utf8(output.stdout).unwrap(),
558 "This is a test doc!\n"
559 );
560 }
561
562 #[cfg(not(windows))]
563 #[test]
564 fn test_cmd_output_output_fail() {
565 let cmd = Command::new("cat").args(["test/"]);
566 let output = tauri::async_runtime::block_on(cmd.output()).unwrap();
567
568 assert_eq!(String::from_utf8(output.stdout).unwrap(), "");
569 assert_eq!(
570 String::from_utf8(output.stderr).unwrap(),
571 "cat: test/: Is a directory\n\n"
572 );
573 }
574}