fix(kill): match processes by their resolved socket directory and skip stuck ones on rescan
- commit
- eef302d49e00166152e58de75486bdb7bd9e8df7
- author
- brice · · 2026-09-23 21:23 UTC
- parent
- 436e203
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +123 -49
M internal/adapters/ipc/listener.go +5 -2
| @@ -181,9 +181,12 @@ func (l *unixListener) Addr() string { | |||
| 181 | 181 | // environments never fall back to the ordinary user's daemon. Listeners and | |
| 182 | 182 | // lifecycle locks still validate ownership and mode; no symlink is introduced. | |
| 183 | 183 | func SocketDir() string { | |
| 184 | - | uid := os.Getuid() | |
| 184 | + | return SocketDirFor(os.Getenv("XDG_RUNTIME_DIR"), os.Getuid()) | |
| 185 | + | } | |
| 185 | 186 | ||
| 186 | - | if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { | |
| 187 | + | // SocketDirFor is SocketDir for a given XDG_RUNTIME_DIR value and uid. | |
| 188 | + | func SocketDirFor(xdg string, uid int) string { | |
| 189 | + | if xdg != "" { | |
| 187 | 190 | dir := filepath.Join(xdg, "vev") | |
| 188 | 191 | if len(filepath.Join(dir, "broker", "broker.sock")) > muxSocketPathMax { | |
| 189 | 192 | return fmt.Sprintf("/tmp/vev-%d-%x", uid, sha256.Sum256([]byte(dir))) | |
| @@ -181,9 +181,12 @@ func (l *unixListener) Addr() string { | |||||
| 181 | // environments never fall back to the ordinary user's daemon. Listeners and | 181 | // environments never fall back to the ordinary user's daemon. Listeners and | ||
| 182 | // lifecycle locks still validate ownership and mode; no symlink is introduced. | 182 | // lifecycle locks still validate ownership and mode; no symlink is introduced. | ||
| 183 | func SocketDir() string { | 183 | func SocketDir() string { | ||
| 184 | - | uid := os.Getuid() | 184 | + | return SocketDirFor(os.Getenv("XDG_RUNTIME_DIR"), os.Getuid()) |
| 185 | + | } | |||
| 185 | 186 | ||||
| 186 | - | if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { | 187 | + | // SocketDirFor is SocketDir for a given XDG_RUNTIME_DIR value and uid. |
| 188 | + | func SocketDirFor(xdg string, uid int) string { | |||
| 189 | + | if xdg != "" { | |||
| 187 | dir := filepath.Join(xdg, "vev") | 190 | dir := filepath.Join(xdg, "vev") | ||
| 188 | if len(filepath.Join(dir, "broker", "broker.sock")) > muxSocketPathMax { | 191 | if len(filepath.Join(dir, "broker", "broker.sock")) > muxSocketPathMax { | ||
| 189 | return fmt.Sprintf("/tmp/vev-%d-%x", uid, sha256.Sum256([]byte(dir))) | 192 | return fmt.Sprintf("/tmp/vev-%d-%x", uid, sha256.Sum256([]byte(dir))) | ||
M internal/app/kill_all_linux.go +58 -33
| @@ -19,6 +19,7 @@ import ( | |||
| 19 | 19 | "github.com/bnema/vev/internal/adapters/brokeripc" | |
| 20 | 20 | "github.com/bnema/vev/internal/adapters/ipc" | |
| 21 | 21 | "github.com/bnema/vev/internal/adapters/lifecycle" | |
| 22 | + | "github.com/bnema/vev/internal/platform" | |
| 22 | 23 | ) | |
| 23 | 24 | ||
| 24 | 25 | const ( | |
| @@ -37,14 +38,14 @@ type vevProcess struct { | |||
| 37 | 38 | } | |
| 38 | 39 | ||
| 39 | 40 | // killAllScope selects which processes `kill --all` may stop: this user's | |
| 40 | - | // processes of the installed binary that run in the same vev runtime | |
| 41 | - | // directory, so VEV_ENV and XDG-sandboxed runs never touch each other. | |
| 41 | + | // processes of the installed binary that use the same vev socket directory, | |
| 42 | + | // so VEV_ENV and XDG-sandboxed runs never touch each other. | |
| 42 | 43 | type killAllScope struct { | |
| 43 | - | procRoot string | |
| 44 | - | exe string | |
| 45 | - | uid int | |
| 46 | - | self int | |
| 47 | - | runtimeDir string // XDG_RUNTIME_DIR as vev resolved it; "" when unset | |
| 44 | + | procRoot string | |
| 45 | + | exe string | |
| 46 | + | uid int | |
| 47 | + | self int | |
| 48 | + | socketDir string // ipc.SocketDir of the caller | |
| 48 | 49 | } | |
| 49 | 50 | ||
| 50 | 51 | // killAllReport is what one `kill --all` did. | |
| @@ -68,29 +69,46 @@ func runKillAll(ctx context.Context, out io.Writer) error { | |||
| 68 | 69 | return fmt.Errorf("vev: resolve executable: %w", err) | |
| 69 | 70 | } | |
| 70 | 71 | scope := killAllScope{ | |
| 71 | - | procRoot: "/proc", | |
| 72 | - | exe: installedPath(exe), | |
| 73 | - | uid: os.Getuid(), | |
| 74 | - | self: os.Getpid(), | |
| 75 | - | runtimeDir: os.Getenv("XDG_RUNTIME_DIR"), | |
| 72 | + | procRoot: "/proc", | |
| 73 | + | exe: installedPath(exe), | |
| 74 | + | uid: os.Getuid(), | |
| 75 | + | self: os.Getpid(), | |
| 76 | + | socketDir: ipc.SocketDir(), | |
| 76 | 77 | } | |
| 77 | - | report := stopEverything(ctx, scope, killAllStopTimeout, ipc.SocketDir()) | |
| 78 | + | report := stopEverything(ctx, scope, killAllStopTimeout) | |
| 78 | 79 | printKillAllReport(out, report) | |
| 79 | 80 | return errors.Join(report.errs...) | |
| 80 | 81 | } | |
| 81 | 82 | ||
| 82 | - | // stopEverything runs scan-and-stop rounds until a scan finds nothing, then removes | |
| 83 | - | // dead sockets. | |
| 84 | - | func stopEverything(ctx context.Context, scope killAllScope, timeout time.Duration, socketDir string) killAllReport { | |
| 83 | + | // stopEverything runs scan-and-stop rounds until a scan finds nothing, then | |
| 84 | + | // removes dead sockets. A process that already failed to die is not retried. | |
| 85 | + | func stopEverything(ctx context.Context, scope killAllScope, timeout time.Duration) killAllReport { | |
| 85 | 86 | var report killAllReport | |
| 86 | - | for round := 0; round < killAllRounds; round++ { | |
| 87 | + | failed := map[int]bool{} | |
| 88 | + | scan := func() ([]vevProcess, error) { | |
| 87 | 89 | procs, err := findVevProcesses(scope) | |
| 90 | + | live := procs[:0] | |
| 91 | + | for _, p := range procs { | |
| 92 | + | if !failed[p.pid] { | |
| 93 | + | live = append(live, p) | |
| 94 | + | } | |
| 95 | + | } | |
| 96 | + | return live, err | |
| 97 | + | } | |
| 98 | + | for round := 0; ; round++ { | |
| 99 | + | procs, err := scan() | |
| 88 | 100 | if err != nil { | |
| 89 | 101 | report.errs = append(report.errs, err) | |
| 90 | 102 | return report | |
| 91 | 103 | } | |
| 92 | 104 | if len(procs) == 0 { | |
| 93 | - | report.removed = removeDeadSockets(socketDir) | |
| 105 | + | if len(failed) == 0 { | |
| 106 | + | report.removed = removeDeadSockets(scope.socketDir) | |
| 107 | + | } | |
| 108 | + | return report | |
| 109 | + | } | |
| 110 | + | if round == killAllRounds { | |
| 111 | + | report.errs = append(report.errs, fmt.Errorf("vev: %d vev processes kept starting again; run `vev kill --all` once more", len(procs))) | |
| 94 | 112 | return report | |
| 95 | 113 | } | |
| 96 | 114 | // Clients first, so none of them can start a broker or daemon from | |
| @@ -104,17 +122,15 @@ func stopEverything(ctx context.Context, scope killAllScope, timeout time.Durati | |||
| 104 | 122 | } | |
| 105 | 123 | } | |
| 106 | 124 | for _, wave := range [][]vevProcess{clients, services} { | |
| 107 | - | stopWave(ctx, wave, timeout, &report) | |
| 125 | + | for _, p := range stopWave(ctx, wave, timeout, &report) { | |
| 126 | + | failed[p.pid] = true | |
| 127 | + | } | |
| 108 | 128 | } | |
| 109 | 129 | if ctx.Err() != nil { | |
| 110 | 130 | report.errs = append(report.errs, ctx.Err()) | |
| 111 | 131 | return report | |
| 112 | 132 | } | |
| 113 | 133 | } | |
| 114 | - | if left, err := findVevProcesses(scope); err == nil && len(left) > 0 { | |
| 115 | - | report.errs = append(report.errs, fmt.Errorf("vev: %d vev processes kept starting again; run `vev kill --all` once more", len(left))) | |
| 116 | - | } | |
| 117 | - | return report | |
| 118 | 134 | } | |
| 119 | 135 | ||
| 120 | 136 | // installedPath is the on-disk path an executable was started from, without | |
| @@ -148,8 +164,7 @@ func findVevProcesses(scope killAllScope) ([]vevProcess, error) { | |||
| 148 | 164 | if err != nil || installedPath(target) != scope.exe { | |
| 149 | 165 | continue | |
| 150 | 166 | } | |
| 151 | - | environ, err := os.ReadFile(filepath.Join(dir, "environ")) | |
| 152 | - | if err != nil || environValue(environ, "XDG_RUNTIME_DIR") != scope.runtimeDir { | |
| 167 | + | if processSocketDir(dir, scope.uid) != scope.socketDir { | |
| 153 | 168 | continue // another VEV_ENV or sandbox | |
| 154 | 169 | } | |
| 155 | 170 | cmdline, err := os.ReadFile(filepath.Join(dir, "cmdline")) | |
| @@ -161,15 +176,23 @@ func findVevProcesses(scope killAllScope) ([]vevProcess, error) { | |||
| 161 | 176 | return procs, nil | |
| 162 | 177 | } | |
| 163 | 178 | ||
| 164 | - | // environValue returns key's value in a NUL-separated environment, or "". | |
| 165 | - | func environValue(environ []byte, key string) string { | |
| 166 | - | prefix := key + "=" | |
| 179 | + | // processSocketDir resolves the vev socket directory of the process in | |
| 180 | + | // procDir from its initial environment and working directory, the same way | |
| 181 | + | // vev itself does after VEV_ENV activation. It returns "" when unreadable. | |
| 182 | + | func processSocketDir(procDir string, uid int) string { | |
| 183 | + | environ, err := os.ReadFile(filepath.Join(procDir, "environ")) | |
| 184 | + | if err != nil { | |
| 185 | + | return "" | |
| 186 | + | } | |
| 187 | + | env := map[string]string{} | |
| 167 | 188 | for _, entry := range strings.Split(string(environ), "\x00") { | |
| 168 | - | if value, ok := strings.CutPrefix(entry, prefix); ok { | |
| 169 | - | return value | |
| 189 | + | if key, value, ok := strings.Cut(entry, "="); ok { | |
| 190 | + | env[key] = value | |
| 170 | 191 | } | |
| 171 | 192 | } | |
| 172 | - | return "" | |
| 193 | + | cwd, _ := os.Readlink(filepath.Join(procDir, "cwd")) | |
| 194 | + | runtime := platform.RuntimeDirFor(func(key string) string { return env[key] }, cwd) | |
| 195 | + | return ipc.SocketDirFor(runtime, uid) | |
| 173 | 196 | } | |
| 174 | 197 | ||
| 175 | 198 | // processRole names a vev process from its NUL-separated command line. | |
| @@ -194,8 +217,8 @@ func processRole(cmdline []byte) string { | |||
| 194 | 217 | ||
| 195 | 218 | // stopWave sends SIGTERM to every process, waits up to timeout for all of them | |
| 196 | 219 | // to exit, then sends SIGKILL to the ones still running. One stuck process | |
| 197 | - | // never stops the rest of the wave. | |
| 198 | - | func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, report *killAllReport) { | |
| 220 | + | // never stops the rest of the wave; the ones that survive are returned. | |
| 221 | + | func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, report *killAllReport) (survivors []vevProcess) { | |
| 199 | 222 | for _, p := range procs { | |
| 200 | 223 | _ = syscall.Kill(p.pid, syscall.SIGTERM) | |
| 201 | 224 | } | |
| @@ -208,11 +231,13 @@ func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, re | |||
| 208 | 231 | _ = syscall.Kill(p.pid, syscall.SIGKILL) | |
| 209 | 232 | if err := waitProcessExit(context.WithoutCancel(ctx), p.pid, time.Second); err != nil { | |
| 210 | 233 | report.errs = append(report.errs, fmt.Errorf("vev: %s (pid %d) did not stop: %w", p.role, p.pid, err)) | |
| 234 | + | survivors = append(survivors, p) | |
| 211 | 235 | continue | |
| 212 | 236 | } | |
| 213 | 237 | report.stopped = append(report.stopped, p) | |
| 214 | 238 | report.forced = append(report.forced, p) | |
| 215 | 239 | } | |
| 240 | + | return survivors | |
| 216 | 241 | } | |
| 217 | 242 | ||
| 218 | 243 | // removeDeadSockets deletes vev sockets nobody listens on any more, so the | |
| @@ -19,6 +19,7 @@ import ( | |||||
| 19 | "github.com/bnema/vev/internal/adapters/brokeripc" | 19 | "github.com/bnema/vev/internal/adapters/brokeripc" | ||
| 20 | "github.com/bnema/vev/internal/adapters/ipc" | 20 | "github.com/bnema/vev/internal/adapters/ipc" | ||
| 21 | "github.com/bnema/vev/internal/adapters/lifecycle" | 21 | "github.com/bnema/vev/internal/adapters/lifecycle" | ||
| 22 | + | "github.com/bnema/vev/internal/platform" | |||
| 22 | ) | 23 | ) | ||
| 23 | 24 | ||||
| 24 | const ( | 25 | const ( | ||
| @@ -37,14 +38,14 @@ type vevProcess struct { | |||||
| 37 | } | 38 | } | ||
| 38 | 39 | ||||
| 39 | // killAllScope selects which processes `kill --all` may stop: this user's | 40 | // killAllScope selects which processes `kill --all` may stop: this user's | ||
| 40 | - | // processes of the installed binary that run in the same vev runtime | 41 | + | // processes of the installed binary that use the same vev socket directory, |
| 41 | - | // directory, so VEV_ENV and XDG-sandboxed runs never touch each other. | 42 | + | // so VEV_ENV and XDG-sandboxed runs never touch each other. |
| 42 | type killAllScope struct { | 43 | type killAllScope struct { | ||
| 43 | - | procRoot string | 44 | + | procRoot string |
| 44 | - | exe string | 45 | + | exe string |
| 45 | - | uid int | 46 | + | uid int |
| 46 | - | self int | 47 | + | self int |
| 47 | - | runtimeDir string // XDG_RUNTIME_DIR as vev resolved it; "" when unset | 48 | + | socketDir string // ipc.SocketDir of the caller |
| 48 | } | 49 | } | ||
| 49 | 50 | ||||
| 50 | // killAllReport is what one `kill --all` did. | 51 | // killAllReport is what one `kill --all` did. | ||
| @@ -68,29 +69,46 @@ func runKillAll(ctx context.Context, out io.Writer) error { | |||||
| 68 | return fmt.Errorf("vev: resolve executable: %w", err) | 69 | return fmt.Errorf("vev: resolve executable: %w", err) | ||
| 69 | } | 70 | } | ||
| 70 | scope := killAllScope{ | 71 | scope := killAllScope{ | ||
| 71 | - | procRoot: "/proc", | 72 | + | procRoot: "/proc", |
| 72 | - | exe: installedPath(exe), | 73 | + | exe: installedPath(exe), |
| 73 | - | uid: os.Getuid(), | 74 | + | uid: os.Getuid(), |
| 74 | - | self: os.Getpid(), | 75 | + | self: os.Getpid(), |
| 75 | - | runtimeDir: os.Getenv("XDG_RUNTIME_DIR"), | 76 | + | socketDir: ipc.SocketDir(), |
| 76 | } | 77 | } | ||
| 77 | - | report := stopEverything(ctx, scope, killAllStopTimeout, ipc.SocketDir()) | 78 | + | report := stopEverything(ctx, scope, killAllStopTimeout) |
| 78 | printKillAllReport(out, report) | 79 | printKillAllReport(out, report) | ||
| 79 | return errors.Join(report.errs...) | 80 | return errors.Join(report.errs...) | ||
| 80 | } | 81 | } | ||
| 81 | 82 | ||||
| 82 | - | // stopEverything runs scan-and-stop rounds until a scan finds nothing, then removes | 83 | + | // stopEverything runs scan-and-stop rounds until a scan finds nothing, then |
| 83 | - | // dead sockets. | 84 | + | // removes dead sockets. A process that already failed to die is not retried. |
| 84 | - | func stopEverything(ctx context.Context, scope killAllScope, timeout time.Duration, socketDir string) killAllReport { | 85 | + | func stopEverything(ctx context.Context, scope killAllScope, timeout time.Duration) killAllReport { |
| 85 | var report killAllReport | 86 | var report killAllReport | ||
| 86 | - | for round := 0; round < killAllRounds; round++ { | 87 | + | failed := map[int]bool{} |
| 88 | + | scan := func() ([]vevProcess, error) { | |||
| 87 | procs, err := findVevProcesses(scope) | 89 | procs, err := findVevProcesses(scope) | ||
| 90 | + | live := procs[:0] | |||
| 91 | + | for _, p := range procs { | |||
| 92 | + | if !failed[p.pid] { | |||
| 93 | + | live = append(live, p) | |||
| 94 | + | } | |||
| 95 | + | } | |||
| 96 | + | return live, err | |||
| 97 | + | } | |||
| 98 | + | for round := 0; ; round++ { | |||
| 99 | + | procs, err := scan() | |||
| 88 | if err != nil { | 100 | if err != nil { | ||
| 89 | report.errs = append(report.errs, err) | 101 | report.errs = append(report.errs, err) | ||
| 90 | return report | 102 | return report | ||
| 91 | } | 103 | } | ||
| 92 | if len(procs) == 0 { | 104 | if len(procs) == 0 { | ||
| 93 | - | report.removed = removeDeadSockets(socketDir) | 105 | + | if len(failed) == 0 { |
| 106 | + | report.removed = removeDeadSockets(scope.socketDir) | |||
| 107 | + | } | |||
| 108 | + | return report | |||
| 109 | + | } | |||
| 110 | + | if round == killAllRounds { | |||
| 111 | + | report.errs = append(report.errs, fmt.Errorf("vev: %d vev processes kept starting again; run `vev kill --all` once more", len(procs))) | |||
| 94 | return report | 112 | return report | ||
| 95 | } | 113 | } | ||
| 96 | // Clients first, so none of them can start a broker or daemon from | 114 | // Clients first, so none of them can start a broker or daemon from | ||
| @@ -104,17 +122,15 @@ func stopEverything(ctx context.Context, scope killAllScope, timeout time.Durati | |||||
| 104 | } | 122 | } | ||
| 105 | } | 123 | } | ||
| 106 | for _, wave := range [][]vevProcess{clients, services} { | 124 | for _, wave := range [][]vevProcess{clients, services} { | ||
| 107 | - | stopWave(ctx, wave, timeout, &report) | 125 | + | for _, p := range stopWave(ctx, wave, timeout, &report) { |
| 126 | + | failed[p.pid] = true | |||
| 127 | + | } | |||
| 108 | } | 128 | } | ||
| 109 | if ctx.Err() != nil { | 129 | if ctx.Err() != nil { | ||
| 110 | report.errs = append(report.errs, ctx.Err()) | 130 | report.errs = append(report.errs, ctx.Err()) | ||
| 111 | return report | 131 | return report | ||
| 112 | } | 132 | } | ||
| 113 | } | 133 | } | ||
| 114 | - | if left, err := findVevProcesses(scope); err == nil && len(left) > 0 { | |||
| 115 | - | report.errs = append(report.errs, fmt.Errorf("vev: %d vev processes kept starting again; run `vev kill --all` once more", len(left))) | |||
| 116 | - | } | |||
| 117 | - | return report | |||
| 118 | } | 134 | } | ||
| 119 | 135 | ||||
| 120 | // installedPath is the on-disk path an executable was started from, without | 136 | // installedPath is the on-disk path an executable was started from, without | ||
| @@ -148,8 +164,7 @@ func findVevProcesses(scope killAllScope) ([]vevProcess, error) { | |||||
| 148 | if err != nil || installedPath(target) != scope.exe { | 164 | if err != nil || installedPath(target) != scope.exe { | ||
| 149 | continue | 165 | continue | ||
| 150 | } | 166 | } | ||
| 151 | - | environ, err := os.ReadFile(filepath.Join(dir, "environ")) | 167 | + | if processSocketDir(dir, scope.uid) != scope.socketDir { |
| 152 | - | if err != nil || environValue(environ, "XDG_RUNTIME_DIR") != scope.runtimeDir { | |||
| 153 | continue // another VEV_ENV or sandbox | 168 | continue // another VEV_ENV or sandbox | ||
| 154 | } | 169 | } | ||
| 155 | cmdline, err := os.ReadFile(filepath.Join(dir, "cmdline")) | 170 | cmdline, err := os.ReadFile(filepath.Join(dir, "cmdline")) | ||
| @@ -161,15 +176,23 @@ func findVevProcesses(scope killAllScope) ([]vevProcess, error) { | |||||
| 161 | return procs, nil | 176 | return procs, nil | ||
| 162 | } | 177 | } | ||
| 163 | 178 | ||||
| 164 | - | // environValue returns key's value in a NUL-separated environment, or "". | 179 | + | // processSocketDir resolves the vev socket directory of the process in |
| 165 | - | func environValue(environ []byte, key string) string { | 180 | + | // procDir from its initial environment and working directory, the same way |
| 166 | - | prefix := key + "=" | 181 | + | // vev itself does after VEV_ENV activation. It returns "" when unreadable. |
| 182 | + | func processSocketDir(procDir string, uid int) string { | |||
| 183 | + | environ, err := os.ReadFile(filepath.Join(procDir, "environ")) | |||
| 184 | + | if err != nil { | |||
| 185 | + | return "" | |||
| 186 | + | } | |||
| 187 | + | env := map[string]string{} | |||
| 167 | for _, entry := range strings.Split(string(environ), "\x00") { | 188 | for _, entry := range strings.Split(string(environ), "\x00") { | ||
| 168 | - | if value, ok := strings.CutPrefix(entry, prefix); ok { | 189 | + | if key, value, ok := strings.Cut(entry, "="); ok { |
| 169 | - | return value | 190 | + | env[key] = value |
| 170 | } | 191 | } | ||
| 171 | } | 192 | } | ||
| 172 | - | return "" | 193 | + | cwd, _ := os.Readlink(filepath.Join(procDir, "cwd")) |
| 194 | + | runtime := platform.RuntimeDirFor(func(key string) string { return env[key] }, cwd) | |||
| 195 | + | return ipc.SocketDirFor(runtime, uid) | |||
| 173 | } | 196 | } | ||
| 174 | 197 | ||||
| 175 | // processRole names a vev process from its NUL-separated command line. | 198 | // processRole names a vev process from its NUL-separated command line. | ||
| @@ -194,8 +217,8 @@ func processRole(cmdline []byte) string { | |||||
| 194 | 217 | ||||
| 195 | // stopWave sends SIGTERM to every process, waits up to timeout for all of them | 218 | // stopWave sends SIGTERM to every process, waits up to timeout for all of them | ||
| 196 | // to exit, then sends SIGKILL to the ones still running. One stuck process | 219 | // to exit, then sends SIGKILL to the ones still running. One stuck process | ||
| 197 | - | // never stops the rest of the wave. | 220 | + | // never stops the rest of the wave; the ones that survive are returned. |
| 198 | - | func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, report *killAllReport) { | 221 | + | func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, report *killAllReport) (survivors []vevProcess) { |
| 199 | for _, p := range procs { | 222 | for _, p := range procs { | ||
| 200 | _ = syscall.Kill(p.pid, syscall.SIGTERM) | 223 | _ = syscall.Kill(p.pid, syscall.SIGTERM) | ||
| 201 | } | 224 | } | ||
| @@ -208,11 +231,13 @@ func stopWave(ctx context.Context, procs []vevProcess, timeout time.Duration, re | |||||
| 208 | _ = syscall.Kill(p.pid, syscall.SIGKILL) | 231 | _ = syscall.Kill(p.pid, syscall.SIGKILL) | ||
| 209 | if err := waitProcessExit(context.WithoutCancel(ctx), p.pid, time.Second); err != nil { | 232 | if err := waitProcessExit(context.WithoutCancel(ctx), p.pid, time.Second); err != nil { | ||
| 210 | report.errs = append(report.errs, fmt.Errorf("vev: %s (pid %d) did not stop: %w", p.role, p.pid, err)) | 233 | report.errs = append(report.errs, fmt.Errorf("vev: %s (pid %d) did not stop: %w", p.role, p.pid, err)) | ||
| 234 | + | survivors = append(survivors, p) | |||
| 211 | continue | 235 | continue | ||
| 212 | } | 236 | } | ||
| 213 | report.stopped = append(report.stopped, p) | 237 | report.stopped = append(report.stopped, p) | ||
| 214 | report.forced = append(report.forced, p) | 238 | report.forced = append(report.forced, p) | ||
| 215 | } | 239 | } | ||
| 240 | + | return survivors | |||
| 216 | } | 241 | } | ||
| 217 | 242 | ||||
| 218 | // removeDeadSockets deletes vev sockets nobody listens on any more, so the | 243 | // removeDeadSockets deletes vev sockets nobody listens on any more, so the | ||
M internal/app/kill_all_linux_test.go +45 -14
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "bytes" | |
| 7 | 7 | "context" | |
| 8 | 8 | "errors" | |
| 9 | + | "fmt" | |
| 9 | 10 | "io" | |
| 10 | 11 | "net" | |
| 11 | 12 | "os" | |
| @@ -17,7 +18,9 @@ import ( | |||
| 17 | 18 | ||
| 18 | 19 | "github.com/stretchr/testify/require" | |
| 19 | 20 | ||
| 21 | + | "github.com/bnema/vev/internal/adapters/ipc" | |
| 20 | 22 | "github.com/bnema/vev/internal/adapters/lifecycle" | |
| 23 | + | "github.com/bnema/vev/internal/platform" | |
| 21 | 24 | ) | |
| 22 | 25 | ||
| 23 | 26 | func TestWaitProcessExit(t *testing.T) { | |
| @@ -77,44 +80,72 @@ func TestProcessRole(t *testing.T) { | |||
| 77 | 80 | } | |
| 78 | 81 | ||
| 79 | 82 | // TestFindVevProcesses runs against a fake /proc: only this user's processes | |
| 80 | - | // of the installed binary (also once replaced on disk) in the same runtime | |
| 81 | - | // directory are found, never self. | |
| 83 | + | // of the installed binary (also once replaced on disk) that resolve to the | |
| 84 | + | // caller's socket directory are found, never self. | |
| 82 | 85 | func TestFindVevProcesses(t *testing.T) { | |
| 83 | 86 | root := t.TempDir() | |
| 84 | 87 | const exe = "/opt/bin/vev" | |
| 85 | - | const runtime = "XDG_RUNTIME_DIR=/run/user/1000" | |
| 86 | 88 | uid := os.Getuid() | |
| 87 | - | add := func(pid, target, cmdline, environ string) { | |
| 89 | + | userRun := "XDG_RUNTIME_DIR=/run/user/1000\x00" | |
| 90 | + | add := func(pid, target, cmdline, environ, cwd string) { | |
| 88 | 91 | dir := filepath.Join(root, pid) | |
| 89 | 92 | require.NoError(t, os.Mkdir(dir, 0o755)) | |
| 90 | 93 | require.NoError(t, os.Symlink(target, filepath.Join(dir, "exe"))) | |
| 94 | + | require.NoError(t, os.Symlink(cwd, filepath.Join(dir, "cwd"))) | |
| 91 | 95 | require.NoError(t, os.WriteFile(filepath.Join(dir, "cmdline"), []byte(cmdline), 0o644)) | |
| 92 | 96 | require.NoError(t, os.WriteFile(filepath.Join(dir, "environ"), []byte(environ), 0o644)) | |
| 93 | 97 | } | |
| 94 | - | add("10", exe, "vev\x00", "HOME=/h\x00"+runtime+"\x00") | |
| 95 | - | add("11", exe+" (deleted)", "/proc/self/exe\x00--daemon\x00", runtime+"\x00") | |
| 96 | - | add("12", "/usr/bin/emacs", "emacs\x00--daemon\x00", runtime+"\x00") | |
| 97 | - | add("13", "/tmp/vev-test-bin", "/tmp/vev-test-bin\x00--daemon\x00", runtime+"\x00") | |
| 98 | - | add("14", exe, "vev\x00", runtime+"\x00") // self | |
| 99 | - | add("15", exe, "/proc/self/exe\x00--daemon\x00", "XDG_RUNTIME_DIR=/repo/.dev/test/runtime\x00") | |
| 98 | + | add("10", exe, "vev\x00", "HOME=/h\x00"+userRun, "/h") | |
| 99 | + | add("11", exe+" (deleted)", "/proc/self/exe\x00--daemon\x00", userRun, "/h") | |
| 100 | + | add("12", "/usr/bin/emacs", "emacs\x00--daemon\x00", userRun, "/h") | |
| 101 | + | add("13", "/tmp/vev-test-bin", "/tmp/vev-test-bin\x00--daemon\x00", userRun, "/h") | |
| 102 | + | add("14", exe, "vev\x00", userRun, "/h") // self | |
| 103 | + | // A VEV_ENV=dev client keeps the user's XDG value in its initial | |
| 104 | + | // environment; its children carry the rewritten one. | |
| 105 | + | add("15", exe, "vev\x00", "VEV_ENV=dev\x00"+userRun, "/repo") | |
| 106 | + | add("16", exe, "/proc/self/exe\x00--daemon\x00", "VEV_ENV=dev\x00VEV_ENV_ROOT=/repo/.dev\x00XDG_RUNTIME_DIR=/repo/.dev/dev/runtime\x00", "/") | |
| 100 | 107 | require.NoError(t, os.Mkdir(filepath.Join(root, "self"), 0o755)) | |
| 101 | 108 | ||
| 102 | - | scope := killAllScope{procRoot: root, exe: exe, uid: uid, self: 14, runtimeDir: "/run/user/1000"} | |
| 109 | + | scope := killAllScope{procRoot: root, exe: exe, uid: uid, self: 14, socketDir: "/run/user/1000/vev"} | |
| 103 | 110 | got, err := findVevProcesses(scope) | |
| 104 | 111 | require.NoError(t, err) | |
| 105 | 112 | require.ElementsMatch(t, []vevProcess{{pid: 10, role: "client"}, {pid: 11, role: "daemon"}}, got) | |
| 106 | 113 | ||
| 107 | - | scope.runtimeDir = "/repo/.dev/test/runtime" | |
| 114 | + | scope.socketDir = "/repo/.dev/dev/runtime/vev" | |
| 108 | 115 | dev, err := findVevProcesses(scope) | |
| 109 | 116 | require.NoError(t, err) | |
| 110 | - | require.Equal(t, []vevProcess{{pid: 15, role: "daemon"}}, dev, "a VEV_ENV run only sees its own environment") | |
| 117 | + | require.ElementsMatch(t, []vevProcess{{pid: 15, role: "client"}, {pid: 16, role: "daemon"}}, dev, "a VEV_ENV run sees its own client and children") | |
| 111 | 118 | ||
| 112 | 119 | scope.uid = uid + 1 | |
| 120 | + | scope.socketDir = "/run/user/1000/vev" | |
| 113 | 121 | none, err := findVevProcesses(scope) | |
| 114 | 122 | require.NoError(t, err) | |
| 115 | 123 | require.Empty(t, none, "another user's processes are never matched") | |
| 116 | 124 | } | |
| 117 | 125 | ||
| 126 | + | func TestRuntimeDirResolutionMatchesVev(t *testing.T) { | |
| 127 | + | tests := []struct { | |
| 128 | + | name string | |
| 129 | + | env map[string]string | |
| 130 | + | cwd string | |
| 131 | + | want string | |
| 132 | + | }{ | |
| 133 | + | {name: "plain", env: map[string]string{"XDG_RUNTIME_DIR": "/run/user/1000"}, want: "/run/user/1000"}, | |
| 134 | + | {name: "dev env from cwd", env: map[string]string{"VEV_ENV": "dev", "XDG_RUNTIME_DIR": "/run/user/1000"}, cwd: "/repo", want: "/repo/.dev/dev/runtime"}, | |
| 135 | + | {name: "dev env with root", env: map[string]string{"VEV_ENV": "t", "VEV_ENV_ROOT": "/r"}, cwd: "/elsewhere", want: "/r/t/runtime"}, | |
| 136 | + | {name: "invalid env name is ignored", env: map[string]string{"VEV_ENV": "../x", "XDG_RUNTIME_DIR": "/run/user/1000"}, want: "/run/user/1000"}, | |
| 137 | + | } | |
| 138 | + | for _, tt := range tests { | |
| 139 | + | t.Run(tt.name, func(t *testing.T) { | |
| 140 | + | require.Equal(t, tt.want, platform.RuntimeDirFor(func(k string) string { return tt.env[k] }, tt.cwd)) | |
| 141 | + | }) | |
| 142 | + | } | |
| 143 | + | if info, err := os.Stat(fmt.Sprintf("/run/user/%d", os.Getuid())); err == nil && info.IsDir() { | |
| 144 | + | require.Equal(t, ipc.SocketDirFor(fmt.Sprintf("/run/user/%d", os.Getuid()), os.Getuid()), ipc.SocketDirFor("", os.Getuid()), | |
| 145 | + | "unset XDG_RUNTIME_DIR and /run/user/UID resolve to the same socket directory") | |
| 146 | + | } | |
| 147 | + | } | |
| 148 | + | ||
| 118 | 149 | // startIgnoringTerm starts a shell that ignores SIGTERM and reports once the | |
| 119 | 150 | // trap is installed. | |
| 120 | 151 | func startIgnoringTerm(t *testing.T) (*exec.Cmd, <-chan *os.ProcessState) { | |
| @@ -139,7 +170,7 @@ func TestStopWaveEscalatesToSigkillAndKeepsGoing(t *testing.T) { | |||
| 139 | 170 | go func() { _ = polite.Wait() }() | |
| 140 | 171 | ||
| 141 | 172 | var report killAllReport | |
| 142 | - | stopWave(context.Background(), []vevProcess{{pid: stubborn.Process.Pid, role: "daemon"}, {pid: polite.Process.Pid, role: "client"}}, 100*time.Millisecond, &report) | |
| 173 | + | _ = stopWave(context.Background(), []vevProcess{{pid: stubborn.Process.Pid, role: "daemon"}, {pid: polite.Process.Pid, role: "client"}}, 100*time.Millisecond, &report) | |
| 143 | 174 | ||
| 144 | 175 | state := <-done | |
| 145 | 176 | status, ok := state.Sys().(syscall.WaitStatus) | |
| @@ -6,6 +6,7 @@ import ( | |||||
| 6 | "bytes" | 6 | "bytes" | ||
| 7 | "context" | 7 | "context" | ||
| 8 | "errors" | 8 | "errors" | ||
| 9 | + | "fmt" | |||
| 9 | "io" | 10 | "io" | ||
| 10 | "net" | 11 | "net" | ||
| 11 | "os" | 12 | "os" | ||
| @@ -17,7 +18,9 @@ import ( | |||||
| 17 | 18 | ||||
| 18 | "github.com/stretchr/testify/require" | 19 | "github.com/stretchr/testify/require" | ||
| 19 | 20 | ||||
| 21 | + | "github.com/bnema/vev/internal/adapters/ipc" | |||
| 20 | "github.com/bnema/vev/internal/adapters/lifecycle" | 22 | "github.com/bnema/vev/internal/adapters/lifecycle" | ||
| 23 | + | "github.com/bnema/vev/internal/platform" | |||
| 21 | ) | 24 | ) | ||
| 22 | 25 | ||||
| 23 | func TestWaitProcessExit(t *testing.T) { | 26 | func TestWaitProcessExit(t *testing.T) { | ||
| @@ -77,44 +80,72 @@ func TestProcessRole(t *testing.T) { | |||||
| 77 | } | 80 | } | ||
| 78 | 81 | ||||
| 79 | // TestFindVevProcesses runs against a fake /proc: only this user's processes | 82 | // TestFindVevProcesses runs against a fake /proc: only this user's processes | ||
| 80 | - | // of the installed binary (also once replaced on disk) in the same runtime | 83 | + | // of the installed binary (also once replaced on disk) that resolve to the |
| 81 | - | // directory are found, never self. | 84 | + | // caller's socket directory are found, never self. |
| 82 | func TestFindVevProcesses(t *testing.T) { | 85 | func TestFindVevProcesses(t *testing.T) { | ||
| 83 | root := t.TempDir() | 86 | root := t.TempDir() | ||
| 84 | const exe = "/opt/bin/vev" | 87 | const exe = "/opt/bin/vev" | ||
| 85 | - | const runtime = "XDG_RUNTIME_DIR=/run/user/1000" | |||
| 86 | uid := os.Getuid() | 88 | uid := os.Getuid() | ||
| 87 | - | add := func(pid, target, cmdline, environ string) { | 89 | + | userRun := "XDG_RUNTIME_DIR=/run/user/1000\x00" |
| 90 | + | add := func(pid, target, cmdline, environ, cwd string) { | |||
| 88 | dir := filepath.Join(root, pid) | 91 | dir := filepath.Join(root, pid) | ||
| 89 | require.NoError(t, os.Mkdir(dir, 0o755)) | 92 | require.NoError(t, os.Mkdir(dir, 0o755)) | ||
| 90 | require.NoError(t, os.Symlink(target, filepath.Join(dir, "exe"))) | 93 | require.NoError(t, os.Symlink(target, filepath.Join(dir, "exe"))) | ||
| 94 | + | require.NoError(t, os.Symlink(cwd, filepath.Join(dir, "cwd"))) | |||
| 91 | require.NoError(t, os.WriteFile(filepath.Join(dir, "cmdline"), []byte(cmdline), 0o644)) | 95 | require.NoError(t, os.WriteFile(filepath.Join(dir, "cmdline"), []byte(cmdline), 0o644)) | ||
| 92 | require.NoError(t, os.WriteFile(filepath.Join(dir, "environ"), []byte(environ), 0o644)) | 96 | require.NoError(t, os.WriteFile(filepath.Join(dir, "environ"), []byte(environ), 0o644)) | ||
| 93 | } | 97 | } | ||
| 94 | - | add("10", exe, "vev\x00", "HOME=/h\x00"+runtime+"\x00") | 98 | + | add("10", exe, "vev\x00", "HOME=/h\x00"+userRun, "/h") |
| 95 | - | add("11", exe+" (deleted)", "/proc/self/exe\x00--daemon\x00", runtime+"\x00") | 99 | + | add("11", exe+" (deleted)", "/proc/self/exe\x00--daemon\x00", userRun, "/h") |
| 96 | - | add("12", "/usr/bin/emacs", "emacs\x00--daemon\x00", runtime+"\x00") | 100 | + | add("12", "/usr/bin/emacs", "emacs\x00--daemon\x00", userRun, "/h") |
| 97 | - | add("13", "/tmp/vev-test-bin", "/tmp/vev-test-bin\x00--daemon\x00", runtime+"\x00") | 101 | + | add("13", "/tmp/vev-test-bin", "/tmp/vev-test-bin\x00--daemon\x00", userRun, "/h") |
| 98 | - | add("14", exe, "vev\x00", runtime+"\x00") // self | 102 | + | add("14", exe, "vev\x00", userRun, "/h") // self |
| 99 | - | add("15", exe, "/proc/self/exe\x00--daemon\x00", "XDG_RUNTIME_DIR=/repo/.dev/test/runtime\x00") | 103 | + | // A VEV_ENV=dev client keeps the user's XDG value in its initial |
| 104 | + | // environment; its children carry the rewritten one. | |||
| 105 | + | add("15", exe, "vev\x00", "VEV_ENV=dev\x00"+userRun, "/repo") | |||
| 106 | + | add("16", exe, "/proc/self/exe\x00--daemon\x00", "VEV_ENV=dev\x00VEV_ENV_ROOT=/repo/.dev\x00XDG_RUNTIME_DIR=/repo/.dev/dev/runtime\x00", "/") | |||
| 100 | require.NoError(t, os.Mkdir(filepath.Join(root, "self"), 0o755)) | 107 | require.NoError(t, os.Mkdir(filepath.Join(root, "self"), 0o755)) | ||
| 101 | 108 | ||||
| 102 | - | scope := killAllScope{procRoot: root, exe: exe, uid: uid, self: 14, runtimeDir: "/run/user/1000"} | 109 | + | scope := killAllScope{procRoot: root, exe: exe, uid: uid, self: 14, socketDir: "/run/user/1000/vev"} |
| 103 | got, err := findVevProcesses(scope) | 110 | got, err := findVevProcesses(scope) | ||
| 104 | require.NoError(t, err) | 111 | require.NoError(t, err) | ||
| 105 | require.ElementsMatch(t, []vevProcess{{pid: 10, role: "client"}, {pid: 11, role: "daemon"}}, got) | 112 | require.ElementsMatch(t, []vevProcess{{pid: 10, role: "client"}, {pid: 11, role: "daemon"}}, got) | ||
| 106 | 113 | ||||
| 107 | - | scope.runtimeDir = "/repo/.dev/test/runtime" | 114 | + | scope.socketDir = "/repo/.dev/dev/runtime/vev" |
| 108 | dev, err := findVevProcesses(scope) | 115 | dev, err := findVevProcesses(scope) | ||
| 109 | require.NoError(t, err) | 116 | require.NoError(t, err) | ||
| 110 | - | require.Equal(t, []vevProcess{{pid: 15, role: "daemon"}}, dev, "a VEV_ENV run only sees its own environment") | 117 | + | require.ElementsMatch(t, []vevProcess{{pid: 15, role: "client"}, {pid: 16, role: "daemon"}}, dev, "a VEV_ENV run sees its own client and children") |
| 111 | 118 | ||||
| 112 | scope.uid = uid + 1 | 119 | scope.uid = uid + 1 | ||
| 120 | + | scope.socketDir = "/run/user/1000/vev" | |||
| 113 | none, err := findVevProcesses(scope) | 121 | none, err := findVevProcesses(scope) | ||
| 114 | require.NoError(t, err) | 122 | require.NoError(t, err) | ||
| 115 | require.Empty(t, none, "another user's processes are never matched") | 123 | require.Empty(t, none, "another user's processes are never matched") | ||
| 116 | } | 124 | } | ||
| 117 | 125 | ||||
| 126 | + | func TestRuntimeDirResolutionMatchesVev(t *testing.T) { | |||
| 127 | + | tests := []struct { | |||
| 128 | + | name string | |||
| 129 | + | env map[string]string | |||
| 130 | + | cwd string | |||
| 131 | + | want string | |||
| 132 | + | }{ | |||
| 133 | + | {name: "plain", env: map[string]string{"XDG_RUNTIME_DIR": "/run/user/1000"}, want: "/run/user/1000"}, | |||
| 134 | + | {name: "dev env from cwd", env: map[string]string{"VEV_ENV": "dev", "XDG_RUNTIME_DIR": "/run/user/1000"}, cwd: "/repo", want: "/repo/.dev/dev/runtime"}, | |||
| 135 | + | {name: "dev env with root", env: map[string]string{"VEV_ENV": "t", "VEV_ENV_ROOT": "/r"}, cwd: "/elsewhere", want: "/r/t/runtime"}, | |||
| 136 | + | {name: "invalid env name is ignored", env: map[string]string{"VEV_ENV": "../x", "XDG_RUNTIME_DIR": "/run/user/1000"}, want: "/run/user/1000"}, | |||
| 137 | + | } | |||
| 138 | + | for _, tt := range tests { | |||
| 139 | + | t.Run(tt.name, func(t *testing.T) { | |||
| 140 | + | require.Equal(t, tt.want, platform.RuntimeDirFor(func(k string) string { return tt.env[k] }, tt.cwd)) | |||
| 141 | + | }) | |||
| 142 | + | } | |||
| 143 | + | if info, err := os.Stat(fmt.Sprintf("/run/user/%d", os.Getuid())); err == nil && info.IsDir() { | |||
| 144 | + | require.Equal(t, ipc.SocketDirFor(fmt.Sprintf("/run/user/%d", os.Getuid()), os.Getuid()), ipc.SocketDirFor("", os.Getuid()), | |||
| 145 | + | "unset XDG_RUNTIME_DIR and /run/user/UID resolve to the same socket directory") | |||
| 146 | + | } | |||
| 147 | + | } | |||
| 148 | + | ||||
| 118 | // startIgnoringTerm starts a shell that ignores SIGTERM and reports once the | 149 | // startIgnoringTerm starts a shell that ignores SIGTERM and reports once the | ||
| 119 | // trap is installed. | 150 | // trap is installed. | ||
| 120 | func startIgnoringTerm(t *testing.T) (*exec.Cmd, <-chan *os.ProcessState) { | 151 | func startIgnoringTerm(t *testing.T) (*exec.Cmd, <-chan *os.ProcessState) { | ||
| @@ -139,7 +170,7 @@ func TestStopWaveEscalatesToSigkillAndKeepsGoing(t *testing.T) { | |||||
| 139 | go func() { _ = polite.Wait() }() | 170 | go func() { _ = polite.Wait() }() | ||
| 140 | 171 | ||||
| 141 | var report killAllReport | 172 | var report killAllReport | ||
| 142 | - | stopWave(context.Background(), []vevProcess{{pid: stubborn.Process.Pid, role: "daemon"}, {pid: polite.Process.Pid, role: "client"}}, 100*time.Millisecond, &report) | 173 | + | _ = stopWave(context.Background(), []vevProcess{{pid: stubborn.Process.Pid, role: "daemon"}, {pid: polite.Process.Pid, role: "client"}}, 100*time.Millisecond, &report) |
| 143 | 174 | ||||
| 144 | state := <-done | 175 | state := <-done | ||
| 145 | status, ok := state.Sys().(syscall.WaitStatus) | 176 | status, ok := state.Sys().(syscall.WaitStatus) | ||
M internal/platform/dir.go +15 -0
| @@ -78,6 +78,21 @@ func asciiAlphaNumeric(value byte) bool { | |||
| 78 | 78 | return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' || value >= '0' && value <= '9' | |
| 79 | 79 | } | |
| 80 | 80 | ||
| 81 | + | // RuntimeDirFor returns the XDG_RUNTIME_DIR a vev process with environment | |
| 82 | + | // getenv and working directory cwd uses once VEV_ENV is activated. It lets a | |
| 83 | + | // caller resolve another process' runtime from its initial environment. | |
| 84 | + | func RuntimeDirFor(getenv func(string) string, cwd string) string { | |
| 85 | + | name := getenv("VEV_ENV") | |
| 86 | + | if name == "" || !validDevelopmentEnvironmentName(name) { | |
| 87 | + | return getenv("XDG_RUNTIME_DIR") | |
| 88 | + | } | |
| 89 | + | base := getenv(devEnvironmentRootEnv) | |
| 90 | + | if base == "" { | |
| 91 | + | base = filepath.Join(cwd, ".dev") | |
| 92 | + | } | |
| 93 | + | return filepath.Join(base, name, "runtime") | |
| 94 | + | } | |
| 95 | + | ||
| 81 | 96 | // DevelopmentEnvironmentTempDir returns the private temporary-file directory | |
| 82 | 97 | // for an activated development environment. Ordinary invocations use the | |
| 83 | 98 | // existing os.TempDir fallback in the daemon. | |
| @@ -78,6 +78,21 @@ func asciiAlphaNumeric(value byte) bool { | |||||
| 78 | return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' || value >= '0' && value <= '9' | 78 | return value >= 'A' && value <= 'Z' || value >= 'a' && value <= 'z' || value >= '0' && value <= '9' | ||
| 79 | } | 79 | } | ||
| 80 | 80 | ||||
| 81 | + | // RuntimeDirFor returns the XDG_RUNTIME_DIR a vev process with environment | |||
| 82 | + | // getenv and working directory cwd uses once VEV_ENV is activated. It lets a | |||
| 83 | + | // caller resolve another process' runtime from its initial environment. | |||
| 84 | + | func RuntimeDirFor(getenv func(string) string, cwd string) string { | |||
| 85 | + | name := getenv("VEV_ENV") | |||
| 86 | + | if name == "" || !validDevelopmentEnvironmentName(name) { | |||
| 87 | + | return getenv("XDG_RUNTIME_DIR") | |||
| 88 | + | } | |||
| 89 | + | base := getenv(devEnvironmentRootEnv) | |||
| 90 | + | if base == "" { | |||
| 91 | + | base = filepath.Join(cwd, ".dev") | |||
| 92 | + | } | |||
| 93 | + | return filepath.Join(base, name, "runtime") | |||
| 94 | + | } | |||
| 95 | + | ||||
| 81 | // DevelopmentEnvironmentTempDir returns the private temporary-file directory | 96 | // DevelopmentEnvironmentTempDir returns the private temporary-file directory | ||
| 82 | // for an activated development environment. Ordinary invocations use the | 97 | // for an activated development environment. Ordinary invocations use the | ||
| 83 | // existing os.TempDir fallback in the daemon. | 98 | // existing os.TempDir fallback in the daemon. | ||