Ver Fonte

refactor(core): move api modules behind allowlist feature flags (#1864)

* refactor(core): move api modules behind allowlist feature flags

* run fmt
Lucas Fernandes Nogueira há 4 anos atrás
pai
commit
aab3e1f18b

+ 5 - 0
.changes/api-feature-flags.md

@@ -0,0 +1,5 @@
+---
+"tauri": patch
+---
+
+Moves `shell`, `dialog::FileDialogBuilder` and `process::Command` APIs behind their allowlist feature flags.

+ 41 - 27
core/tauri/Cargo.toml

@@ -33,12 +33,11 @@ normal = ["attohttpc"] # we ignore attohttpc because we can't remove it based on
 [dependencies]
 serde_json = { version = "1.0", features = [ "raw_value" ] }
 serde = { version = "1.0", features = [ "derive" ] }
-base64 = "0.13.0"
 tokio = { version = "1.6", features = [ "rt", "rt-multi-thread", "sync" ] }
 futures = "0.3"
-uuid = { version = "0.8.2", features = [ "v4" ] }
-thiserror = "1.0.24"
-once_cell = "1.7.2"
+uuid = { version = "0.8", features = [ "v4" ] }
+thiserror = "1.0"
+once_cell = "1.7"
 tauri-runtime = { version = "0.1.1", path = "../tauri-runtime" }
 tauri-macros = { version = "1.0.0-beta.1", path = "../tauri-macros" }
 tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils" }
@@ -47,30 +46,45 @@ rand = "0.8"
 tempfile = "3"
 semver = "0.11"
 serde_repr = "0.1"
-dirs-next = "2.0.0"
-zip = "0.5.12"
-ignore = "^0.4.17"
-either = "1.6.1"
+zip = "0.5"
+ignore = "0.4"
+either = "1.6"
 tar = "0.4"
 flate2 = "1.0"
-rfd = "0.3.0"
 tinyfiledialogs = "3.3"
 http = "0.2"
-clap = { version = "=3.0.0-beta.2", optional = true }
-notify-rust = { version = "4.5.2", optional = true }
-tauri-hotkey = { version = "0.1.2", optional = true }
-open = "1.7.0"
-shared_child = "0.3"
-os_pipe = "0.9"
-minisign-verify = "0.1.8"
 state = "0.4"
 bincode = "1.3"
+dirs-next = "2.0"
+
+# FS
+base64 = { version = "0.13", optional = true } # also used on the updater
+
+# CLI
+clap = { version = "=3.0.0-beta.2", optional = true }
+
+# Notifications
+notify-rust = { version = "4.5", optional = true }
+
+# Global shortcut
+tauri-hotkey = { version = "0.1.2", optional = true }
 
 # HTTP
 reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true }
 bytes = { version = "1", features = [ "serde" ], optional = true }
 attohttpc = { version = "0.17", features = [ "json", "form" ] }
 
+# Shell
+open = { version = "1.7", optional = true }
+shared_child = { version = "0.3", optional = true }
+os_pipe = { version = "0.9", optional = true }
+
+# Dialogs
+rfd = { version = "0.3", optional = true }
+
+# Updater
+minisign-verify = { version = "0.1", optional = true }
+
 [build-dependencies]
 cfg_aliases = "0.1.1"
 
@@ -90,31 +104,31 @@ dox = [ "tauri-runtime-wry/dox" ]
 wry = [ "tauri-runtime-wry" ]
 cli = [ "clap" ]
 custom-protocol = [ "tauri-macros/custom-protocol" ]
-api-all = [ "notification-all", "global-shortcut-all", "updater" ]
-updater = [ ]
+api-all = [ "notification-all", "global-shortcut-all", "shell-all", "dialog-all", "updater" ]
+updater = [ "minisign-verify", "base64" ]
 menu = [ "tauri-runtime/menu", "tauri-runtime-wry/menu" ]
-system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ]
+system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray"]
 reqwest-client = [ "reqwest", "bytes" ]
 fs-all = [ ]
 fs-read-text-file = [ ]
 fs-read-binary-file = [ ]
 fs-write-file = [ ]
