|
| @@ -241,45 +242,67 @@ func winePrefixPath(prefixPath string) string { |
| 241 | | return filepath.Join(prefixPath, "pfx") | 242 | | return filepath.Join(prefixPath, "pfx") |
| 242 | | } | 243 | | } |
| 243 | | | 244 | | |
| 244 | - | // startFakeWineProcess starts a long-running process that the prefix scan accepts as | 245 | + | // TestWineProcessHelper runs only in a child test binary. Unlike a renamed |
| 245 | - | // part of a wine session, either through wine's environment (env) or through a | 246 | + | // system sleep, this also works with multicall coreutils that reject aliases. |
| 246 | - | // Windows executable name. A name other than "sleep" runs a copy of the sleep | 247 | + | func TestWineProcessHelper(t *testing.T) { |
| 247 | - | // binary, so /proc/<pid>/comm matches it. | 248 | + | name := os.Getenv("BNETCTL_TEST_PROCESS_NAME") |
| 248 | - | func startFakeWineProcess(t *testing.T, name string, env ...string) *exec.Cmd { | 249 | + | if name == "" { |
| 249 | - | t.Helper() | 250 | + | return |
| | | 251 | + | } |
| | | 252 | + | if err := os.WriteFile("/proc/self/comm", []byte(name), 0600); err != nil { |
| | | 253 | + | t.Fatal(err) |
| | | 254 | + | } |
| | | 255 | + | fmt.Println("ready") |
| | | 256 | + | for { |
| | | 257 | + | time.Sleep(time.Hour) |
| | | 258 | + | } |
| | | 259 | + | } |
| 250 | | | 260 | | |
| | | 261 | + | type fakeWineProcess struct { |
| | | 262 | + | *exec.Cmd |
| | | 263 | + | done chan struct{} |
| | | 264 | + | exitErr error |
| | | 265 | + | } |
| | | 266 | + | |
| | | 267 | + | // startFakeWineProcess waits until comm has been set before allowing a scan. |
| | | 268 | + | // Every child has exactly one Wait, including clients killed by stub Wine. |
| | | 269 | + | func startFakeWineProcess(t *testing.T, name string, env ...string) *fakeWineProcess { |
| | | 270 | + | t.Helper() |
| 251 | | if len(name) > 15 { | 271 | | if len(name) > 15 { |
| 252 | - | t.Fatalf("process name %q exceeds the 15 bytes the kernel keeps in comm", name) | 272 | + | t.Fatalf("process name %q exceeds comm limit", name) |
| 253 | | } | 273 | | } |
| 254 | - | | 274 | + | exe, err := os.Executable() |
| 255 | - | sleep, err := exec.LookPath("sleep") | | | |
| 256 | | if err != nil { | 275 | | if err != nil { |
| 257 | - | t.Skipf("sleep is unavailable: %v", err) | 276 | + | t.Fatal(err) |
| 258 | | } | 277 | | } |
| 259 | - | | 278 | + | cmd := exec.Command(exe, "-test.run=^TestWineProcessHelper$") |
| 260 | - | exe := sleep | 279 | + | cmd.Env = append(os.Environ(), append(env, "BNETCTL_TEST_PROCESS_NAME="+name)...) |
| 261 | - | if name != filepath.Base(sleep) { | 280 | + | stdout, err := cmd.StdoutPipe() |
| 262 | - | data, err := os.ReadFile(sleep) | 281 | + | if err != nil { |
| 263 | - | if err != nil { | 282 | + | t.Fatal(err) |
| 264 | - | t.Fatal(err) | | | |
| 265 | - | } | | | |
| 266 | - | exe = filepath.Join(t.TempDir(), name) | | | |
| 267 | - | if err := os.WriteFile(exe, data, 0o755); err != nil { | | | |
| 268 | - | t.Fatal(err) | | | |
| 269 | - | } | | | |
| 270 | | } | 283 | | } |
| 271 | - | | 284 | + | cmd.Stderr = os.Stderr |
| 272 | - | cmd := exec.Command(exe, "60") | | | |
| 273 | - | cmd.Env = append(os.Environ(), env...) | | | |
| 274 | | if err := cmd.Start(); err != nil { | 285 | | if err := cmd.Start(); err != nil { |
| 275 | - | t.Fatalf("start %s: %v", name, err) | 286 | + | t.Fatal(err) |
| | | 287 | + | } |
| | | 288 | + | child := &fakeWineProcess{Cmd: cmd, done: make(chan struct{})} |
| | | 289 | + | go func() { child.exitErr = cmd.Wait(); close(child.done) }() |
| | | 290 | + | t.Cleanup(func() { _ = cmd.Process.Kill(); <-child.done }) |
| | | 291 | + | ready := make(chan string, 1) |
| | | 292 | + | go func() { line, _ := bufio.NewReader(stdout).ReadString('\n'); ready <- line }() |
| | | 293 | + | select { |
| | | 294 | + | case line := <-ready: |
| | | 295 | + | if line != "ready\n" { |
| | | 296 | + | t.Fatalf("child did not become ready: %q", line) |
| | | 297 | + | } |
| | | 298 | + | case <-time.After(5 * time.Second): |
| | | 299 | + | t.Fatal("child readiness timed out") |
| 276 | | } | 300 | | } |
| 277 | - | t.Cleanup(func() { _ = cmd.Process.Kill() }) | 301 | + | return child |
| 278 | - | return cmd | | | |
| 279 | | } | 302 | | } |
| 280 | | | 303 | | |
| 281 | | // requireStopped fails the test unless the result reports exactly the given process. | 304 | | // requireStopped fails the test unless the result reports exactly the given process. |
| 282 | - | func requireStopped(t *testing.T, result domain.StopResult, cmd *exec.Cmd, name string) { | 305 | + | func requireStopped(t *testing.T, result domain.StopResult, cmd *fakeWineProcess, name string) { |
| 283 | | t.Helper() | 306 | | t.Helper() |
| 284 | | | 307 | | |
| 285 | | if result.Wineserver != nil || len(result.Session) != 0 { | 308 | | if result.Wineserver != nil || len(result.Session) != 0 { |
|
| @@ -392,15 +415,13 @@ func TestGracefulKillReportsWineserverWithoutSnapshot(t *testing.T) { |
| 392 | | } | 415 | | } |
| 393 | | | 416 | | |
| 394 | | // waitForExit fails the test unless the process exits on its own. | 417 | | // waitForExit fails the test unless the process exits on its own. |
| 395 | - | func waitForExit(t *testing.T, cmd *exec.Cmd) { | 418 | + | func waitForExit(t *testing.T, cmd *fakeWineProcess) { |
| 396 | | t.Helper() | 419 | | t.Helper() |
| 397 | | | 420 | | |
| 398 | - | done := make(chan error, 1) | | | |
| 399 | - | go func() { done <- cmd.Wait() }() | | | |
| 400 | | select { | 421 | | select { |
| 401 | - | case err := <-done: | 422 | + | case <-cmd.done: |
| 402 | - | if err != nil { | 423 | + | if cmd.exitErr != nil { |
| 403 | - | t.Logf("process exited: %v", err) | 424 | + | t.Logf("process exited: %v", cmd.exitErr) |
| 404 | | } | 425 | | } |
| 405 | | case <-time.After(2 * time.Second): | 426 | | case <-time.After(2 * time.Second): |
| 406 | | t.Fatalf("process %d survived the kill", cmd.Process.Pid) | 427 | | t.Fatalf("process %d survived the kill", cmd.Process.Pid) |