tauri-apps / tauri

Build smaller, faster, and more secure desktop applications with a web frontend.
https://tauri.app
Apache License 2.0
82.01k stars 2.46k forks source link

[feat] Make stdin closable via JS `shell` API #5316

Open salmenf opened 1 year ago

salmenf commented 1 year ago

Describe the problem

@FabianLars asked me to open a feature request, so here we are. As elaborated in #4440, there is an issue when using Tauri's JS API method shell.Child.write: stdin is not being closed. This can make command line interfaces that depend on stdin unusable, for example esbuild in this code snippet:

const command = Command.sidecar("../binaries/esbuild", "--loader=ts")
command.stdout.on("data", data => console.log(data))
command.stderr.on("data", data => console.log(data))
command.on("error", data => console.log(data))
command.on("close", data => console.log(data))
const child = await command.spawn()
console.log(child.pid)
await child.write("echo 'let x: number = 1'")
// => Only the pid is logged, nothing else

The expected output here would the transformed code or an error, but since stdin is not closed, no stdout/stderr is ever produced.

A workaround (credit to @lanc33llis, who faced the same issue) is this:

#[tauri::command]
async fn function(text: &str) -> Result<String, String> {
  let (mut rx, mut child) = Command::new_sidecar("my-sidecar")
    .expect("failed to create `my-sidecar` binary command")
    .spawn()
    .expect("Failed to spawn sidecar");

  child.write(text.as_bytes()).expect("Failed to write to sidecar");
  drop(child);

  let mut output = String::new();
  while let Some(event) = rx.recv().await {
    if let CommandEvent::Stdout(line) = event {
      output.push_str(&line);
    }
  }

  Ok(output)
}

In my personal opinion, that workaround shouldn't be neccessary, hence this feature request.

Describe the solution you'd like

Possibly the easiest fix would be adding a closeAfter flag to the shell.Child.write method. This is backwards compatible and follows a similar pattern to other parts of the API.

New signature:

type WriteOptions = {closeAfter?: boolean}
write(data: string | Uint8Array, options?: WriteOptions): Promise<void>

Semantics: If set to true, it would close stdin after writing. This would avoid adding extra methods and still allow usage such as this:

child.write("my first stdin message") // stdin is open
child.write("my second stdin message") // still open...
child.write("my final stdin message", {closeAfter: true}) // stdin is closed

Alternatives considered

A separate method for closing stdin on shell.Child would be possible, but this would increase the API surface.

Additional context

I don't know if this problem would also appear in other parts of the JS API and would need more adjustments.

fzzzy commented 5 months ago

I just ran into needing this.

fzzzy commented 5 months ago

I was able to work around this by exposing zsh -c as the scope and passing my actual command line as a single string with "</dev/null" at the end.