Переглянути джерело

feat(core): add env, cwd to the command API, closes #1634 (#1635)

Co-authored-by: Amr Bashir <48618675+amrbashir@users.noreply.github.com>
Lucas Fernandes Nogueira 4 роки тому
батько
коміт
721e98f175

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

@@ -0,0 +1,6 @@
+---
+"api": patch
+"tauri": patch
+---
+
+Adds `options` argument to the shell command API (`env` and `cwd` configuration).

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
core/tauri/scripts/bundle.js


+ 34 - 0
core/tauri/src/api/command.rs

@@ -3,7 +3,9 @@
 // SPDX-License-Identifier: MIT
 
 use std::{
+  collections::HashMap,
   io::{BufRead, BufReader, Write},
+  path::PathBuf,
   process::{Command as StdCommand, Stdio},
   sync::Arc,
 };
@@ -52,6 +54,13 @@ macro_rules! get_std_command {
     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
@@ -62,6 +71,9 @@ macro_rules! get_std_command {
 pub struct Command {
   program: String,
   args: Vec<String>,
+  env_clear: bool,
+  env: HashMap<String, String>,
+  current_dir: Option<PathBuf>,
 }
 
 /// Child spawned.
@@ -76,6 +88,7 @@ impl CommandChild {
     self.stdin_writer.write_all(buf)?;
     Ok(())
   }
+
   /// Send a kill signal to the child.
   pub fn kill(self) -> crate::api::Result<()> {
     self.inner.kill()?;
@@ -118,6 +131,9 @@ impl Command {
     Self {
       program: program.into(),
       args: Default::default(),
+      env_clear: false,
+      env: Default::default(),
+      current_dir: None,
     }
   }
 
@@ -143,6 +159,24 @@ impl Command {
     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);

+ 30 - 7
core/tauri/src/endpoints/shell.rs

@@ -6,10 +6,8 @@ use crate::{endpoints::InvokeResponse, Params, Window};
 use serde::Deserialize;
 
 #[cfg(shell_execute)]
-use std::{
-  collections::HashMap,
-  sync::{Arc, Mutex},
-};
+use std::sync::{Arc, Mutex};
+use std::{collections::HashMap, path::PathBuf};
 
 type ChildId = u32;
 #[cfg(shell_execute)]
@@ -29,6 +27,23 @@ pub enum Buffer {
   Raw(Vec<u8>),
 }
 
+fn default_env() -> Option<HashMap<String, String>> {
+  Some(Default::default())
+}
+
+#[allow(dead_code)]
+#[derive(Default, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CommandOptions {
+  #[serde(default)]
+  sidecar: bool,
+  cwd: Option<PathBuf>,
+  // by default we don't add any env variables to the spawned process
+  // but the env is an `Option` so when it's `None` we clear the env.
+  #[serde(default = "default_env")]
+  env: Option<HashMap<String, String>>,
+}
+
 /// The API descriptor.
 #[derive(Deserialize)]
 #[serde(tag = "cmd", rename_all = "camelCase")]
@@ -40,7 +55,7 @@ pub enum Cmd {
     args: Vec<String>,
     on_event_fn: String,
     #[serde(default)]
-    sidecar: bool,
+    options: CommandOptions,
   },
   StdinWrite {
     pid: ChildId,
@@ -63,16 +78,24 @@ impl Cmd {
         program,
         args,
         on_event_fn,
-        sidecar,
+        options,
       } => {
         #[cfg(shell_execute)]
         {
-          let mut command = if sidecar {
+          let mut command = if options.sidecar {
             crate::api::command::Command::new_sidecar(program)?
           } else {
             crate::api::command::Command::new(program)
           };
           command = command.args(args);
+          if let Some(cwd) = options.cwd {
+            command = command.current_dir(cwd);
+          }
+          if let Some(env) = options.env {
+            command = command.envs(env);
+          } else {
+            command = command.env_clear();
+          }
           let (mut rx, child) = command.spawn()?;
 
           let pid = child.pid();

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
examples/api/public/build/bundle.js


Різницю між файлами не показано, бо вона завелика
+ 0 - 0
examples/api/public/build/bundle.js.map


+ 17 - 1
examples/api/src/components/Shell.svelte

@@ -7,12 +7,24 @@
   export let onMessage;
 
   let script = 'echo "hello world"'
+  let cwd = null
+  let env = 'SOMETHING=value ANOTHER=2'
   let stdin = ''
   let child
 
+  function _getEnv() {
+    return env.split(' ').reduce((env, clause) => {
+      let [key, value] = clause.split('=')
+      return {
+        ...env,
+        [key]: value
+      }
+    }, {})
+  }
+
   function spawn() {
     child = null
-    const command = new Command(cmd, [...args, script])
+    const command = new Command(cmd, [...args, script], { cwd: cwd || null, env: _getEnv() })
 
     command.on('close', data => {
       onMessage(`command finished with code ${data.code} and signal ${data.signal}`)
@@ -49,4 +61,8 @@
       <button class="button" on:click={writeToStdin}>Write</button>
     {/if}
   </div>
+  <div>
+    <input bind:value={cwd} placeholder="Working directory">
+    <input bind:value={env} placeholder="Environment variables" style="width: 300px">
+  </div>
 </div>

+ 36 - 19
tooling/api/src/shell.ts

@@ -5,6 +5,24 @@
 import { invokeTauriCommand } from './helpers/tauri'
 import { transformCallback } from './tauri'
 
+interface SpawnOptions {
+  /** Current working directory. */
+  cwd?: string
+  /** Environment variables. set to `null` to clear the process env. */
+  env?: { [name: string]: string }
+}
+
+interface InternalSpawnOptions extends SpawnOptions {
+  sidecar?: boolean
+}
+
+interface ChildProcess {
+  code: number | null
+  signal: number | null
+  stdout: string
+  stderr: string
+}
+
 /**
  * Spawns a process.
  *
@@ -15,10 +33,10 @@ import { transformCallback } from './tauri'
  * @returns A promise resolving to the process id.
  */
 async function execute(
-  program: string,
-  sidecar: boolean,
   onEvent: (event: CommandEvent) => void,
-  args?: string | string[]
+  program: string,
+  args?: string | string[],
+  options?: InternalSpawnOptions
 ): Promise<number> {
   if (typeof args === 'object') {
     Object.freeze(args)
@@ -29,20 +47,13 @@ async function execute(
     message: {
       cmd: 'execute',
       program,
-      sidecar,
-      onEventFn: transformCallback(onEvent),
-      args: typeof args === 'string' ? [args] : args
+      args: typeof args === 'string' ? [args] : args,
+      options,
+      onEventFn: transformCallback(onEvent)
     }
   })
 }
 
-interface ChildProcess {
-  code: number | null
-  signal: number | null
-  stdout: string
-  stderr: string
-}
-
 class EventEmitter<E> {
   // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
   eventListeners: { [key: string]: Array<(arg: any) => void> } = Object.create(
@@ -107,15 +118,20 @@ class Child {
 class Command extends EventEmitter<'close' | 'error'> {
   program: string
   args: string[]
-  sidecar = false
+  options: InternalSpawnOptions
   stdout = new EventEmitter<'data'>()
   stderr = new EventEmitter<'data'>()
   pid: number | null = null
 
-  constructor(program: string, args: string | string[] = []) {
+  constructor(
+    program: string,
+    args: string | string[] = [],
+    options?: SpawnOptions
+  ) {
     super()
     this.program = program
     this.args = typeof args === 'string' ? [args] : args
+    this.options = options ?? {}
   }
 
   /**
@@ -126,14 +142,12 @@ class Command extends EventEmitter<'close' | 'error'> {
    */
   static sidecar(program: string, args: string | string[] = []): Command {
     const instance = new Command(program, args)
-    instance.sidecar = true
+    instance.options.sidecar = true
     return instance
   }
 
   async spawn(): Promise<Child> {
     return execute(
-      this.program,
-      this.sidecar,
       (event) => {
         switch (event.event) {
           case 'Error':
@@ -150,7 +164,9 @@ class Command extends EventEmitter<'close' | 'error'> {
             break
         }
       },
-      this.args
+      this.program,
+      this.args,
+      this.options
     ).then((pid) => new Child(pid))
   }
 
@@ -214,3 +230,4 @@ async function open(path: string, openWith?: string): Promise<void> {
 }
 
 export { Command, Child, open }
+export type { ChildProcess, SpawnOptions }

Деякі файли не було показано, через те що забагато файлів було змінено