diff --git a/go/forgekey/runner.go b/go/forgekey/runner.go index 14bba8a..422e868 100644 --- a/go/forgekey/runner.go +++ b/go/forgekey/runner.go @@ -43,7 +43,8 @@ func ExecRunnerFor(bound time.Duration) Runner { // ExecRunnerClean is ExecRunnerFor whose commands get exactly the env argument as their environment, and nothing of // this process's (issue 1497; novox/hq ADR 0466 §4: no registry password in a fetch's environment). The caller -// gives PATH and HOME itself. A directory is the command's own business (`git -C`, `go -C`). +// gives PATH and HOME itself. A directory is the command's own business (`git -C`, `go -C`): it is started in the HOME +// it is given, or the machine's temporary directory, never in this process's (stableDir). func ExecRunnerClean(bound time.Duration) Runner { return func(ctx context.Context, env []string, name string, args ...string) Ran { return execRun(ctx, bound, true, env, name, args...) @@ -62,6 +63,7 @@ func execRun(ctx context.Context, bound time.Duration, clean bool, env []string, } else { cmd.Env = append(os.Environ(), env...) } + cmd.Dir = stableDir(clean, env) var out, errb bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &errb err := cmd.Run() @@ -83,6 +85,29 @@ func execRun(ctx context.Context, bound time.Duration, clean bool, env []string, return r } +// stableDir is the directory a command starts in when it names none (novox/hq issue 734). **Never one that may be +// deleted under it**: a module runs in the directory its build was unpacked in, which a new build replaces, and a +// command started there inherits it — `go version` then fails "getwd: no such file or directory" before it reads +// anything. A clean command starts in the HOME it is given, or the temporary directory; any other keeps this +// process's directory while it exists, and starts in the temporary directory once it is gone. "" is this process's. +func stableDir(clean bool, env []string) string { + if !clean { + if _, err := os.Getwd(); err == nil { + return "" + } + return os.TempDir() + } + for i := len(env) - 1; i >= 0; i-- { + if home, ok := strings.CutPrefix(env[i], "HOME="); ok { + if info, err := os.Stat(home); err == nil && info.IsDir() { + return home + } + break + } + } + return os.TempDir() +} + func firstLine(s string) string { for _, l := range strings.Split(s, "\n") { if l = strings.TrimSpace(l); l != "" { diff --git a/go/forgekey/runner_test.go b/go/forgekey/runner_test.go index 13f3669..2dc09b4 100644 --- a/go/forgekey/runner_test.go +++ b/go/forgekey/runner_test.go @@ -48,13 +48,29 @@ func TestExecRunnerForZeroIsCutByItsContext(t *testing.T) { } } -// 1497/AC4: ExecRunnerClean(0) runs env with only the given words: PATH and HOME, not the process's secret. +// 1497/AC4: ExecRunnerClean(0) runs env with only the given words: PATH and HOME, not the process's secret. And issue +// 734: run from a process whose own directory was deleted under it (a module whose build a new one replaced), a clean +// command starts in its HOME, and any other in the temporary directory, rather than failing to find where it is. func TestExecRunnerCleanGivesTheCommandOnlyTheEnvArgument(t *testing.T) { t.Setenv("MESH_TEST_SECRET", "x") r := ExecRunnerClean(0)(context.Background(), []string{"PATH=" + os.Getenv("PATH"), "HOME=/h"}, "env") if r.Status != 0 || !strings.Contains(r.Stdout, "HOME=/h") || !strings.Contains(r.Stdout, "PATH=") || strings.Contains(r.Stdout, "MESH_TEST_SECRET") { t.Errorf("ran = %+v", r) } + + home, gone := t.TempDir(), t.TempDir() + t.Chdir(gone) + if err := os.Remove(gone); err != nil { + t.Fatal(err) + } + r = ExecRunnerClean(0)(context.Background(), []string{"PATH=" + os.Getenv("PATH"), "HOME=" + home}, "pwd") + if r.Status != 0 || strings.TrimSpace(r.Stdout) != home { + t.Errorf("a clean command run from a deleted directory = %+v, want it started in its HOME %s", r, home) + } + r = ExecRunnerFor(0)(context.Background(), nil, "pwd") + if r.Status != 0 || strings.TrimSpace(r.Stdout) != os.TempDir() { + t.Errorf("a command run from a deleted directory = %+v, want it started in %s", r, os.TempDir()) + } } // 1497/AC5: ExecRunnerFor(0) is unchanged: the env argument is added to the process's environment.