Skip to content

Add working-directory and environment controls for child processes - #16

Open
mjrusso wants to merge 5 commits into
nyo16:masterfrom
mjrusso:pr/child-launch-context
Open

mjrusso wants to merge 5 commits into
nyo16:masterfrom
mjrusso:pr/child-launch-context

Conversation

@mjrusso

@mjrusso mjrusso commented Sep 7, 2026

Copy link
Copy Markdown

Summary

This PR adds two child-process options:

  • cwd: selects the working directory
  • env: accepts an overlay or a complete replacement environment

This PR also fixes shepherd startup failure handling. If the operating system rejects the shepherd’s arguments or environment, NetRunner now reports the failure promptly instead of waiting for the Unix-socket timeout.

Note that the first commit documents existing MSG_ERROR behaviour; it does not change the wire format.

Spawn sequence

BEAM
  validate cwd and env
  create the Unix-socket listener
  start the shepherd with the requested environment changes
  wait for either:
    ├─ an authenticated connection
    └─ shepherd exit → {:shepherd_spawn_failed, reason}

shepherd, after authentication
  chdir(cwd), when present
  filter inherited variables, in replacement mode
  fork
  exec the child

Waiting for socket readiness and shepherd exit in the same receive loop avoids the previous 10-second timeout when the shepherd could not start. An immediate child exit after a successful spawn remains a normal process result.

Temporary socket paths are removed after successful and failed spawns.

Working directory

cwd: is available through run/2, stream/2, stream!/2, and NetRunner.Process.start/3. NetRunner.Daemon accepts it through process_opts.

The value must be a non-empty binary without NUL. It does not need to contain valid UTF-8, which permits valid filesystem paths with arbitrary bytes.

The shepherd calls chdir after authentication and before fork. This means:

  • Relative child paths and executable names resolve from cwd.
  • A relative cwd: resolves from the BEAM working directory.
  • A failed chdir returns {:error, {:shepherd_error, reason}} before a child starts.
  • PWD is not updated automatically.

Environment

env: accepts a map or a list of {name, value} pairs.

Form Inherited variables Command lookup
env: environment Retained unless overridden or removed Uses the resulting PATH
env: {:replace, environment} All unselected variables are removed Uses only the selected PATH

A binary value sets a variable. nil and "" remove it. If a list contains the same name more than once, the last value wins.

Names must be non-empty UTF-8 binaries without = or NUL. Values must be UTF-8 binaries without NUL, or nil. Invalid input raises ArgumentError before NetRunner starts the shepherd.

In replacement mode, omitting PATH disables the default execvp search path. A command containing / is still attempted directly.

Environment transport

Values pass through the Port.open/2 environment option and do not appear in the shepherd command line. They remain visible through normal process-environment inspection.

Replacement mode passes the names of retained variables, but not their values, in the shepherd argument allowlist. Those names count toward both the operating system’s argument and environment size limits.

If the operating system rejects the shepherd because its arguments or environment are too large, NetRunner returns:

{:error, {:shepherd_spawn_failed, reason}}

Add `cwd:` to `run/2`, `stream/2`, `stream!/2`, and `Process.start/3`. `Daemon`
accepts it in `process_opts`.

The shepherd calls `chdir` after authentication and before `fork`. A failure
returns `{:error, {:shepherd_error, reason}}` before the command starts. The
selected directory controls relative paths and executables.

Accept non-empty binaries without NUL. Relative values use the BEAM working
directory. Paths travel as argv bytes, so non-UTF-8 paths remain valid. The
option does not change `PWD`.
`MSG_ERROR` carries at most 255 bytes. A failure before FD passing will send
this frame without file descriptors.
If the OS rejects the arguments or environment for the shepherd, the port exits
before it connects to the Unix socket. NetRunner then waited 10 seconds and
returned `:shepherd_connect_timeout`. A failed token write could also exit the
Process server.

Monitor and unlink the port before sending the token. Wait for socket readiness
and port exit in the same receive block. If the port exits, check the listener
because no sender orders messages from the port and socket.

Return `{:error, {:shepherd_spawn_failed, reason}}` if the listener has no
connection. Preserve immediate child exits.
Accept `:env` as a map or a list of `{name, value}` pairs.

`Port.open` expects environment entries as character lists. The previous
byte-list conversion double-encoded non-ASCII text. Validate UTF-8 before
converting names and values with `String.to_charlist/1`.

An empty string and `nil` both remove a variable because a port cannot set an
empty value. The modified `PATH` controls executable lookup.
Add `env: {:replace, environment}` to define the complete child environment.
Untagged `env:` remains an overlay.

Normalize environment input during option validation. Pass values through the
port environment and selected names through a shepherd allowlist. After
authentication, the shepherd removes all unselected variables before `fork`.

If the replacement omits `PATH`, prevent `execvp` from using its default search
path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant