fix(obs): isolate role XDG directories
- commit
- 8d86d44339a6cdb432a69eeb1d76e367b2cc888e
- author
- brice · · 2026-07-13 02:25 UTC
- parent
- 89cf334
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
2 files · +54 -1
M cmd/vev-perf-harness/main.go +1 -1
| @@ -287,7 +287,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 287 | 287 | // a role working directory: daemon-owned subprocess cleanup may otherwise | |
| 288 | 288 | // treat the evidence directory as its working tree and remove a preallocated | |
| 289 | 289 | // trace while a later repetition is being merged. | |
| 290 | - | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT"), traceEnvironment(m)...) | |
| 290 | + | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...) | |
| 291 | 291 | cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir, | |
| 292 | 292 | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | |
| 293 | 293 | if m.Role == "client" { | |
| @@ -287,7 +287,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||||
| 287 | // a role working directory: daemon-owned subprocess cleanup may otherwise | 287 | // a role working directory: daemon-owned subprocess cleanup may otherwise | ||
| 288 | // treat the evidence directory as its working tree and remove a preallocated | 288 | // treat the evidence directory as its working tree and remove a preallocated | ||
| 289 | // trace while a later repetition is being merged. | 289 | // trace while a later repetition is being merged. | ||
| 290 | - | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT"), traceEnvironment(m)...) | 290 | + | cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...) |
| 291 | cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir, | 291 | cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir, | ||
| 292 | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | 292 | "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color") | ||
| 293 | if m.Role == "client" { | 293 | if m.Role == "client" { | ||
M cmd/vev-perf-harness/main_test.go +53 -0
| @@ -130,6 +130,59 @@ func TestCLIProcessWaitReadyRequiresDaemonSocket(t *testing.T) { | |||
| 130 | 130 | } | |
| 131 | 131 | } | |
| 132 | 132 | ||
| 133 | + | func TestCLILauncherIsolatesRoleXDGEnvironmentAcrossRepetitions(t *testing.T) { | |
| 134 | + | captureDir := t.TempDir() | |
| 135 | + | t.Setenv("CAPTURE_DIR", captureDir) | |
| 136 | + | t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "inherited-runtime")) | |
| 137 | + | t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "inherited-state")) | |
| 138 | + | ||
| 139 | + | bin := filepath.Join(t.TempDir(), "capture-env") | |
| 140 | + | if err := os.WriteFile(bin, []byte("#!/bin/sh\nenv > \"$CAPTURE_DIR/$VEV_PERF_PROCESS_ID\"\n"), 0o700); err != nil { | |
| 141 | + | t.Fatal(err) | |
| 142 | + | } | |
| 143 | + | launcher := &cliLauncher{bin: bin} | |
| 144 | + | for run := 1; run <= 3; run++ { | |
| 145 | + | runDir := filepath.Join(t.TempDir(), fmt.Sprintf("run-%d", run)) | |
| 146 | + | if err := os.MkdirAll(runDir, 0o700); err != nil { | |
| 147 | + | t.Fatal(err) | |
| 148 | + | } | |
| 149 | + | roles := []string{"daemon", "client"} | |
| 150 | + | for _, role := range roles { | |
| 151 | + | id := fmt.Sprintf("run-%d-%s", run, role) | |
| 152 | + | p, err := launcher.Launch(processMapping{ | |
| 153 | + | Role: role, TracePath: filepath.Join(runDir, role+".jsonl"), ProcessID: id, | |
| 154 | + | }, roleCommand{Args: []string{"test"}}) | |
| 155 | + | if err != nil { | |
| 156 | + | t.Fatalf("run %d %s launch: %v", run, role, err) | |
| 157 | + | } | |
| 158 | + | runtimeDir := launcher.runtimes[runDir] | |
| 159 | + | if err := p.Close(); err != nil { | |
| 160 | + | t.Fatalf("run %d %s close: %v", run, role, err) | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | env, err := os.ReadFile(filepath.Join(captureDir, id)) | |
| 164 | + | if err != nil { | |
| 165 | + | t.Fatalf("run %d %s capture: %v", run, role, err) | |
| 166 | + | } | |
| 167 | + | for _, want := range []string{ | |
| 168 | + | "XDG_RUNTIME_DIR=" + runtimeDir, | |
| 169 | + | "XDG_STATE_HOME=" + filepath.Join(runDir, "state"), | |
| 170 | + | } { | |
| 171 | + | name := strings.SplitN(want, "=", 2)[0] | |
| 172 | + | if count := bytes.Count(env, []byte(name+"=")); count != 1 { | |
| 173 | + | t.Fatalf("run %d %s %s entries = %d, want exactly one; env:\n%s", run, role, name, count, env) | |
| 174 | + | } | |
| 175 | + | if !bytes.Contains(env, []byte(want+"\n")) { | |
| 176 | + | t.Fatalf("run %d %s missing %q; env:\n%s", run, role, want, env) | |
| 177 | + | } | |
| 178 | + | } | |
| 179 | + | } | |
| 180 | + | if err := launcher.releaseRuntime(runDir); err != nil { | |
| 181 | + | t.Fatalf("run %d release runtime: %v", run, err) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | } | |
| 185 | + | ||
| 133 | 186 | func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) { | |
| 134 | 187 | wait := make(chan error, 1) | |
| 135 | 188 | wait <- nil | |
| @@ -130,6 +130,59 @@ func TestCLIProcessWaitReadyRequiresDaemonSocket(t *testing.T) { | |||||
| 130 | } | 130 | } | ||
| 131 | } | 131 | } | ||
| 132 | 132 | ||||
| 133 | + | func TestCLILauncherIsolatesRoleXDGEnvironmentAcrossRepetitions(t *testing.T) { | |||
| 134 | + | captureDir := t.TempDir() | |||
| 135 | + | t.Setenv("CAPTURE_DIR", captureDir) | |||
| 136 | + | t.Setenv("XDG_RUNTIME_DIR", filepath.Join(t.TempDir(), "inherited-runtime")) | |||
| 137 | + | t.Setenv("XDG_STATE_HOME", filepath.Join(t.TempDir(), "inherited-state")) | |||
| 138 | + | ||||
| 139 | + | bin := filepath.Join(t.TempDir(), "capture-env") | |||
| 140 | + | if err := os.WriteFile(bin, []byte("#!/bin/sh\nenv > \"$CAPTURE_DIR/$VEV_PERF_PROCESS_ID\"\n"), 0o700); err != nil { | |||
| 141 | + | t.Fatal(err) | |||
| 142 | + | } | |||
| 143 | + | launcher := &cliLauncher{bin: bin} | |||
| 144 | + | for run := 1; run <= 3; run++ { | |||
| 145 | + | runDir := filepath.Join(t.TempDir(), fmt.Sprintf("run-%d", run)) | |||
| 146 | + | if err := os.MkdirAll(runDir, 0o700); err != nil { | |||
| 147 | + | t.Fatal(err) | |||
| 148 | + | } | |||
| 149 | + | roles := []string{"daemon", "client"} | |||
| 150 | + | for _, role := range roles { | |||
| 151 | + | id := fmt.Sprintf("run-%d-%s", run, role) | |||
| 152 | + | p, err := launcher.Launch(processMapping{ | |||
| 153 | + | Role: role, TracePath: filepath.Join(runDir, role+".jsonl"), ProcessID: id, | |||
| 154 | + | }, roleCommand{Args: []string{"test"}}) | |||
| 155 | + | if err != nil { | |||
| 156 | + | t.Fatalf("run %d %s launch: %v", run, role, err) | |||
| 157 | + | } | |||
| 158 | + | runtimeDir := launcher.runtimes[runDir] | |||
| 159 | + | if err := p.Close(); err != nil { | |||
| 160 | + | t.Fatalf("run %d %s close: %v", run, role, err) | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | env, err := os.ReadFile(filepath.Join(captureDir, id)) | |||
| 164 | + | if err != nil { | |||
| 165 | + | t.Fatalf("run %d %s capture: %v", run, role, err) | |||
| 166 | + | } | |||
| 167 | + | for _, want := range []string{ | |||
| 168 | + | "XDG_RUNTIME_DIR=" + runtimeDir, | |||
| 169 | + | "XDG_STATE_HOME=" + filepath.Join(runDir, "state"), | |||
| 170 | + | } { | |||
| 171 | + | name := strings.SplitN(want, "=", 2)[0] | |||
| 172 | + | if count := bytes.Count(env, []byte(name+"=")); count != 1 { | |||
| 173 | + | t.Fatalf("run %d %s %s entries = %d, want exactly one; env:\n%s", run, role, name, count, env) | |||
| 174 | + | } | |||
| 175 | + | if !bytes.Contains(env, []byte(want+"\n")) { | |||
| 176 | + | t.Fatalf("run %d %s missing %q; env:\n%s", run, role, want, env) | |||
| 177 | + | } | |||
| 178 | + | } | |||
| 179 | + | } | |||
| 180 | + | if err := launcher.releaseRuntime(runDir); err != nil { | |||
| 181 | + | t.Fatalf("run %d release runtime: %v", run, err) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | } | |||
| 185 | + | ||||
| 133 | func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) { | 186 | func TestCLIProcessCloseUsesPublicDaemonShutdown(t *testing.T) { | ||
| 134 | wait := make(chan error, 1) | 187 | wait := make(chan error, 1) | ||
| 135 | wait <- nil | 188 | wait <- nil | ||