Skip to content

feature: Add subprocess execution APIs (run/call/spawn) to IO#454

Merged
xerial merged 11 commits into
mainfrom
feature/add-subprocess-execution
Mar 30, 2026
Merged

feature: Add subprocess execution APIs (run/call/spawn) to IO#454
xerial merged 11 commits into
mainfrom
feature/add-subprocess-execution

Conversation

@xerial
Copy link
Copy Markdown
Member

@xerial xerial commented Mar 30, 2026

Summary

  • Add IO.run, IO.call, and IO.spawn for subprocess execution in wvlet.uni.io
  • CommandResult captures exit code, stdout, and stderr
  • ProcessConfig supports working directory, env vars, inheritIO, and stderr redirection
  • Process trait for spawned process handle (stdin/stdout/stderr streams, waitFor, destroy)
  • JVM/Native: java.lang.ProcessBuilder; JS: throws UnsupportedOperationException
  • 11 JVM tests covering all APIs

Test plan

  • ./sbt "coreJVM/compile" — compiles
  • ./sbt "coreJS/compile" — compiles (stub)
  • ./sbt "coreNative/compile" — compiles
  • ./sbt "uniJVM/testOnly *IOProcessTest" — 11/11 tests pass
  • ./sbt scalafmtAll — formatted

🤖 Generated with Claude Code

Add IO.run, IO.call, and IO.spawn for cross-platform subprocess
execution in wvlet.uni.io, filling the highest-impact gap vs os-lib.

- IO.run: execute command, capture stdout/stderr, return CommandResult
- IO.call: like run but throws NonZeroExitCodeException on failure
- IO.spawn: start process without waiting, return Process handle
- ProcessConfig: working directory, env vars, inheritIO, redirect stderr
- JVM/Native: java.lang.ProcessBuilder; JS: throws UnsupportedOperationException

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the feature New feature label Mar 30, 2026
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a unified cross-platform API for subprocess execution across JVM, Scala Native, and Scala.js. The implementation includes a ProcessApi trait with methods for running, calling, and spawning processes, along with configuration options for environment variables and working directories. Review feedback suggests deduplicating the nearly identical JVM and Native implementations into a shared source directory, simplifying the destroyForcibly implementation, and enhancing the Process trait with timeout-based waitFor and exitValue methods to prevent potential hangs.

/**
* Scala Native implementation of process execution using java.lang.ProcessBuilder.
*/
trait IOCompat extends ProcessApi:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This file is nearly identical to uni-core/.jvm/src/main/scala/wvlet/uni/io/IOCompat.scala. To avoid code duplication and improve maintainability, consider moving the shared code to a common source directory for both JVM and Native platforms. In sbt, you can create a uni-core/.jvm-native/src/main/scala directory for this purpose and place the shared implementation there.

Comment on lines +28 to +30
override def destroyForcibly(): Unit =
process.destroyForcibly();
()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This implementation of destroyForcibly can be simplified. The semicolon and () are redundant because Scala automatically discards the return value of a method when the expected return type is Unit.

  override def destroyForcibly(): Unit = process.destroyForcibly()

Comment on lines +52 to +59
trait Process:
def stdin: OutputStream
def stdout: InputStream
def stderr: InputStream
def isAlive: Boolean
def waitFor(): Int
def destroy(): Unit
def destroyForcibly(): Unit
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Process trait could be made more robust by adding a timed waitFor and an exitValue method, similar to java.lang.Process. This is crucial for preventing applications or tests from hanging indefinitely when waiting for a process to complete.

Here's a suggested enhancement:

import java.util.concurrent.TimeUnit

// ... in Process trait
def waitFor(timeout: Long, unit: TimeUnit): Boolean
def exitValue(): Int

This would enable safer process handling, like so:

if (proc.waitFor(5, TimeUnit.SECONDS)) {
  val exitCode = proc.exitValue()
  // ... handle completed process
} else {
  // The process timed out
  proc.destroyForcibly()
}

xerial and others added 6 commits March 30, 2026 09:21
- Close process stdin in run/call so commands waiting for EOF don't hang
- Apply redirectErrorToOutput config in spawn (was only applied in run)
- Fix applied to both JVM and Native implementations

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… redundant @volatile

- Move redirectErrorToOutput config into buildProcess alongside other ProcessBuilder config
- Remove @volatile on local vars since Thread.join() provides happens-before guarantee

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…yForcibly

Address Gemini review feedback:
- Add waitFor(timeout, unit) and exitValue() to Process trait
- Simplify destroyForcibly implementation

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…onflicts

Cross-platform projects can have duplicate class names across JVM/JS/Native
source directories, which only surfaces during packageSrc. Run packageSrc
for all platforms in CI to catch this early.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…etup

packageSrc needs Node.js (for JS) and libcurl (for Native), so it
should be its own job rather than appended to the Scala 3 test job.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Document IO.run, IO.call, IO.spawn, ProcessConfig, and platform support.
Add IO page to sidebar navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added the doc Improvements or additions to documentation label Mar 30, 2026
xerial and others added 4 commits March 30, 2026 10:20
IO.run("ls -la /tmp") now automatically tokenizes into Seq("ls", "-la", "/tmp").
Handles single/double quotes and escaped characters.
Multi-arg calls like IO.run("echo", "hello") still work as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…osed quotes

- Empty quoted strings (e.g., "") are now preserved as empty arguments
- Unclosed quotes now throw IllegalArgumentException
- Document space-in-path limitation in resolveCommand and run Scaladoc

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Include IO, FileSystem, and Utilities entries from both branches.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enhance ProcessApi.tokenize with \n, \t, \r escape sequence support
- Replace CommandLineTokenizer implementation with delegation to
  ProcessApi.tokenize, eliminating ~70 lines of duplicate logic

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@xerial xerial merged commit 646fe09 into main Mar 30, 2026
14 checks passed
@xerial xerial deleted the feature/add-subprocess-execution branch March 30, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Improvements or additions to documentation feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant