From 6fafae88fd8cfcb972c1ed98d8eb591a910043d2 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 22 Jun 2026 14:19:15 +0300 Subject: Fix browser process reaping for mq0 --- internal/ui/keyactions.go | 6 ++--- internal/ui/table_test.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/internal/ui/keyactions.go b/internal/ui/keyactions.go index e8e0c55..ef0189a 100644 --- a/internal/ui/keyactions.go +++ b/internal/ui/keyactions.go @@ -122,9 +122,9 @@ func openURLCmd(browserCmd, url string, taskID int) tea.Cmd { if err := cmd.Start(); err != nil { return openURLDoneMsg{err: fmt.Errorf("opening browser: %w", err)} } - if err := cmd.Process.Release(); err != nil { - return openURLDoneMsg{err: fmt.Errorf("releasing browser process: %w", err)} - } + go func() { + _ = cmd.Wait() + }() return openURLDoneMsg{taskID: taskID} } } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 065d817..f255fe3 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "regexp" + "runtime" "strings" "testing" "time" @@ -2715,6 +2716,71 @@ func TestOpenURLCommandDoesNotWaitForBrowserExit(t *testing.T) { } } +func TestOpenURLCommandReapsBrowserProcess(t *testing.T) { + if runtime.GOOS != "linux" { + t.Skip("requires /proc process state") + } + + tmp := t.TempDir() + taskPath := filepath.Join(tmp, "task") + pidPath := filepath.Join(tmp, "browser.pid") + browserPath := filepath.Join(tmp, "browser") + + taskScript := "#!/bin/sh\n" + + "if echo \"$@\" | grep -q export; then\n" + + " echo '{\"id\":1,\"uuid\":\"1\",\"description\":\"alpha https://example.com\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n" + + " exit 0\n" + + "fi\n" + if err := os.WriteFile(taskPath, []byte(taskScript), 0o755); err != nil { + t.Fatal(err) + } + + browserScript := "#!/bin/sh\n" + + "echo $$ > " + pidPath + "\n" + if err := os.WriteFile(browserPath, []byte(browserScript), 0o755); err != nil { + t.Fatal(err) + } + setupEnv(t, taskPath) + + m, err := New(nil, browserPath) + if err != nil { + t.Fatalf("New: %v", err) + } + + mv, cmd := (&m).Update(tea.KeyPressMsg{Code: 'o', Text: "o"}) + if cmd == nil { + t.Fatalf("open URL unexpectedly returned no command") + } + m = *mv.(*Model) + + msg := cmd() + done, ok := msg.(openURLDoneMsg) + if !ok { + t.Fatalf("open URL command returned %T, want openURLDoneMsg", msg) + } + if done.err != nil { + t.Fatalf("open URL command failed: %v", done.err) + } + + if !waitForFile(pidPath, 2*time.Second) { + t.Fatalf("browser pid file was not written") + } + data, err := os.ReadFile(pidPath) + if err != nil { + t.Fatalf("read browser pid: %v", err) + } + procPath := filepath.Join("/proc", strings.TrimSpace(string(data))) + + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + if _, err := os.Stat(procPath); errors.Is(err, os.ErrNotExist) { + return + } + time.Sleep(10 * time.Millisecond) + } + t.Fatalf("browser process still exists at %s; child may not have been reaped", procPath) +} + func TestUltraReloadPreservesFilteredSelection(t *testing.T) { tmp := t.TempDir() taskPath, phaseFile := setupUltraReloadTaskSet(t, tmp) -- cgit v1.2.3