瀏覽代碼

refactor(core): rewrite shell execute API, closes #1229 (#1408)

Lucas Fernandes Nogueira 4 年之前
父節點
當前提交
3713066e45

+ 6 - 0
.changes/command-refactor.md

@@ -0,0 +1,6 @@
+---
+"tauri": minor
+"api": minor
+---
+
+The shell process spawning API was rewritten and now includes stream access.

+ 168 - 6
api/src/shell.ts

@@ -1,30 +1,192 @@
 import { invokeTauriCommand } from './helpers/tauri'
+import { transformCallback } from './tauri'
 
 /**
  * spawns a process
  *
- * @param command the name of the cmd to execute e.g. 'mkdir' or 'node'
+ * @param program the name of the program to execute e.g. 'mkdir' or 'node'
+ * @param sidecar whether the program is a sidecar or a system program
  * @param [args] command args
  * @return promise resolving to the stdout text
  */
 async function execute(
-  command: string,
+  program: string,
+  sidecar: boolean,
+  onEvent: (event: CommandEvent) => void,
   args?: string | string[]
-): Promise<string> {
+): Promise<number> {
   if (typeof args === 'object') {
     Object.freeze(args)
   }
 
-  return invokeTauriCommand<string>({
+  return invokeTauriCommand<number>({
     __tauriModule: 'Shell',
     message: {
       cmd: 'execute',
-      command,
+      program,
+      sidecar,
+      onEventFn: transformCallback(onEvent),
       args: typeof args === 'string' ? [args] : args
     }
   })
 }
 
+interface ChildProcess {
+  code: number | null
+  signal: number | null
+  stdout: string
+  stderr: string
+}
+
+class EventEmitter<E> {
+  eventListeners: { [key: string]: Array<(arg: any) => void> } = {}
+
+  private addEventListener(event: string, handler: (arg: any) => void): void {
+    if (event in this.eventListeners) {
+      // eslint-disable-next-line security/detect-object-injection
+      this.eventListeners[event].push(handler)
+    } else {
+      // eslint-disable-next-line security/detect-object-injection
+      this.eventListeners[event] = [handler]
+    }
+  }
+
+  _emit(event: E, payload: any): void {
+    if (event in this.eventListeners) {
+      // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+      const listeners = this.eventListeners[event as any]
+      for (const listener of listeners) {
+        listener(payload)
+      }
+    }
+  }
+
+  on(event: E, handler: (arg: any) => void): EventEmitter<E> {
+    this.addEventListener(event as any, handler)
+    return this
+  }
+}
+
+class Child {
+  pid: number
+
+  constructor(pid: number) {
+    this.pid = pid
+  }
+
+  async write(data: string | number[]): Promise<void> {
+    return invokeTauriCommand({
+      __tauriModule: 'Shell',
+      message: {
+        cmd: 'stdinWrite',
+        pid: this.pid,
+        buffer: data
+      }
+    })
+  }
+
+  async kill(): Promise<void> {
+    return invokeTauriCommand({
+      __tauriModule: 'Shell',
+      message: {
+        cmd: 'killChild',
+        pid: this.pid
+      }
+    })
+  }
+}
+
+class Command extends EventEmitter<'close' | 'error'> {
+  program: string
+  args: string[]
+  sidecar = false
+  stdout = new EventEmitter<'data'>()
+  stderr = new EventEmitter<'data'>()
+  pid: number | null = null
+
+  constructor(program: string, args: string | string[] = []) {
+    super()
+    this.program = program
+    this.args = typeof args === 'string' ? [args] : args
+  }
+
+  /**
+   * Creates a command to execute the given sidecar binary
+   *
+   * @param {string} program  Binary name
+   *
+   * @return {Command}
+   */
+  static sidecar(program: string, args: string | string[] = []): Command {
+    const instance = new Command(program, args)
+    instance.sidecar = true
+    return instance
+  }
+
+  async spawn(): Promise<Child> {
+    return execute(
+      this.program,
+      this.sidecar,
+      (event) => {
+        switch (event.event) {
+          case 'Error':
+            this._emit('error', event.payload)
+            break
+          case 'Terminated':
+            this._emit('close', event.payload)
+            break
+          case 'Stdout':
+            this.stdout._emit('data', event.payload)
+            break
+          case 'Stderr':
+            this.stderr._emit('data', event.payload)
+            break
+        }
+      },
+      this.args
+    ).then((pid) => new Child(pid))
+  }
+
+  async execute(): Promise<ChildProcess> {
+    return new Promise((resolve, reject) => {
+      this.on('error', reject)
+      const stdout: string[] = []
+      const stderr: string[] = []
+      this.stdout.on('data', (line) => {
+        stdout.push(line)
+      })
+      this.stderr.on('data', (line) => {
+        stderr.push(line)
+      })
+      this.on('close', (payload: TerminatedPayload) => {
+        resolve({
+          code: payload.code,
+          signal: payload.signal,
+          stdout: stdout.join('\n'),
+          stderr: stderr.join('\n')
+        })
+      })
+      this.spawn().catch(reject)
+    })
+  }
+}
+
+interface Event<T, V> {
+  event: T
+  payload: V
+}
+
+interface TerminatedPayload {
+  code: number | null
+  signal: number | null
+}
+
+type CommandEvent =
+  | Event<'Stdout', string>
+  | Event<'Stderr', string>
+  | Event<'Terminated', TerminatedPayload>
+  | Event<'Error', string>
+
 /**
  * opens a path or URL with the system's default app,
  * or the one specified with `openWith`
@@ -43,4 +205,4 @@ async function open(path: string, openWith?: string): Promise<void> {
   })
 }
 
-export { execute, open }
+export { Command, Child, open }

文件差異過大導致無法顯示
+ 0 - 0
examples/api/public/build/bundle.js


文件差異過大導致無法顯示
+ 0 - 0
examples/api/public/build/bundle.js.map


+ 7 - 2
examples/api/src/App.svelte

@@ -2,6 +2,7 @@
   import { onMount } from "svelte";
   import { open } from "@tauri-apps/api/shell";
 
+  import Welcome from "./components/Welcome.svelte";
   import Cli from "./components/Cli.svelte";
   import Communication from "./components/Communication.svelte";
   import Dialog from "./components/Dialog.svelte";
@@ -10,7 +11,7 @@
   import Notifications from "./components/Notifications.svelte";
   import Window from "./components/Window.svelte";
   import Shortcuts from "./components/Shortcuts.svelte";
-  import Welcome from "./components/Welcome.svelte";
+  import Shell from "./components/Shell.svelte";
 
   const views = [
     {
@@ -49,6 +50,10 @@
       label: "Shortcuts",
       component: Shortcuts,
     },
+    {
+      label: "Shell",
+      component: Shell,
+    }
   ];
 
   let selected = views[0];
@@ -97,7 +102,7 @@
       <svelte:component this={selected.component} {onMessage} />
     </div>
   </div>
-  <div id="response">
+  <div id="response" style="white-space: pre-line">
     <p class="flex row just-around">
       <strong>Tauri Console</strong>
       <a class="nv" on:click={()=> {

+ 52 - 0
examples/api/src/components/Shell.svelte

@@ -0,0 +1,52 @@
+<script>
+  import { Command } from "@tauri-apps/api/shell"
+  const windows = navigator.userAgent.includes('Windows')
+  let cmd = windows ? 'cmd' : 'sh'
+  let args = windows ? ['/C'] : ['-c']
+
+  export let onMessage;
+
+  let script = 'echo "hello world"'
+  let stdin = ''
+  let child
+
+  function spawn() {
+    child = null
+    const command = new Command(cmd, [...args, script])
+
+    command.on('close', data => {
+      onMessage(`command finished with code ${data.code} and signal ${data.signal}`)
+      child = null
+    })
+    command.on('error', error => onMessage(`command error: "${error}"`))
+
+    command.stdout.on('data', line => onMessage(`command stdout: "${line}"`))
+    command.stderr.on('data', line => onMessage(`command stderr: "${line}"`))
+    
+    command.spawn()
+      .then(c => {
+        child = c
+      })
+      .catch(onMessage)
+  }
+
+  function kill() {
+    child.kill().then(() => onMessage('killed child process')).error(onMessage)
+  }
+
+  function writeToStdin() {
+    child.write(stdin).catch(onMessage)
+  }
+</script>
+
+<div>
+  <div>
+    <input bind:value={script}>
+    <button class="button" on:click={spawn}>Run</button>
+    <button class="button" on:click={kill}>Kill</button>
+    {#if child}
+      <input placeholder="write to stdin" bind:value={stdin}>
+      <button class="button" on:click={writeToStdin}>Write</button>
+    {/if}
+  </div>
+</div>

+ 3 - 0
tauri-api/Cargo.toml

@@ -38,6 +38,9 @@ notify-rust = { version = "4.3.0", optional = true }
 once_cell = "1.7.2"
 tauri-hotkey = { git = "https://github.com/tauri-apps/tauri-hotkey-rs", branch = "dev", optional = true }
 open = "1.6.0"
+tokio = { version = "1.3", features = ["rt", "rt-multi-thread", "sync"] }
+shared_child = "0.3"
+os_pipe = "0.9"
 
 [dev-dependencies]
 quickcheck = "1.0.3"

+ 192 - 145
tauri-api/src/command.rs

@@ -1,104 +1,181 @@
-use std::process::{Child, Command, Stdio};
-
+use std::{
+  io::{BufRead, BufReader, Write},
+  process::{Command as StdCommand, Stdio},
+  sync::Arc,
+};
+
+#[cfg(unix)]
+use std::os::unix::process::ExitStatusExt;
 #[cfg(windows)]
 use std::os::windows::process::CommandExt;
 
 #[cfg(windows)]
 const CREATE_NO_WINDOW: u32 = 0x0800_0000;
 
+use crate::private::async_runtime::{channel, spawn, Receiver};
+use os_pipe::{pipe, PipeWriter};
+use serde::Serialize;
+use shared_child::SharedChild;
 use tauri_utils::platform;
 
-/// Gets the output of the given command.
-#[cfg(not(windows))]
-pub fn get_output(cmd: String, args: Vec<String>, stdout: Stdio) -> crate::Result<String> {
-  let output = Command::new(cmd).args(args).stdout(stdout).output()?;
-
-  if output.status.success() {
-    Ok(String::from_utf8_lossy(&output.stdout).to_string())
-  } else {
-    Err(crate::Error::Command(
-      String::from_utf8_lossy(&output.stderr).to_string(),
-    ))
-  }
+/// Payload for the `Terminated` command event.
+#[derive(Serialize)]
+pub struct TerminatedPayload {
+  /// Exit code of the process.
+  pub code: Option<i32>,
+  /// If the process was terminated by a signal, represents that signal.
+  pub signal: Option<i32>,
 }
 
-/// Gets the output of the given command.
-#[cfg(windows)]
-pub fn get_output(cmd: String, args: Vec<String>, stdout: Stdio) -> crate::Result<String> {
-  let output = Command::new(cmd)
-    .args(args)
-    .stdout(stdout)
-    .creation_flags(CREATE_NO_WINDOW)
-    .output()?;
-
-  if output.status.success() {
-    Ok(String::from_utf8_lossy(&output.stdout).to_string())
-  } else {
-    Err(crate::Error::Command(
-      String::from_utf8_lossy(&output.stderr).to_string(),
-    ))
-  }
+/// A event sent to the command callback.
+#[derive(Serialize)]
+#[serde(tag = "event", content = "payload")]
+pub enum CommandEvent {
+  /// Stderr line.
+  Stderr(String),
+  /// Stdout line.
+  Stdout(String),
+  /// An error happened.
+  Error(String),
+  /// Command process terminated.
+  Terminated(TerminatedPayload),
 }
 
-/// Gets the path to command relative to the current executable path.
-#[cfg(not(windows))]
-pub fn command_path(command: String) -> crate::Result<String> {
-  match std::env::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!("{}/{}", exe_dir.display().to_string(), command)),
-    None => Err(crate::Error::Command(
-      "Could not evaluate executable dir".to_string(),
-    )),
-  }
+macro_rules! get_std_command {
+  ($self: ident) => {{
+    let mut command = StdCommand::new($self.program);
+    command.args(&$self.args);
+    command.stdout(Stdio::piped());
+    command.stdin(Stdio::piped());
+    command.stderr(Stdio::piped());
+    #[cfg(windows)]
+    command.creation_flags(CREATE_NO_WINDOW);
+    command
+  }};
 }
 
-/// Gets the path to command relative to the current executable path.
-#[cfg(windows)]
-pub fn command_path(command: String) -> crate::Result<String> {
-  match std::env::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!("{}/{}.exe", exe_dir.display().to_string(), command)),
-    None => Err(crate::Error::Command(
-      "Could not evaluate executable dir".to_string(),
-    )),
-  }
+/// API to spawn commands.
+pub struct Command {
+  program: String,
+  args: Vec<String>,
 }
 
-/// Spawns a process with a command string relative to the current executable path.
-/// For example, if your app bundles two executables, you don't need to worry about its path and just run `second-app`.
-#[cfg(windows)]
-pub fn spawn_relative_command(
-  command: String,
-  args: Vec<String>,
-  stdout: Stdio,
-) -> crate::Result<Child> {
-  let cmd = command_path(command)?;
-  Ok(
-    Command::new(cmd)
-      .args(args)
-      .creation_flags(CREATE_NO_WINDOW)
-      .stdout(stdout)
-      .spawn()?,
-  )
+/// Child spawned.
+pub struct CommandChild {
+  inner: Arc<SharedChild>,
+  stdin_writer: PipeWriter,
 }
 
-/// Spawns a process with a command string relative to the current executable path.
-/// For example, if your app bundles two executables, you don't need to worry about its path and just run `second-app`.
-#[cfg(not(windows))]
-pub fn spawn_relative_command(
-  command: String,
-  args: Vec<String>,
-  stdout: Stdio,
-) -> crate::Result<Child> {
-  let cmd = command_path(command)?;
-  Ok(Command::new(cmd).args(args).stdout(stdout).spawn()?)
+impl CommandChild {
+  /// Write to process stdin.
+  pub fn write(&mut self, buf: &[u8]) -> crate::Result<()> {
+    self.stdin_writer.write_all(buf)?;
+    Ok(())
+  }
+  /// Send a kill signal to the child.
+  pub fn kill(self) -> crate::Result<()> {
+    self.inner.kill()?;
+    Ok(())
+  }
+
+  /// Returns the process pid.
+  pub fn pid(&self) -> u32 {
+    self.inner.id()
+  }
 }
 
-/// Gets the binary command with the current target triple.
-pub fn binary_command(binary_name: String) -> crate::Result<String> {
-  Ok(format!(
-    "{}-{}",
-    binary_name,
-    platform::target_triple().map_err(|e| crate::Error::FailedToDetectPlatform(e.to_string()))?
-  ))
+impl Command {
+  /// Creates a new Command for launching the given program.
+  pub fn new<S: Into<String>>(program: S) -> Self {
+    Self {
+      program: program.into(),
+      args: Default::default(),
+    }
+  }
+
+  /// Creates a new Command for launching the given sidecar program.
+  pub fn new_sidecar<S: Into<String>>(program: S) -> Self {
+    Self::new(format!(
+      "{}-{}",
+      program.into(),
+      platform::target_triple().expect("unsupported platform")
+    ))
+  }
+
+  /// Append args to the command.
+  pub fn args<I, S>(mut self, args: I) -> Self
+  where
+    I: IntoIterator<Item = S>,
+    S: AsRef<str>,
+  {
+    for arg in args {
+      self.args.push(arg.as_ref().to_string());
+    }
+    self
+  }
+
+  /// Spawns the command.
+  pub fn spawn(self) -> crate::Result<(Receiver<CommandEvent>, CommandChild)> {
+    let mut command = get_std_command!(self);
+    let (stdout_reader, stdout_writer) = pipe()?;
+    let (stderr_reader, stderr_writer) = pipe()?;
+    let (stdin_reader, stdin_writer) = pipe()?;
+    command.stdout(stdout_writer);
+    command.stderr(stderr_writer);
+    command.stdin(stdin_reader);
+
+    let shared_child = SharedChild::spawn(&mut command)?;
+    let child = Arc::new(shared_child);
+    let child_ = child.clone();
+
+    let (tx, rx) = channel(1);
+    let tx_ = tx.clone();
+    spawn(async move {
+      let _ = match child_.wait() {
+        Ok(status) => {
+          tx_
+            .send(CommandEvent::Terminated(TerminatedPayload {
+              code: status.code(),
+              #[cfg(windows)]
+              signal: None,
+              #[cfg(unix)]
+              signal: status.signal(),
+            }))
+            .await
+        }
+        Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
+      };
+    });
+
+    let tx_ = tx.clone();
+    spawn(async move {
+      let reader = BufReader::new(stdout_reader);
+      for line in reader.lines() {
+        let _ = match line {
+          Ok(line) => tx_.send(CommandEvent::Stdout(line)).await,
+          Err(e) => tx_.send(CommandEvent::Error(e.to_string())).await,
+        };
+      }
+    });
+
+    spawn(async move {
+      let reader = BufReader::new(stderr_reader);
+      for line in reader.lines() {
+        let _ = match line {
+          Ok(line) => tx.send(CommandEvent::Stderr(line)).await,
+          Err(e) => tx.send(CommandEvent::Error(e.to_string())).await,
+        };
+      }
+    });
+
+    Ok((
+      rx,
+      CommandChild {
+        inner: child,
+        stdin_writer,
+      },
+    ))
+  }
 }
 
 // tests for the commands functions.
@@ -108,75 +185,45 @@ mod test {
 
   #[cfg(not(windows))]
   #[test]
-  // test the get_output function with a unix cat command.
   fn test_cmd_output() {
-    // create a string with cat in it.
-    let cmd = String::from("cat");
-
-    // call get_output with cat and the argument test/test.txt on the stdio.
-    let res = get_output(cmd, vec!["test/test.txt".to_string()], Stdio::piped());
-
-    // assert that the result is an Ok() type
-    assert!(res.is_ok());
-
-    // if the assertion passes, assert the incoming data.
-    if let Ok(s) = &res {
-      // assert that cat returns the string in the test.txt document.
-      assert_eq!(*s, "This is a test doc!".to_string());
-    }
+    // create a command to run cat.
+    let cmd = Command::new("cat").args(&["test/test.txt"]);
+    let (mut rx, _) = cmd.spawn().unwrap();
+
+    crate::private::async_runtime::block_on(async move {
+      while let Some(event) = rx.recv().await {
+        match event {
+          CommandEvent::Terminated(payload) => {
+            assert_eq!(payload.code, Some(0));
+          }
+          CommandEvent::Stdout(line) => {
+            assert_eq!(line, "This is a test doc!".to_string());
+          }
+          _ => {}
+        }
+      }
+    });
   }
 
   #[cfg(not(windows))]
   #[test]
-  // test the failure case for get_output
+  // test the failure case
   fn test_cmd_fail() {
-    use crate::Error;
-
-    // queue up a string with cat in it.
-    let cmd = String::from("cat");
-
-    // call get output with test/ as an argument on the stdio.
-    let res = get_output(cmd, vec!["test/".to_string()], Stdio::piped());
-
-    // assert that the result is an Error type.
-    assert!(res.is_err());
-
-    // destruct the Error to check the ErrorKind and test that it is a Command type.
-    if let Error::Command(e) = res.unwrap_err() {
-      // assert that the message in the error matches this string.
-      assert_eq!(*e, "cat: test/: Is a directory\n".to_string());
-    }
-  }
-
-  #[test]
-  // test the command_path function
-  fn check_command_path() {
-    // generate a string for cat
-    let cmd = String::from("cat");
-
-    // call command_path on cat
-    let res = command_path(cmd);
-
-    // assert that the result is an OK() type.
-    assert!(res.is_ok());
-  }
-
-  #[test]
-  // check the spawn_relative_command function
-  fn check_spawn_cmd() {
-    // generate a cat string
-    let cmd = String::from("cat");
-
-    // call spawn_relative_command with cat and the argument test/test.txt on the Stdio.
-    let res = spawn_relative_command(cmd, vec!["test/test.txt".to_string()], Stdio::piped());
-
-    // this fails because there is no cat binary in the relative parent folder of this current executing command.
-    assert!(res.is_err());
-
-    // after asserting that the result is an error, check that the error kind is ErrorKind::Io
-    if let crate::Error::Io(s) = res.unwrap_err() {
-      // assert that the ErrorKind inside of the ErrorKind Io is ErrorKind::NotFound
-      assert_eq!(s.kind(), std::io::ErrorKind::NotFound);
-    }
+    let cmd = Command::new("cat").args(&["test/"]);
+    let (mut rx, _) = cmd.spawn().unwrap();
+
+    crate::private::async_runtime::block_on(async move {
+      while let Some(event) = rx.recv().await {
+        match event {
+          CommandEvent::Terminated(payload) => {
+            assert_eq!(payload.code, Some(1));
+          }
+          CommandEvent::Stderr(line) => {
+            assert_eq!(line, "cat: test/: Is a directory".to_string());
+          }
+          _ => {}
+        }
+      }
+    });
   }
 }

+ 0 - 3
tauri-api/src/error.rs

@@ -4,9 +4,6 @@ pub enum Error {
   /// The extract archive error.
   #[error("Extract Error: {0}")]
   Extract(String),
-  /// The Command (spawn process) error.
-  #[error("Command Error: {0}")]
-  Command(String),
   /// The path operation error.
   #[error("Path Error: {0}")]
   Path(String),

+ 27 - 0
tauri-api/src/lib.rs

@@ -52,6 +52,33 @@ pub type Result<T> = std::result::Result<T, Error>;
 // Not public API
 #[doc(hidden)]
 pub mod private {
+  // Core API only.
+  pub mod async_runtime {
+    use once_cell::sync::OnceCell;
+    use tokio::runtime::Runtime;
+    pub use tokio::sync::{
+      mpsc::{channel, Receiver, Sender},
+      Mutex,
+    };
+
+    use std::future::Future;
+
+    static RUNTIME: OnceCell<Runtime> = OnceCell::new();
+
+    pub fn block_on<F: Future>(task: F) -> F::Output {
+      let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
+      runtime.block_on(task)
+    }
+
+    pub fn spawn<F>(task: F)
+    where
+      F: Future + Send + 'static,
+      F::Output: Send + 'static,
+    {
+      let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
+      runtime.spawn(task);
+    }
+  }
   pub use once_cell::sync::OnceCell;
 
   pub trait AsTauriContext {

文件差異過大導致無法顯示
+ 0 - 0
tauri/scripts/bundle.js


+ 0 - 16
tauri/src/app/utils.rs

@@ -43,20 +43,6 @@ pub(super) fn get_url(context: &Context) -> String {
   format!("tauri://{}", context.config.tauri.bundle.identifier)
 }
 
-// spawn an updater process.
-#[cfg(feature = "updater")]
-#[allow(dead_code)]
-pub(super) fn spawn_updater() {
-  std::thread::spawn(|| {
-    tauri_api::command::spawn_relative_command(
-      "updater".to_string(),
-      Vec::new(),
-      std::process::Stdio::inherit(),
-    )
-    .expect("Unable to spawn relative command");
-  });
-}
-
 pub(super) fn initialization_script(
   plugin_initialization_script: &str,
   with_global_tauri: bool,
@@ -230,7 +216,6 @@ pub(super) fn build_webview<A: ApplicationExt + 'static>(
                 .canonicalize()
                 .or_else(|_| Err(crate::Error::AssetNotFound(path.clone())))
                 .and_then(|pathbuf| {
-
                   if pathbuf.is_file() && pathbuf.starts_with(&dist_dir) {
                     match std::fs::read(pathbuf) {
                       Ok(asset) => return Ok(asset),
@@ -242,7 +227,6 @@ pub(super) fn build_webview<A: ApplicationExt + 'static>(
                   }
 
                   Err(crate::Error::AssetNotFound(path))
-
                 })
             })
         }

+ 0 - 21
tauri/src/async_runtime.rs

@@ -1,21 +0,0 @@
-use once_cell::sync::OnceCell;
-use tokio::runtime::Runtime;
-pub use tokio::sync::Mutex;
-
-use std::future::Future;
-
-static RUNTIME: OnceCell<Runtime> = OnceCell::new();
-
-pub fn block_on<F: Future>(task: F) -> F::Output {
-  let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
-  runtime.block_on(task)
-}
-
-pub fn spawn<F>(task: F)
-where
-  F: Future + Send + 'static,
-  F::Output: Send + 'static,
-{
-  let runtime = RUNTIME.get_or_init(|| Runtime::new().unwrap());
-  runtime.spawn(task);
-}

+ 6 - 2
tauri/src/endpoints.rs

@@ -60,8 +60,12 @@ impl Module {
           .and_then(|r| r.json)
           .map_err(|e| e.to_string())
       }),
-      Self::Shell(cmd) => message
-        .respond_async(async move { cmd.run().and_then(|r| r.json).map_err(|e| e.to_string()) }),
+      Self::Shell(cmd) => message.respond_async(async move {
+        cmd
+          .run(webview_manager)
+          .and_then(|r| r.json)
+          .map_err(|e| e.to_string())
+      }),
       Self::Event(cmd) => message.respond_async(async move {
         cmd
           .run(&webview_manager)

+ 101 - 5
tauri/src/endpoints/shell.rs

@@ -1,14 +1,54 @@
 use super::InvokeResponse;
+use crate::{
+  api::{
+    command::{Command, CommandChild, CommandEvent},
+    rpc::format_callback,
+  },
+  app::ApplicationExt,
+};
+
+use once_cell::sync::Lazy;
 use serde::Deserialize;
 
+use std::{
+  collections::HashMap,
+  sync::{Arc, Mutex},
+};
+
+type ChildId = u32;
+type ChildStore = Arc<Mutex<HashMap<ChildId, CommandChild>>>;
+
+fn command_childs() -> &'static ChildStore {
+  static STORE: Lazy<ChildStore> = Lazy::new(Default::default);
+  &STORE
+}
+
+#[derive(Deserialize)]
+#[serde(untagged)]
+pub enum Buffer {
+  Text(String),
+  Raw(Vec<u8>),
+}
+
 /// The API descriptor.
 #[derive(Deserialize)]
 #[serde(tag = "cmd", rename_all = "camelCase")]
 pub enum Cmd {
   /// The execute script API.
+  #[serde(rename_all = "camelCase")]
   Execute {
-    command: String,
+    program: String,
     args: Vec<String>,
+    on_event_fn: String,
+    #[serde(default)]
+    sidecar: bool,
+  },
+  StdinWrite {
+    pid: ChildId,
+    buffer: Buffer,
+  },
+  KillChild {
+    pid: ChildId,
   },
   Open {
     path: String,
@@ -17,15 +57,71 @@ pub enum Cmd {
 }
 
 impl Cmd {
-  pub fn run(self) -> crate::Result<InvokeResponse> {
+  pub fn run<A: ApplicationExt + 'static>(
+    self,
+    webview_manager: crate::WebviewManager<A>,
+  ) -> crate::Result<InvokeResponse> {
     match self {
       Self::Execute {
-        command: _,
-        args: _,
+        program,
+        args,
+        on_event_fn,
+        sidecar,
       } => {
         #[cfg(shell_execute)]
         {
-          //TODO
+          let mut command = if sidecar {
+            Command::new_sidecar(program)
+          } else {
+            Command::new(program)
+          };
+          command = command.args(args);
+          let (mut rx, child) = command.spawn()?;
+
+          let pid = child.pid();
+          command_childs().lock().unwrap().insert(pid, child);
+
+          crate::async_runtime::spawn(async move {
+            while let Some(event) = rx.recv().await {
+              if matches!(event, CommandEvent::Terminated(_)) {
+                command_childs().lock().unwrap().remove(&pid);
+              }
+              let js = format_callback(on_event_fn.clone(), serde_json::to_value(event).unwrap());
+              if let Ok(dispatcher) = webview_manager.current_webview() {
+                let _ = dispatcher.eval(js.as_str());
+              }
+            }
+          });
+
+          Ok(pid.into())
+        }
+        #[cfg(not(shell_execute))]
+        Err(crate::Error::ApiNotAllowlisted(
+          "shell > execute".to_string(),
+        ))
+      }
+      Self::KillChild { pid } => {
+        #[cfg(shell_execute)]
+        {
+          if let Some(child) = command_childs().lock().unwrap().remove(&pid) {
+            child.kill()?;
+          }
+          Ok(().into())
+        }
+        #[cfg(not(shell_execute))]
+        Err(crate::Error::ApiNotAllowlisted(
+          "shell > execute".to_string(),
+        ))
+      }
+      Self::StdinWrite { pid, buffer } => {
+        #[cfg(shell_execute)]
+        {
+          if let Some(child) = command_childs().lock().unwrap().get_mut(&pid) {
+            match buffer {
+              Buffer::Text(t) => child.write(t.as_bytes())?,
+              Buffer::Raw(r) => child.write(&r)?,
+            }
+          }
           Ok(().into())
         }
         #[cfg(not(shell_execute))]

+ 1 - 2
tauri/src/lib.rs

@@ -25,13 +25,12 @@ pub use error::Error;
 /// Tauri result type.
 pub type Result<T> = std::result::Result<T, Error>;
 
-pub(crate) mod async_runtime;
-
 /// A task to run on the main thread.
 pub type SyncTask = Box<dyn FnOnce() + Send>;
 
 pub use app::*;
 pub use tauri_api as api;
+pub(crate) use tauri_api::private::async_runtime;
 pub use tauri_macros::*;
 
 /// The Tauri webview implementations.

部分文件因文件數量過多而無法顯示