package proc import ( "encoding/json" "os" "os/exec" "path/filepath" "syscall" "testing" "time" ) const watchLockHelperEnv = "BROWSEROS_DEV_WATCH_LOCK_HELPER" func TestMain(m *testing.M) { if os.Getenv(watchLockHelperEnv) == "1" { runWatchLockHelper() return } os.Exit(m.Run()) } func TestWatchRunPathsStableAndDistinct(t *testing.T) { baseDir := t.TempDir() identity := WatchRunIdentity{ Mode: "watch", Profile: "/tmp/browseros-dev", Ports: Ports{CDP: 9005, Server: 9105, Extension: 9305}, } first := watchRunPaths(baseDir, identity) second := watchRunPaths(baseDir, identity) if first != second { t.Fatalf("expected stable paths, got %#v and %#v", first, second) } withDifferentPort := identity withDifferentPort.Ports.Server = 9106 third := watchRunPaths(baseDir, withDifferentPort) if third.Lock == first.Lock || third.State == first.State { t.Fatalf("expected distinct paths for different ports, got %#v and %#v", first, third) } } func TestBrowserProfilePIDsFromPSSelectsOnlyDevAndTestProfiles(t *testing.T) { output := ` 111 111 /Applications/BrowserOS.app/Contents/MacOS/BrowserOS --user-data-dir=/tmp/browseros-dev 222 222 /Applications/BrowserOS.app/Contents/MacOS/BrowserOS --user-data-dir=/tmp/browseros-dev-abcd 333 333 /Applications/BrowserOS.app/Contents/MacOS/BrowserOS --user-data-dir=/var/folders/x/browseros-test-abcd 444 444 /Applications/BrowserOS.app/Contents/MacOS/BrowserOS --user-data-dir=/Users/me/Library/Application Support/BrowserOS 555 555 rg browseros-test- ` pids := browserProfilePIDsFromPS(output) if len(pids) != 3 || pids[0] != 111 || pids[1] != 222 || pids[2] != 333 { t.Fatalf("expected dev/test browser pids, got %#v", pids) } } func TestAcquireWatchRunLockWritesStateAndReleases(t *testing.T) { baseDir := t.TempDir() identity := WatchRunIdentity{ Mode: "watch", Profile: "/tmp/browseros-dev", Ports: Ports{CDP: 9005, Server: 9105, Extension: 9305}, } lock, stopped, err := AcquireWatchRunLockInDir(baseDir, identity, time.Second) if err != nil { t.Fatalf("AcquireWatchRunLockInDir returned error: %v", err) } if stopped { t.Fatal("expected first acquisition not to stop another run") } paths := watchRunPaths(baseDir, identity) state, err := ReadWatchRunState(paths.State) if err != nil { t.Fatalf("ReadWatchRunState returned error: %v", err) } if state.PID != os.Getpid() { t.Fatalf("expected state PID %d, got %d", os.Getpid(), state.PID) } if state.PGID <= 0 { t.Fatalf("expected positive PGID, got %d", state.PGID) } if state.Identity != identity { t.Fatalf("expected identity %#v, got %#v", identity, state.Identity) } if err := lock.Close(); err != nil { t.Fatalf("closing lock: %v", err) } if _, err := os.Stat(paths.State); !os.IsNotExist(err) { t.Fatalf("expected state file to be removed on close, got %v", err) } if _, err := os.Stat(paths.Lock); err != nil { t.Fatalf("expected lock file path to remain reusable, got %v", err) } lock, stopped, err = AcquireWatchRunLockInDir(baseDir, identity, time.Second) if err != nil { t.Fatalf("reacquiring lock returned error: %v", err) } if stopped { t.Fatal("expected reacquisition after close not to stop another run") } if err := lock.Close(); err != nil { t.Fatalf("closing reacquired lock: %v", err) } } func TestAcquireWatchRunLockRejectsInvalidPorts(t *testing.T) { identity := WatchRunIdentity{ Mode: "watch", Profile: "/tmp/browseros-dev", Ports: Ports{CDP: 9005, Server: 65536, Extension: 9305}, } if _, _, err := AcquireWatchRunLockInDir(t.TempDir(), identity, time.Second); err == nil { t.Fatal("expected invalid port error") } } func TestAcquireWatchRunLockStopsExistingOwnerByStatePGID(t *testing.T) { baseDir := t.TempDir() readyPath := filepath.Join(baseDir, "ready") identity := WatchRunIdentity{ Mode: "watch", Profile: "/tmp/browseros-dev", Ports: Ports{CDP: 9005, Server: 9105, Extension: 9305}, } identityJSON, err := json.Marshal(identity) if err != nil { t.Fatal(err) } cmd := exec.Command(os.Args[0], "-test.run=TestMain") cmd.Env = append(os.Environ(), watchLockHelperEnv+"=1", "BROWSEROS_DEV_WATCH_LOCK_BASE="+baseDir, "BROWSEROS_DEV_WATCH_LOCK_READY="+readyPath, "BROWSEROS_DEV_WATCH_LOCK_IDENTITY="+string(identityJSON), ) cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err := cmd.Start(); err != nil { t.Fatalf("starting helper: %v", err) } defer cmd.Process.Kill() waitForFile(t, readyPath, 3*time.Second) lock, stopped, err := AcquireWatchRunLockInDir(baseDir, identity, 3*time.Second) if err != nil { t.Fatalf("AcquireWatchRunLockInDir returned error: %v", err) } defer lock.Close() if !stopped { t.Fatal("expected takeover to stop existing owner") } done := make(chan error, 1) go func() { done <- cmd.Wait() }() select { case <-done: case <-time.After(3 * time.Second): t.Fatal("expected helper process to exit after takeover") } } func runWatchLockHelper() { baseDir := os.Getenv("BROWSEROS_DEV_WATCH_LOCK_BASE") readyPath := os.Getenv("BROWSEROS_DEV_WATCH_LOCK_READY") var identity WatchRunIdentity if err := json.Unmarshal([]byte(os.Getenv("BROWSEROS_DEV_WATCH_LOCK_IDENTITY")), &identity); err != nil { os.Exit(2) } lock, _, err := AcquireWatchRunLockInDir(baseDir, identity, time.Second) if err != nil { os.Exit(3) } defer lock.Close() if err := os.WriteFile(readyPath, []byte("ready\n"), 0o644); err != nil { os.Exit(4) } time.Sleep(30 * time.Second) } func waitForFile(t *testing.T, path string, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for { if _, err := os.Stat(path); err == nil { return } if time.Now().After(deadline) { t.Fatalf("timed out waiting for %s", path) } time.Sleep(50 * time.Millisecond) } }