Summary
ScanAndBroadcast() in the envd port scanner uses a bare time.Sleep to pace scans. Because Destroy() closes the scanExit channel but the goroutine checks it before sleeping (not after), a Destroy() call that arrives during the sleep is not noticed until the sleep completes.
Location: packages/envd/internal/port/scan.go:44
func (s *Scanner) ScanAndBroadcast() {
for {
processes, _ := net.Connections("tcp")
for _, sub := range s.subs.Items() {
sub.Signal(processes)
}
select {
case <-s.scanExit:
return
default:
time.Sleep(s.period) // ← shutdown waits up to `period` here
}
}
}
Root cause
The select only tests scanExit at the top of each iteration, before the sleep. Once time.Sleep starts, a concurrent Destroy() closes scanExit but the goroutine doesn't wake up — it sleeps the full period before looping back and seeing the closed channel.
If period is long (the default is not specified here, but envd calls NewScanner(portScannerInterval) — if that's on the order of seconds) shutdown is delayed unnecessarily.
Proposed fix
Replace the time.Sleep with a time.After inside the select so Destroy() can interrupt the sleep immediately:
func (s *Scanner) ScanAndBroadcast() {
for {
processes, _ := net.Connections("tcp")
for _, sub := range s.subs.Items() {
sub.Signal(processes)
}
select {
case <-s.scanExit:
return
case <-time.After(s.period):
}
}
}
Or use a time.Ticker (more efficient — avoids a new timer allocation per iteration):
func (s *Scanner) ScanAndBroadcast() {
ticker := time.NewTicker(s.period)
defer ticker.Stop()
for {
processes, _ := net.Connections("tcp")
for _, sub := range s.subs.Items() {
sub.Signal(processes)
}
select {
case <-s.scanExit:
return
case <-ticker.C:
}
}
}
Impact
With the current code, every envd shutdown is delayed by up to portScannerInterval waiting for the port scanner goroutine to wake from its sleep and notice the exit signal. The fix makes shutdown instantaneous for this goroutine.
Summary
ScanAndBroadcast()in the envd port scanner uses a baretime.Sleepto pace scans. BecauseDestroy()closes thescanExitchannel but the goroutine checks it before sleeping (not after), aDestroy()call that arrives during the sleep is not noticed until the sleep completes.Location:
packages/envd/internal/port/scan.go:44Root cause
The
selectonly testsscanExitat the top of each iteration, before the sleep. Oncetime.Sleepstarts, a concurrentDestroy()closesscanExitbut the goroutine doesn't wake up — it sleeps the fullperiodbefore looping back and seeing the closed channel.If
periodis long (the default is not specified here, but envd callsNewScanner(portScannerInterval)— if that's on the order of seconds) shutdown is delayed unnecessarily.Proposed fix
Replace the
time.Sleepwith atime.Afterinside the select soDestroy()can interrupt the sleep immediately:Or use a
time.Ticker(more efficient — avoids a new timer allocation per iteration):Impact
With the current code, every envd shutdown is delayed by up to
portScannerIntervalwaiting for the port scanner goroutine to wake from its sleep and notice the exit signal. The fix makes shutdown instantaneous for this goroutine.