Skip to content

PtySpawnOptions has no cwd: every per-directory terminal has to smuggle the cd through the shell's own command line #223

Description

@sonhyrd

PtySpawnOptions has no cwd, so a pty child always starts in the app process's working directory. Any terminal that should open somewhere specific has to smuggle the cd through the shell's own command line.

The gap, top to bottom

PtySpawnOptions (src/runtime/effects.zig:2890) takes key, argv, cols, rows, term, on_event — and nothing else. startRealPty (:11025) forwards exactly argv, env, term, cols, rows into pty.spawn, whose own SpawnOptions (src/runtime/pty.zig:392) carries the same five fields. There is no directory anywhere on the path, so the child inherits the app's cwd from the fork at src/runtime/pty.zig:641.

fx.spawn's SpawnOptions (:5696) has no cwd either, for what it is worth — so this is a consistent absence rather than an inconsistency. It just costs more on the pty side, because a pty child is nearly always an interactive shell and an interactive shell nearly always wants to start somewhere. An app with several terminals open on different directories has no way to express it.

What the workaround looks like

const script = std.fmt.bufPrint(
    &script_storage,
    "cd '{s}' && exec {s} -i",
    .{ path, login_shell },
) catch { /* fall back to a plain interactive shell */ };
argv_storage[0] = login_shell;
argv_storage[1] = "-c";
argv_storage[2] = script;

This works. It is also three problems:

  1. It is a quoting bug waiting to happen. A path containing a single quote breaks the script, so the caller has to detect that case and fall back to a shell in the wrong directory rather than run a mis-quoted command. A path is not a shell word and should not have to be turned into one.
  2. It burns the shell's own -c. The child becomes a -c shell that execs an -i shell, so -c is unavailable for anything else and the process tree has a shape nobody chose.
  3. It is posix-only by construction. There is no cmd/powershell spelling of this trick worth trusting, so a Windows branch simply drops the directory.

The ask

/// The child's initial working directory. Null inherits the
/// app process's, as today.
cwd: ?[]const u8 = null,

on PtySpawnOptions, forwarded through pty.SpawnOptions to a chdir in the child.

The one design constraint is already written down: src/runtime/pty.zig:21 states that everything the child touches between fork and exec is async-signal-safe — "login_tty, dup2, close, execve, and _exit — no allocator, no setenv". chdir is async-signal-safe, and the path can be NUL-terminated before the fork alongside the argv/envp arrays that are already built there (:446), so this fits the existing rule rather than bending it. On Windows CreateProcessW (src/runtime/pty_windows.zig:806) takes lpCurrentDirectory directly.

A failed chdir should presumably fail the spawn the same way a failed execve does, through the exec self-pipe (:601) — "the pty forked but the program could not start" is exactly the case that machinery already distinguishes.

Verified against ea98365.

A patch is up: #220. Filed separately as the defect record — close whichever of the two is the redundant one.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions