fix(whatsapp_native): fix goroutine and resource leak in Start/Stop lifecycle

- Move runCtx/runCancel creation before event handler registration and
  QR loop so Stop() can cancel at any point during startup
- Replace blocking QR event loop in Start() with a background goroutine
  that selects on runCtx.Done(), preventing Start() from hanging
  indefinitely when waiting for QR scan
- Track all background goroutines (QR handler, reconnect) with
  sync.WaitGroup; Stop() waits for them to finish before releasing
  client/container resources
- Cancel runCtx on error paths in Start() to avoid leaked contexts

Fixes resource leak introduced in #655.
This commit is contained in:
Hoshina
2026-02-28 01:39:06 +08:00
parent cdbc9c4bd6
commit c7d75a18f8
+46 -13
View File
@@ -56,6 +56,7 @@ type WhatsAppNativeChannel struct {
runCancel context.CancelFunc
reconnectMu sync.Mutex
reconnecting bool
wg sync.WaitGroup // tracks background goroutines (QR handler, reconnect)
}
// NewWhatsAppNativeChannel creates a WhatsApp channel that uses whatsmeow for connection.
@@ -112,6 +113,12 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
}
client := whatsmeow.NewClient(deviceStore, waLogger)
// Create runCtx/runCancel BEFORE registering event handler and starting
// goroutines so that Stop() can cancel them at any time, including during
// the QR-login flow.
c.runCtx, c.runCancel = context.WithCancel(ctx)
client.AddEventHandler(c.eventHandler)
c.mu.Lock()
@@ -122,33 +129,50 @@ func (c *WhatsAppNativeChannel) Start(ctx context.Context) error {
if client.Store.ID == nil {
qrChan, err := client.GetQRChannel(ctx)
if err != nil {
c.runCancel()
_ = container.Close()
return fmt.Errorf("get QR channel: %w", err)
}
if err := client.Connect(); err != nil {
c.runCancel()
_ = container.Close()
return fmt.Errorf("connect: %w", err)
}
for evt := range qrChan {
if evt.Event == "code" {
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
HalfBlocks: true,
})
} else {
logger.InfoCF("whatsapp", "WhatsApp login event", map[string]any{"event": evt.Event})
// Handle QR events in a background goroutine so Start() returns
// promptly. The goroutine is tracked via c.wg and respects
// c.runCtx for cancellation.
c.wg.Add(1)
go func() {
defer c.wg.Done()
for {
select {
case <-c.runCtx.Done():
return
case evt, ok := <-qrChan:
if !ok {
return
}
if evt.Event == "code" {
logger.InfoCF("whatsapp", "Scan this QR code with WhatsApp (Linked Devices):", nil)
qrterminal.GenerateWithConfig(evt.Code, qrterminal.Config{
Level: qrterminal.L,
Writer: os.Stdout,
HalfBlocks: true,
})
} else {
logger.InfoCF("whatsapp", "WhatsApp login event", map[string]any{"event": evt.Event})
}
}
}
}
}()
} else {
if err := client.Connect(); err != nil {
c.runCancel()
_ = container.Close()
return fmt.Errorf("connect: %w", err)
}
}
c.runCtx, c.runCancel = context.WithCancel(ctx)
c.SetRunning(true)
logger.InfoC("whatsapp", "WhatsApp native channel connected")
return nil
@@ -159,6 +183,11 @@ func (c *WhatsAppNativeChannel) Stop(ctx context.Context) error {
if c.runCancel != nil {
c.runCancel()
}
// Wait for background goroutines (QR handler, reconnect) to finish so
// they don't reference the client/container after cleanup.
c.wg.Wait()
c.mu.Lock()
client := c.client
container := c.container
@@ -189,7 +218,11 @@ func (c *WhatsAppNativeChannel) eventHandler(evt any) {
}
c.reconnecting = true
c.reconnectMu.Unlock()
go c.reconnectWithBackoff()
c.wg.Add(1)
go func() {
defer c.wg.Done()
c.reconnectWithBackoff()
}()
}
}