-fs-write-binary-file = [ ]
+fs-write-binary-file = [ "base64" ]
 fs-read-dir = [ ]
 fs-copy-file = [ ]
 fs-create-dir = [ ]
 fs-remove-dir = [ ]
 fs-remove-file = [ ]
 fs-rename-file = [ ]
-fs-path-api = [ ]
+fs-path = [ ]
 window-all = [ ]
 window-create = [ ]
-shell-all = [ ]
-shell-execute = [ ]
-shell-open = [ ]
-dialog-all = [ ]
-dialog-open = [ ]
-dialog-save = [ ]
+shell-all = [ "shell-open", "shell-execute" ]
+shell-execute = [ "shared_child", "os_pipe" ]
+shell-open = [ "open" ]
+dialog-all = [ "dialog-open", "dialog-save" ]
+dialog-open = [ "rfd" ]
+dialog-save = [ "rfd" ]
 http-all = [ ]
 http-request = [ ]
 notification-all = [ "notify-rust" ]

+ 4 - 2
core/tauri/src/api/dialog.rs

@@ -2,16 +2,18 @@
 // SPDX-License-Identifier: Apache-2.0
 // SPDX-License-Identifier: MIT
 
+#[cfg(any(dialog_open, dialog_save))]
 use std::path::{Path, PathBuf};
 
-use rfd::FileDialog;
 use tinyfiledialogs::{message_box_ok, message_box_yes_no, MessageBoxIcon, YesNo};
 
 /// The file dialog builder.
 /// Constructs file picker dialogs that can select single/multiple files or directories.
+#[cfg(any(dialog_open, dialog_save))]
 #[derive(Default)]
-pub struct FileDialogBuilder(FileDialog);
+pub struct FileDialogBuilder(rfd::FileDialog);
 
