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

Co-authored-by: Amr Bashir <48618675+amrbashir@users.noreply.github.com>
This commit is contained in:
Lucas Fernandes Nogueira 2021-04-28 18:25:44 -03:00 committed by GitHub
parent f867e1396d
commit 721e98f175
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 127 additions and 31 deletions

View File

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

File diff suppressed because one or more lines are too long

View File

@ -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);

View File

@ -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();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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>

View File

@ -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 }