hare

The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

commit 7e657ecd8c171350c5b440c9cf8c9a56116328e3
parent 9c2a84de884dd95c13081b775897edd86cd13851
Author: Drew DeVault <sir@cmpwn.com>
Date:   Mon, 22 Nov 2021 10:00:04 +0100

os::exec: improve fd manipulation

Signed-off-by: Drew DeVault <sir@cmpwn.com>

Diffstat:
Mcmd/hare/release.ha | 12+++++++++---
Mos/exec/cmd.ha | 46++++++++++++++++++++++++++++++++--------------
Mos/exec/exec+linux.ha | 29+++++++++++++++++++++++++++--
Mos/exec/types.ha | 8+++++++-
4 files changed, 75 insertions(+), 20 deletions(-)

diff --git a/cmd/hare/release.ha b/cmd/hare/release.ha @@ -37,8 +37,7 @@ const changelog_template: str = "# This is the changelog for your release. It ha {} version {} "; -const initial_template: str = " -# These are the release notes for the initial release of {0}. +const initial_template: str = "# These are the release notes for the initial release of {0}. # # Any lines which begin with \"#\", like this one, are for your information # only, and will be removed from the final release notes. Edit this file to your @@ -275,7 +274,6 @@ fn choosekey() (str | release_error) = { }; fn signtag(name: str, tag: str, key: str) (void | release_error) = { - fmt::printfln("Using SSH key {} for release signature.", key)!; const prefix = fmt::asprintf("--prefix={}-{}/", name, tag); defer free(prefix); const archive = exec::cmd("git", "archive", @@ -285,6 +283,12 @@ fn signtag(name: str, tag: str, key: str) (void | release_error) = { const note = exec::cmd("git", "notes", "add", "-F", "-", tag)?; exec::setenv(&note, "GIT_NOTES_REF", "refs/notes/signatures/tar.gz"); + // Squelch "Signing data on standard input" + // TODO: It might be better to capture this and print it to stderr + // ourselves if ssh-keygen exits nonzero, so that the error details are + // available to the user for diagnosis. + exec::addfile(&ssh, exec::nullfd, os::stderr); + const pipe1 = unix::pipe()?; const pipe2 = unix::pipe()?; exec::addfile(&archive, pipe1.1, os::stdout_file); @@ -305,6 +309,7 @@ fn signtag(name: str, tag: str, key: str) (void | release_error) = { fn git_runcmd(args: str...) (void | release_error) = { const cmd = exec::cmd("git", args...)?; + exec::addfile(&cmd, exec::nullfd, os::stderr); const proc = exec::start(&cmd)?; const status = exec::wait(&proc)?; return exec::check(&status)?; @@ -315,6 +320,7 @@ fn git_readcmd(args: str...) (str | release_error) = { defer io::close(pipe.0); const cmd = exec::cmd("git", args...)?; exec::addfile(&cmd, pipe.1, os::stdout_file); + exec::addfile(&cmd, exec::nullfd, os::stderr); const proc = exec::start(&cmd)?; io::close(pipe.1); const result = io::drain(pipe.0)?; diff --git a/os/exec/cmd.ha b/os/exec/cmd.ha @@ -116,26 +116,44 @@ export fn setenv(cmd: *command, key: str, value: str) void = { append(cmd.env, strings::concat(fullkey, value)); }; -// Adds an [[io::file]] to the child process's file table. All mappings are -// performed atomically, such that the following code swaps stdout and stderr: +// Configures a file in the child process's file table, such that 'from' is +// mapped onto file descriptor slot 'to' via one of the dup(2) family of +// syscalls. This is done atomically, such that the following code swaps stdout +// and stderr: // // exec::addfile(&cmd, os::stdout_file, os::stderr); // exec::addfile(&cmd, os::stderr, os::stdout_file); // -// If the same [[io::file]] is mapped to multiple times, only the last mapping -// will take effect. -export fn addfile(cmd: *command, old: io::file, new: io::file) void = { - // TODO: Can we make old be an io::handle? - append(cmd.files, (old, new)); +// Pass [[os::exec::nullfd]] in the 'to' argument to map the child's file +// descriptor to /dev/null or the appropriate platform-specific equivalent. +// +// Pass [[os::exec::closefd]] in the 'to' argument to close a file descriptor +// which was not opened with the CLOEXEC flag. Note that Hare opens all files +// with CLOEXEC by default, so this is not usually necessary. +export fn addfile( + cmd: *command, + from: (io::file | nullfd | closefd), + to: io::file, +) void = { + // TODO: Can we make 'to' be an io::handle? + append(cmd.files, (from, to)); +}; + +// Closes all standard files (stdin, stdout, and stderr) in the child process. +// Many programs do not work well under these conditions; you may want +// [[nullstd]] instead. +export fn closestd(cmd: *command) void = { + addfile(cmd, closefd, os::stdin_file); + addfile(cmd, closefd, os::stdout_file); + addfile(cmd, closefd, os::stderr); }; -// Adds [[os::stdin]], [[os::stdout]], and [[os::stderr]] from the parent -// process (i.e. the one which calls this function) into the child process's -// file table. -export fn addstd(cmd: *command) void = { - addfile(cmd, os::stdin_file, os::stdin_file); - addfile(cmd, os::stdout_file, os::stdout_file); - addfile(cmd, os::stderr, os::stderr); +// Redirects all standard files (stdin, stdout, and stderr) to /dev/null or the +// platform-specific equivalent. +export fn nullstd(cmd: *command) void = { + addfile(cmd, nullfd, os::stdin_file); + addfile(cmd, nullfd, os::stdout_file); + addfile(cmd, nullfd, os::stderr); }; fn lookup(name: str) (platform_cmd | void) = { diff --git a/os/exec/exec+linux.ha b/os/exec/exec+linux.ha @@ -57,8 +57,19 @@ fn platform_exec(cmd: *command) error = { envp = env: *[*]nullable *const char; }; + let need_devnull = false; for (let i = 0z; i < len(cmd.files); i += 1) { - cmd.files[i].0 = match (rt::fcntl(cmd.files[i].0, rt::F_DUPFD_CLOEXEC, 0)) { + const from = match (cmd.files[i].0) { + case file: io::file => + yield file; + case nullfd => + need_devnull = true; + continue; + case closefd => + continue; + }; + + cmd.files[i].0 = match (rt::fcntl(from, rt::F_DUPFD_CLOEXEC, 0)) { case fd: int => yield fd; case err: rt::errno => @@ -66,8 +77,22 @@ fn platform_exec(cmd: *command) error = { }; }; + const devnull: io::file = if (need_devnull) { + yield os::open("/dev/null")!; + } else -1; + for (let i = 0z; i < len(cmd.files); i += 1) { - match (rt::dup2(cmd.files[i].0, cmd.files[i].1)) { + const from = match (cmd.files[i].0) { + case file: io::file => + yield file; + case nullfd => + yield devnull; + case closefd => + io::close(cmd.files[i].1); + continue; + }; + + match (rt::dup2(from, cmd.files[i].1)) { case int => void; case e: rt::errno => return errors::errno(e); diff --git a/os/exec/types.ha b/os/exec/types.ha @@ -1,12 +1,18 @@ use errors; use io; +// Represents a "null" file descriptor, e.g. /dev/null. +export type nullfd = void; + +// Used to close a file descriptor which does not have the CLOEXEC flag set. +export type closefd = void; + // An executable command. export type command = struct { platform: platform_cmd, argv: []str, env: []str, - files: [](io::file, io::file), + files: []((io::file | nullfd | closefd), io::file), }; // Returned when path resolution fails to find a command by its name.