+#[cfg(any(dialog_open, dialog_save))]
 impl FileDialogBuilder {
   /// Gets the default file dialog builder.
   pub fn new() -> Self {

+ 1 - 0
core/tauri/src/api/mod.rs

@@ -21,6 +21,7 @@ pub mod process;
 /// The RPC module includes utilities to send messages to the JS layer of the webview.
 pub mod rpc;
 /// The shell api.
+#[cfg(shell_open)]
 pub mod shell;
 /// The semver API.
 pub mod version;

+ 5 - 373
core/tauri/src/api/process.rs

@@ -3,71 +3,15 @@
 // SPDX-License-Identifier: MIT
 
 use std::{
-  collections::HashMap,
   env,
-  io::{BufRead, BufReader, Write},
   path::PathBuf,
-  process::{exit, Command as StdCommand, Stdio},
-  sync::Arc,
+  process::{exit, Command as StdCommand},
 };
 
-#[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::async_runtime::{channel, spawn, Receiver, RwLock};
-use os_pipe::{pipe, PipeWriter};
-use serde::Serialize;
-use shared_child::SharedChild;
-use tauri_utils::platform;
-
-/// Payload for the `Terminated` command event.
-#[derive(Debug, Clone, 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>,
-}
-
-/// A event sent to the command callback.
-#[derive(Debug, Clone, Serialize)]
-#[serde(tag = "event", content = "payload")]
-#[non_exhaustive]
-pub enum CommandEvent {
-  /// Stderr line.
-  Stderr(String),
-  /// Stdout line.
-  Stdout(String),
-  /// An error happened.
-  Error(String),
-  /// Command process terminated.
-  Terminated(TerminatedPayload),
-}
-
-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());
-    if $self.env_clear {
-      command.env_clear();
-    }
-    command.envs($self.env);
-    if let Some(current_dir) = $self.current_dir {
-      command.current_dir(current_dir);
-    }
-    #[cfg(windows)]
-    command.creation_flags(CREATE_NO_WINDOW);
-    command
-  }};
-}
+#[cfg(shell_execute)]
+mod command;
+#[cfg(shell_execute)]
+pub use command::*;
 
 /// Get the current binary
 pub fn current_binary() -> Option<PathBuf> {
@@ -100,315 +44,3 @@ pub fn restart() {
 
   exit(0);
 }
-
-/// API to spawn commands.
-pub struct Command {
-  program: String,
-  args: Vec<String>,
-  env_clear: bool,
-  env: HashMap<String, String>,
-  current_dir: Option<PathBuf>,
-}
-
-/// Child spawned.
-pub struct CommandChild {
-  inner: Arc<SharedChild>,
-  stdin_writer: PipeWriter,
-}
-
-impl CommandChild {
-  /// Write to process stdin.
-  pub fn write(&mut self, buf: &[u8]) -> crate::api::Result<()> {
-    self.stdin_writer.write_all(buf)?;
-    Ok(())
-  }
-
-  /// Send a kill signal to the child.
-  pub fn kill(self) -> crate::api::Result<()> {
-    self.inner.kill()?;
-    Ok(())
-  }
-
-  /// Returns the process pid.
-  pub fn pid(&self) -> u32 {
-    self.inner.id()
-  }
-}
-
-/// Describes the result of a process after it has terminated.
-pub struct ExitStatus {
-  code: Option<i32>,
-}
-
-impl ExitStatus {
-  /// Returns the exit code of the process, if any.
-  pub fn code(&self) -> Option<i32> {
-    self.code
-  }
-
-  /// Was termination successful? Signal termination is not considered a success, and success is defined as a zero exit status.
-  pub fn success(&self) -> bool {
-    self.code == Some(0)
-  }
-}
-
-/// The output of a finished process.
-pub struct Output {
-  /// The status (exit code) of the process.
-  pub status: ExitStatus,
-  /// The data that the process wrote to stdout.
-  pub stdout: String,
-  /// The data that the process wrote to stderr.
-  pub stderr: String,
-}
-
-#[cfg(not(windows))]
-fn relative_command_path(command: String) -> crate::Result<String> {
-  match std::env::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!(
-      "{}/{}",
-      exe_dir.to_string_lossy().to_string(),
-      command
-    )),
-    None => Err(super::Error::Command("Could not evaluate executable dir".to_string()).into()),
-  }
-}
-
-#[cfg(windows)]
-fn relative_command_path(command: String) -> crate::Result<String> {
-  match std::env::current_exe()?.parent() {
-    Some(exe_dir) => Ok(format!(
-      "{}/{}.exe",
-      exe_dir.to_string_lossy().to_string(),
-      command
-    )),
-    None => Err(super::Error::Command("Could not evaluate executable dir".to_string()).into()),
-  }
-}
-
-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(),
-      env_clear: false,
-      env: Default::default(),
-      current_dir: None,
-    }
-  }
-
-  /// Creates a new Command for launching the given sidecar program.
-  pub fn new_sidecar<S: Into<String>>(program: S) -> crate::Result<Self> {
-    let program = format!(
-      "{}-{}",
-      program.into(),
-      platform::target_triple().expect("unsupported platform")
-    );
-    Ok(Self::new(relative_command_path(program)?))
-  }
-
-  /// 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
-  }
-
-  /// Clears the entire environment map for the child process.
-  pub fn env_clear(mut self) -> Self {
-    self.env_clear = true;
-    self
-  }
-
-  /// Adds or updates multiple environment variable mappings.
-  pub fn envs(mut self, env: HashMap<String, String>) -> Self {
-    self.env = env;
-    self
-  }
-
-  /// Sets the working directory for the child process.
-  pub fn current_dir(mut self, current_dir: PathBuf) -> Self {
-    self.current_dir.replace(current_dir);
-    self
-  }
-
-  /// Spawns the command.
-  pub fn spawn(self) -> crate::api::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 guard = Arc::new(RwLock::new(()));
-
-    let (tx, rx) = channel(1);
-
-    let tx_ = tx.clone();
-    let guard_ = guard.clone();
-    spawn(async move {
-      let _lock = guard_.read().await;
-      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,
-        };
-      }
-    });
-
-    let tx_ = tx.clone();
-    let guard_ = guard.clone();
-    spawn(async move {
-      let _lock = guard_.read().await;
-      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,
-        };
-      }
-    });
-
-    spawn(async move {
-      let _ = match child_.wait() {
-        Ok(status) => {
-          guard.write().await;
-          tx.send(CommandEvent::Terminated(TerminatedPayload {
-            code: status.code(),
-            #[cfg(windows)]
-            signal: None,
-            #[cfg(unix)]
-            signal: status.signal(),
-          }))
-          .await
-        }
-        Err(e) => {
-          guard.write().await;
-          tx.send(CommandEvent::Error(e.to_string())).await
-        }
-      };
-    });
-
-    Ok((
-      rx,
-      CommandChild {
-        inner: child,
-        stdin_writer,
-      },
-    ))
-  }
-
-  /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
-  /// Stdin, stdout and stderr are ignored.
-  pub fn status(self) -> crate::api::Result<ExitStatus> {
-    let (mut rx, _child) = self.spawn()?;
-    let code = crate::async_runtime::block_on(async move {
-      let mut code = None;
-      while let Some(event) = rx.recv().await {
-        if let CommandEvent::Terminated(payload) = event {
-          code = payload.code;
-        }
-      }
-      code
-    });
-    Ok(ExitStatus { code })
-  }
-
-  /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
-  /// Stdin is ignored.
-  pub fn output(self) -> crate::api::Result<Output> {
-    let (mut rx, _child) = self.spawn()?;
-
-    let output = crate::async_runtime::block_on(async move {
-      let mut code = None;
-      let mut stdout = String::new();
-      let mut stderr = String::new();
-      while let Some(event) = rx.recv().await {
-        match event {
-          CommandEvent::Terminated(payload) => {
-            code = payload.code;
-          }
-          CommandEvent::Stdout(line) => {
-            stdout.push_str(line.as_str());
-            stdout.push('\n');
-          }
-          CommandEvent::Stderr(line) => {
-            stderr.push_str(line.as_str());
-            stderr.push('\n');
-          }
-          CommandEvent::Error(_) => {}
-        }
-      }
-      Output {
-        status: ExitStatus { code },
-        stdout,
-        stderr,
-      }
-    });
-
-    Ok(output)
-  }
-}
-
-// tests for the commands functions.
-#[cfg(test)]
-mod test {
-  #[cfg(not(windows))]
-  use super::*;
-
-  #[cfg(not(windows))]
-  #[test]
-  fn test_cmd_output() {
-    // create a command to run cat.
-    let cmd = Command::new("cat").args(&["test/api/test.txt"]);
-    let (mut rx, _) = cmd.spawn().unwrap();
-
-    crate::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
-  fn test_cmd_fail() {
-    let cmd = Command::new("cat").args(&["test/api/"]);
-    let (mut rx, _) = cmd.spawn().unwrap();
-
-    crate::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/api/: Is a directory".to_string());
-          }
-          _ => {}
-        }
-      }
-    });
-  }
-}

+ 381 - 0
core/tauri/src/api/process/command.rs

@@ -0,0 +1,381 @@
+// Copyright 2019-2021 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{
+  collections::HashMap,
+  io::{BufRead, BufReader, Write},
+  path::PathBuf,
+  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::async_runtime::{channel, spawn, Receiver, RwLock};
+use os_pipe::{pipe, PipeWriter};
+use serde::Serialize;
+use shared_child::SharedChild;
+use tauri_utils::platform;
+
+/// Payload for the `Terminated` command event.
+#[derive(Debug, Clone, 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>,
+}
+
+/// A event sent to the command callback.
+#[derive(Debug, Clone, Serialize)]
+#[serde(tag = "event", content = "payload")]
+#[non_exhaustive]
+pub enum CommandEvent {
+  /// Stderr line.
+  Stderr(String),
+  /// Stdout line.
+  Stdout(String),
+  /// An error happened.
+  Error(String),
+  /// Command process terminated.
+  Terminated(TerminatedPayload),
+}
+
+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());
+    if $self.env_clear {
+      command.env_clear();
+    }
+    command.envs($self.env);
+    if let Some(current_dir) = $self.current_dir {
+      command.current_dir(current_dir);
+    }
+    #[cfg(windows)]
+    command.creation_flags(CREATE_NO_WINDOW);
+    command
+  }};
+}
+
+/// API to spawn commands.
+pub struct Command {
+  program: String,
+  args: Vec<String>,
+  env_clear: bool,
+  env: HashMap<String, String>,
+  current_dir: Option<PathBuf>,
+}
+
+/// Child spawned.
+pub struct CommandChild {
+  inner: Arc<SharedChild>,
+  stdin_writer: PipeWriter,
+}
+
+impl CommandChild {
+  /// Write to process stdin.
+  pub fn write(&mut self, buf: &[u8]) -> crate::api::Result<()> {
+    self.stdin_writer.write_all(buf)?;
+    Ok(())
+  }
+
+  /// Send a kill signal to the child.
+  pub fn kill(self) -> crate::api::Result<()> {
+    self.inner.kill()?;
+    Ok(())
+  }
+
+  /// Returns the process pid.
+  pub fn pid(&self) -> u32 {
+    self.inner.id()
+  }
+}
+
+/// Describes the result of a process after it has terminated.
+pub struct ExitStatus {
+  code: Option<i32>,
+}
+
+impl ExitStatus {
+  /// Returns the exit code of the process, if any.
+  pub fn code(&self) -> Option<i32> {
+    self.code
+  }
+
+  /// Was termination successful? Signal termination is not considered a success, and success is defined as a zero exit status.
+  pub fn success(&self) -> bool {
+    self.code == Some(0)
+  }
+}
+
+/// The output of a finished process.
+pub struct Output {
+  /// The status (exit code) of the process.
+  pub status: ExitStatus,
+  /// The data that the process wrote to stdout.
+  pub stdout: String,
+  /// The data that the process wrote to stderr.
+  pub stderr: String,
+}
+
+#[cfg(not(windows))]
+fn relative_command_path(command: String) -> crate::Result<String> {
+  match std::env::current_exe()?.parent() {
+    Some(exe_dir) => Ok(format!(
+      "{}/{}",
+      exe_dir.to_string_lossy().to_string(),
+      command
+    )),
+    None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()),
+  }
+}
+
+#[cfg(windows)]
+fn relative_command_path(command: String) -> crate::Result<String> {
+  match std::env::current_exe()?.parent() {
+    Some(exe_dir) => Ok(format!(
+      "{}/{}.exe",
+      exe_dir.to_string_lossy().to_string(),
+      command
+    )),
+    None => Err(crate::api::Error::Command("Could not evaluate executable dir".to_string()).into()),
+  }
+}
+
+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(),
+      env_clear: false,
+      env: Default::default(),
+      current_dir: None,
+    }
+  }
+
+  /// Creates a new Command for launching the given sidecar program.
+  pub fn new_sidecar<S: Into<String>>(program: S) -> crate::Result<Self> {
+    let program = format!(
+      "{}-{}",
+      program.into(),
+      platform::target_triple().expect("unsupported platform")
+    );
+    Ok(Self::new(relative_command_path(program)?))
+  }
+
+  /// 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
+  }
+
+  /// Clears the entire environment map for the child process.
+  pub fn env_clear(mut self) -> Self {
+    self.env_clear = true;
+    self
+  }
+
+  /// Adds or updates multiple environment variable mappings.
+  pub fn envs(mut self, env: HashMap<String, String>) -> Self {
+    self.env = env;
+    self
+  }
+
+  /// Sets the working directory for the child process.
+  pub fn current_dir(mut self, current_dir: PathBuf) -> Self {
+    self.current_dir.replace(current_dir);
+    self
+  }
+
+  /// Spawns the command.
+  pub fn spawn(self) -> crate::api::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 guard = Arc::new(RwLock::new(()));
+
+    let (tx, rx) = channel(1);
+
+    let tx_ = tx.clone();
+    let guard_ = guard.clone();
+    spawn(async move {
+      let _lock = guard_.read().await;
+      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,
+        };
+      }
+    });
+
+    let tx_ = tx.clone();
+    let guard_ = guard.clone();
+    spawn(async move {
+      let _lock = guard_.read().await;
+      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,
+        };
+      }
+    });
+
+    spawn(async move {
+      let _ = match child_.wait() {
+        Ok(status) => {
+          guard.write().await;
+          tx.send(CommandEvent::Terminated(TerminatedPayload {
+            code: status.code(),
+            #[cfg(windows)]
+            signal: None,
+            #[cfg(unix)]
+            signal: status.signal(),
+          }))
+          .await
+        }
+        Err(e) => {
+          guard.write().await;
+          tx.send(CommandEvent::Error(e.to_string())).await
+        }
+      };
+    });
+
+    Ok((
+      rx,
+      CommandChild {
+        inner: child,
+        stdin_writer,
+      },
+    ))
+  }
+
+  /// Executes a command as a child process, waiting for it to finish and collecting its exit status.
+  /// Stdin, stdout and stderr are ignored.
+  pub fn status(self) -> crate::api::Result<ExitStatus> {
+    let (mut rx, _child) = self.spawn()?;
+    let code = crate::async_runtime::block_on(async move {
+      let mut code = None;
+      while let Some(event) = rx.recv().await {
+        if let CommandEvent::Terminated(payload) = event {
+          code = payload.code;
+        }
+      }
+      code
+    });
+    Ok(ExitStatus { code })
+  }
+
+  /// Executes the command as a child process, waiting for it to finish and collecting all of its output.
+  /// Stdin is ignored.
+  pub fn output(self) -> crate::api::Result<Output> {
+    let (mut rx, _child) = self.spawn()?;
+
+    let output = crate::async_runtime::block_on(async move {
+      let mut code = None;
+      let mut stdout = String::new();
+      let mut stderr = String::new();
+      while let Some(event) = rx.recv().await {
+        match event {
+          CommandEvent::Terminated(payload) => {
+            code = payload.code;
+          }
+          CommandEvent::Stdout(line) => {
+            stdout.push_str(line.as_str());
+            stdout.push('\n');
+          }
+          CommandEvent::Stderr(line) => {
+            stderr.push_str(line.as_str());
+            stderr.push('\n');
+          }
+          CommandEvent::Error(_) => {}
+        }
+      }
+      Output {
+        status: ExitStatus { code },
+        stdout,
+        stderr,
+      }
+    });
+
+    Ok(output)
+  }
+}
+
+// tests for the commands functions.
+#[cfg(test)]
+mod test {
+  #[cfg(not(windows))]
+  use super::*;
+
+  #[cfg(not(windows))]
+  #[test]
+  fn test_cmd_output() {
+    // create a command to run cat.
+    let cmd = Command::new("cat").args(&["test/api/test.txt"]);
+    let (mut rx, _) = cmd.spawn().unwrap();
+
+    crate::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
+  fn test_cmd_fail() {
+    let cmd = Command::new("cat").args(&["test/api/"]);
+    let (mut rx, _) = cmd.spawn().unwrap();
+
+    crate::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/api/: Is a directory".to_string());
+          }
+          _ => {}
+        }
+      }
+    });
+  }
+}

+ 1 - 0
core/tauri/src/error.rs

@@ -39,6 +39,7 @@ pub enum Error {
   #[error("{0}")]
   Io(#[from] std::io::Error),
   /// Failed to decode base64.
+  #[cfg(any(fs_write_binary_file, feature = "updater"))]
   #[error("Failed to decode base64 string: {0}")]
   Base64Decode(#[from] base64::DecodeError),
   /// Failed to load window icon.

+ 1 - 1
examples/helloworld/src-tauri/Cargo.toml

@@ -10,7 +10,7 @@ tauri-build = { path = "../../../core/tauri-build", features = [ "codegen" ] }
 [dependencies]
 serde_json = "1.0"
 serde = { version = "1.0", features = [ "derive" ] }
-tauri = { path = "../../../core/tauri", features = ["api-all"] }
+tauri = { path = "../../../core/tauri", features = [] }
 
 [features]
 default = [ "custom-protocol" ]

+ 1 - 1
examples/helloworld/src-tauri/tauri.conf.json

@@ -35,7 +35,7 @@
       }
     },
     "allowlist": {
-      "all": true
+      "all": false
     },
     "windows": [
       {