Skip to content

Commit

Permalink
feat(core): add env, cwd to the command API, closes #1634
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog committed Apr 28, 2021
1 parent a755d23 commit 730d9b2
Show file tree
Hide file tree
Showing 8 changed files with 126 additions and 31 deletions.
6 changes: 6 additions & 0 deletions .changes/command-options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"api": patch
"tauri": patch
---

Adds `options` argument to the shell command API (`env` and `cwd` configuration).
2 changes: 1 addition & 1 deletion core/tauri/scripts/bundle.js

Large diffs are not rendered by default.

34 changes: 34 additions & 0 deletions core/tauri/src/api/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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()?;
Expand Down Expand Up @@ -118,6 +131,9 @@ impl Command {
Self {
program: program.into(),
args: Default::default(),
env_clear: false,
env: Default::default(),
current_dir: None,
}
}

Expand All @@ -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);
Expand Down
36 changes: 29 additions & 7 deletions core/tauri/src/endpoints/shell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand All @@ -29,6 +27,22 @@ pub enum Buffer {
Raw(Vec<u8>),
}

fn default_env() -> Option<HashMap<String, String>> {
Some(Default::default())
}

#[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")]
Expand All @@ -40,7 +54,7 @@ pub enum Cmd {
args: Vec<String>,
on_event_fn: String,
#[serde(default)]
sidecar: bool,
options: CommandOptions,
},
StdinWrite {
pid: ChildId,
Expand All @@ -63,16 +77,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();
Expand Down
4 changes: 2 additions & 2 deletions examples/api/public/build/bundle.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/api/public/build/bundle.js.map

Large diffs are not rendered by default.

18 changes: 17 additions & 1 deletion examples/api/src/components/Shell.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand Down Expand Up @@ -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>
55 changes: 36 additions & 19 deletions tooling/api/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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)
Expand All @@ -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(
Expand Down Expand Up @@ -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 ?? {}
}

/**
Expand All @@ -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':
Expand All @@ -150,7 +164,9 @@ class Command extends EventEmitter<'close' | 'error'> {
break
}
},
this.args
this.program,
this.args,
this.options
).then((pid) => new Child(pid))
}

Expand Down Expand Up @@ -214,3 +230,4 @@ async function open(path: string, openWith?: string): Promise<void> {
}

export { Command, Child, open }
export type { ChildProcess, SpawnOptions }

0 comments on commit 730d9b2

Please sign in to comment.