diff --git a/util/eventbus/bus_test.go b/util/eventbus/bus_test.go index 67f68cd4a..f9e7ee3dd 100644 --- a/util/eventbus/bus_test.go +++ b/util/eventbus/bus_test.go @@ -236,6 +236,17 @@ func TestMonitor(t *testing.T) { } }) + t.Run("ZeroDone", func(t *testing.T) { + var zero eventbus.Monitor + + select { + case <-zero.Done(): + // OK + case <-time.After(time.Second): + t.Fatal("timeout waiting for zero monitor to be done") + } + }) + t.Run("ZeroClose", func(t *testing.T) { var zero eventbus.Monitor @@ -276,7 +287,13 @@ func TestMonitor(t *testing.T) { // While the goroutine is running, Wait does not complete. select { case <-done: - t.Error("monitor is ready before its goroutine is finished") + t.Error("monitor is ready before its goroutine is finished (Wait)") + default: + // OK + } + select { + case <-m.Done(): + t.Error("monitor is ready before its goroutine is finished (Done)") default: // OK } @@ -286,7 +303,13 @@ func TestMonitor(t *testing.T) { case <-done: // OK case <-time.After(time.Second): - t.Fatal("timeout waiting for monitor to complete") + t.Fatal("timeout waiting for monitor to complete (Wait)") + } + select { + case <-m.Done(): + // OK + case <-time.After(time.Second): + t.Fatal("timeout waiting for monitor to complete (Done)") } } } diff --git a/util/eventbus/monitor.go b/util/eventbus/monitor.go index 18cc2a413..db6fe1be4 100644 --- a/util/eventbus/monitor.go +++ b/util/eventbus/monitor.go @@ -3,9 +3,12 @@ package eventbus +import "tailscale.com/syncs" + // A Monitor monitors the execution of a goroutine processing events from a // [Client], allowing the caller to block until it is complete. The zero value -// of m is valid and its Close and Wait methods return immediately. +// of m is valid; its Close and Wait methods return immediately, and its Done +// method returns an already-closed channel. type Monitor struct { // These fields are immutable after initialization cli *Client @@ -32,6 +35,15 @@ func (m Monitor) Wait() { <-m.done } +// Done returns a channel that is closed when the monitored goroutine has +// finished executing. +func (m Monitor) Done() <-chan struct{} { + if m.done == nil { + return syncs.ClosedChan() + } + return m.done +} + // Monitor executes f in a new goroutine attended by a [Monitor]. The caller // is responsible for waiting for the goroutine to complete, by calling either // [Monitor.Close] or [Monitor.Wait].