|
|
@@ -96,6 +96,7 @@ import (
|
|
|
"tailscale.com/types/views"
|
|
|
"tailscale.com/util/deephash"
|
|
|
"tailscale.com/util/dnsname"
|
|
|
+ "tailscale.com/util/goroutines"
|
|
|
"tailscale.com/util/httpm"
|
|
|
"tailscale.com/util/mak"
|
|
|
"tailscale.com/util/multierr"
|
|
|
@@ -178,7 +179,7 @@ type watchSession struct {
|
|
|
// state machine generates events back out to zero or more components.
|
|
|
type LocalBackend struct {
|
|
|
// Elements that are thread-safe or constant after construction.
|
|
|
- ctx context.Context // canceled by Close
|
|
|
+ ctx context.Context // canceled by [LocalBackend.Shutdown]
|
|
|
ctxCancel context.CancelFunc // cancels ctx
|
|
|
logf logger.Logf // general logging
|
|
|
keyLogf logger.Logf // for printing list of peers on change
|
|
|
@@ -231,6 +232,10 @@ type LocalBackend struct {
|
|
|
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
|
|
numClientStatusCalls atomic.Uint32
|
|
|
|
|
|
+ // goTracker accounts for all goroutines started by LocalBacked, primarily
|
|
|
+ // for testing and graceful shutdown purposes.
|
|
|
+ goTracker goroutines.Tracker
|
|
|
+
|
|
|
// The mutex protects the following elements.
|
|
|
mu sync.Mutex
|
|
|
conf *conffile.Config // latest parsed config, or nil if not in declarative mode
|
|
|
@@ -866,7 +871,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
|
|
// TODO(raggi,tailscale/corp#22574): authReconfig should be refactored such that we can call the
|
|
|
// necessary operations here and avoid the need for asynchronous behavior that is racy and hard
|
|
|
// to test here, and do less extra work in these conditions.
|
|
|
- go b.authReconfig()
|
|
|
+ b.goTracker.Go(b.authReconfig)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -879,7 +884,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
|
|
|
want := b.netMap.GetAddresses().Len()
|
|
|
if len(b.peerAPIListeners) < want {
|
|
|
b.logf("linkChange: peerAPIListeners too low; trying again")
|
|
|
- go b.initPeerAPIListener()
|
|
|
+ b.goTracker.Go(b.initPeerAPIListener)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -1004,6 +1009,33 @@ func (b *LocalBackend) Shutdown() {
|
|
|
b.ctxCancel()
|
|
|
b.e.Close()
|
|
|
<-b.e.Done()
|
|
|
+ b.awaitNoGoroutinesInTest()
|
|
|
+}
|
|
|
+
|
|
|
+func (b *LocalBackend) awaitNoGoroutinesInTest() {
|
|
|
+ if !testenv.InTest() {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
|
+ defer cancel()
|
|
|
+
|
|
|
+ ch := make(chan bool, 1)
|
|
|
+ defer b.goTracker.AddDoneCallback(func() { ch <- true })()
|
|
|
+
|
|
|
+ for {
|
|
|
+ n := b.goTracker.RunningGoroutines()
|
|
|
+ if n == 0 {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ select {
|
|
|
+ case <-ctx.Done():
|
|
|
+ // TODO(bradfitz): pass down some TB-like failer interface from
|
|
|
+ // tests, without depending on testing from here?
|
|
|
+ // But this is fine in tests too:
|
|
|
+ panic(fmt.Sprintf("timeout waiting for %d goroutines to stop", n))
|
|
|
+ case <-ch:
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
|
|
|
@@ -2152,7 +2184,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|
|
|
|
|
if b.portpoll != nil {
|
|
|
b.portpollOnce.Do(func() {
|
|
|
- go b.readPoller()
|
|
|
+ b.goTracker.Go(b.readPoller)
|
|
|
})
|
|
|
}
|
|
|
|
|
|
@@ -2366,7 +2398,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
|
|
b.e.SetJailedFilter(filter.NewShieldsUpFilter(localNets, logNets, oldJailedFilter, b.logf))
|
|
|
|
|
|
if b.sshServer != nil {
|
|
|
- go b.sshServer.OnPolicyChange()
|
|
|
+ b.goTracker.Go(b.sshServer.OnPolicyChange)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -2843,7 +2875,7 @@ func (b *LocalBackend) WatchNotificationsAs(ctx context.Context, actor ipnauth.A
|
|
|
// request every 2 seconds.
|
|
|
// TODO(bradfitz): plumb this further and only send a Notify on change.
|
|
|
if mask&ipn.NotifyWatchEngineUpdates != 0 {
|
|
|
- go b.pollRequestEngineStatus(ctx)
|
|
|
+ b.goTracker.Go(func() { b.pollRequestEngineStatus(ctx) })
|
|
|
}
|
|
|
|
|
|
// TODO(marwan-at-work): streaming background logs?
|
|
|
@@ -3850,7 +3882,7 @@ func (b *LocalBackend) editPrefsLockedOnEntry(mp *ipn.MaskedPrefs, unlock unlock
|
|
|
if mp.EggSet {
|
|
|
mp.EggSet = false
|
|
|
b.egg = true
|
|
|
- go b.doSetHostinfoFilterServices()
|
|
|
+ b.goTracker.Go(b.doSetHostinfoFilterServices)
|
|
|
}
|
|
|
p0 := b.pm.CurrentPrefs()
|
|
|
p1 := b.pm.CurrentPrefs().AsStruct()
|
|
|
@@ -3943,7 +3975,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
|
|
|
|
|
if oldp.ShouldSSHBeRunning() && !newp.ShouldSSHBeRunning() {
|
|
|
if b.sshServer != nil {
|
|
|
- go b.sshServer.Shutdown()
|
|
|
+ b.goTracker.Go(b.sshServer.Shutdown)
|
|
|
b.sshServer = nil
|
|
|
}
|
|
|
}
|
|
|
@@ -4285,8 +4317,14 @@ func (b *LocalBackend) authReconfig() {
|
|
|
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.keyExpired, b.logf, version.OS())
|
|
|
// If the current node is an app connector, ensure the app connector machine is started
|
|
|
b.reconfigAppConnectorLocked(nm, prefs)
|
|
|
+ closing := b.shutdownCalled
|
|
|
b.mu.Unlock()
|
|
|
|
|
|
+ if closing {
|
|
|
+ b.logf("[v1] authReconfig: skipping because in shutdown")
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
if blocked {
|
|
|
b.logf("[v1] authReconfig: blocked, skipping.")
|
|
|
return
|
|
|
@@ -4751,7 +4789,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
|
|
b.peerAPIListeners = append(b.peerAPIListeners, pln)
|
|
|
}
|
|
|
|
|
|
- go b.doSetHostinfoFilterServices()
|
|
|
+ b.goTracker.Go(b.doSetHostinfoFilterServices)
|
|
|
}
|
|
|
|
|
|
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
|
|
@@ -5020,7 +5058,7 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
|
|
|
// can be shut down if we transition away from Running.
|
|
|
if b.captiveCancel == nil {
|
|
|
b.captiveCtx, b.captiveCancel = context.WithCancel(b.ctx)
|
|
|
- go b.checkCaptivePortalLoop(b.captiveCtx)
|
|
|
+ b.goTracker.Go(func() { b.checkCaptivePortalLoop(b.captiveCtx) })
|
|
|
}
|
|
|
} else if oldState == ipn.Running {
|
|
|
// Transitioning away from running.
|
|
|
@@ -5272,7 +5310,7 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
|
|
b.statusLock.Lock()
|
|
|
defer b.statusLock.Unlock()
|
|
|
|
|
|
- go b.e.RequestStatus()
|
|
|
+ b.goTracker.Go(b.e.RequestStatus)
|
|
|
b.logf("requestEngineStatusAndWait: waiting...")
|
|
|
b.statusChanged.Wait() // temporarily releases lock while waiting
|
|
|
b.logf("requestEngineStatusAndWait: got status update.")
|
|
|
@@ -5383,7 +5421,7 @@ func (b *LocalBackend) setWebClientAtomicBoolLocked(nm *netmap.NetworkMap) {
|
|
|
shouldRun := !nm.HasCap(tailcfg.NodeAttrDisableWebClient)
|
|
|
wasRunning := b.webClientAtomicBool.Swap(shouldRun)
|
|
|
if wasRunning && !shouldRun {
|
|
|
- go b.webClientShutdown() // stop web client
|
|
|
+ b.goTracker.Go(b.webClientShutdown) // stop web client
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -5900,7 +5938,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
|
|
if wire := b.wantIngressLocked(); b.hostinfo != nil && b.hostinfo.WireIngress != wire {
|
|
|
b.logf("Hostinfo.WireIngress changed to %v", wire)
|
|
|
b.hostinfo.WireIngress = wire
|
|
|
- go b.doSetHostinfoFilterServices()
|
|
|
+ b.goTracker.Go(b.doSetHostinfoFilterServices)
|
|
|
}
|
|
|
|
|
|
b.setTCPPortsIntercepted(handlePorts)
|