diff --git a/pkg/call/service/call_service.go b/pkg/call/service/call_service.go index 3c14a948..29148311 100644 --- a/pkg/call/service/call_service.go +++ b/pkg/call/service/call_service.go @@ -29,7 +29,9 @@ type RejectCallStruct struct { } func (c *callService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -43,7 +45,9 @@ func (c *callService) ensureClientConnected(instanceId string) (*whatsmeow.Clien c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/chat/service/chat_service.go b/pkg/chat/service/chat_service.go index c4eda9d7..2b5309ca 100644 --- a/pkg/chat/service/chat_service.go +++ b/pkg/chat/service/chat_service.go @@ -40,7 +40,9 @@ type HistorySyncRequestStruct struct { } func (c *chatService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -54,7 +56,9 @@ func (c *chatService) ensureClientConnected(instanceId string) (*whatsmeow.Clien c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/community/service/community_service.go b/pkg/community/service/community_service.go index aded6109..64f14950 100644 --- a/pkg/community/service/community_service.go +++ b/pkg/community/service/community_service.go @@ -36,7 +36,9 @@ type AddParticipantStruct struct { } func (c *communityService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -50,7 +52,9 @@ func (c *communityService) ensureClientConnected(instanceId string) (*whatsmeow. c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = c.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() c.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/group/service/group_service.go b/pkg/group/service/group_service.go index b99e10bb..7bc611c4 100644 --- a/pkg/group/service/group_service.go +++ b/pkg/group/service/group_service.go @@ -117,7 +117,9 @@ type UpdateGroupRequestParticipantsStruct struct { } func (g *groupService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := g.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -131,7 +133,9 @@ func (g *groupService) ensureClientConnected(instanceId string) (*whatsmeow.Clie g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = g.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() g.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/instance/service/instance_service.go b/pkg/instance/service/instance_service.go index fa43c946..114d264e 100644 --- a/pkg/instance/service/instance_service.go +++ b/pkg/instance/service/instance_service.go @@ -124,7 +124,9 @@ type ForceReconnectStruct struct { func (i *instances) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { logger := i.loggerWrapper.GetLogger(instanceId) + whatsmeow_service.ClientMapsMu.RLock() client := i.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() logger.LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -138,7 +140,9 @@ func (i *instances) ensureClientConnected(instanceId string) (*whatsmeow.Client, logger.LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = i.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() logger.LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, @@ -242,7 +246,9 @@ func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instanc } // Verifica se a instância já está rodando + whatsmeow_service.ClientMapsMu.RLock() isInstanceRunning := i.clientPointer[instance.Id] != nil + whatsmeow_service.ClientMapsMu.RUnlock() // Sincroniza as configurações na instância em execução (se já estiver conectada) err = i.whatsmeowService.UpdateInstanceSettings(instance.Id) @@ -258,7 +264,9 @@ func (i instances) Connect(data *ConnectStruct, instance *instance_model.Instanc if !isInstanceRunning { i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Starting new client instance", instance.Id) + whatsmeow_service.ClientMapsMu.Lock() i.killChannel[instance.Id] = make(chan bool) + whatsmeow_service.ClientMapsMu.Unlock() clientData := &whatsmeow_service.ClientData{ Instance: instance, @@ -317,7 +325,10 @@ func (i instances) Disconnect(instance *instance_model.Instance) (*instance_mode if client.IsConnected() { if client.IsLoggedIn() { i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id) - i.killChannel[instance.Id] <- true + whatsmeow_service.ClientMapsMu.RLock() + killChan := i.killChannel[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + killChan <- true instance.Events = "" @@ -352,13 +363,18 @@ func (i instances) Logout(instance *instance_model.Instance) (*instance_model.In return instance, err } + whatsmeow_service.ClientMapsMu.RLock() + logoutKillChan := i.killChannel[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() select { - case i.killChannel[instance.Id] <- true: + case logoutKillChan <- true: case <-time.After(5 * time.Second): } + whatsmeow_service.ClientMapsMu.Lock() delete(i.clientPointer, instance.Id) delete(i.killChannel, instance.Id) + whatsmeow_service.ClientMapsMu.Unlock() i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Logout successful", instance.Id) return instance, nil @@ -367,13 +383,18 @@ func (i instances) Logout(instance *instance_model.Instance) (*instance_model.In if client.IsConnected() { client.Disconnect() + whatsmeow_service.ClientMapsMu.RLock() + disconnectKillChan := i.killChannel[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() select { - case i.killChannel[instance.Id] <- true: + case disconnectKillChan <- true: case <-time.After(5 * time.Second): } + whatsmeow_service.ClientMapsMu.Lock() delete(i.clientPointer, instance.Id) delete(i.killChannel, instance.Id) + whatsmeow_service.ClientMapsMu.Unlock() i.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Disconnection successful", instance.Id) return instance, nil @@ -384,7 +405,9 @@ func (i instances) Logout(instance *instance_model.Instance) (*instance_model.In } func (i instances) Status(instance *instance_model.Instance) (*StatusStruct, error) { + whatsmeow_service.ClientMapsMu.RLock() client := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() if client == nil { return &StatusStruct{ @@ -413,7 +436,9 @@ func (i instances) Status(instance *instance_model.Instance) (*StatusStruct, err func (i instances) GetQr(instance *instance_model.Instance) (*QrcodeStruct, error) { logger := i.loggerWrapper.GetLogger(instance.Id) + whatsmeow_service.ClientMapsMu.RLock() client := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() // Se não há cliente ou o cliente está logado, precisamos iniciar um novo cliente if client == nil || client.IsLoggedIn() { @@ -435,7 +460,9 @@ func (i instances) GetQr(instance *instance_model.Instance) (*QrcodeStruct, erro time.Sleep(3 * time.Second) // Verificar novamente se há cliente + whatsmeow_service.ClientMapsMu.RLock() client = i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() if client != nil && client.IsLoggedIn() { return nil, fmt.Errorf("session already logged in") } @@ -511,7 +538,9 @@ func buildPasskeyOpenURL(token string) string { func (i instances) Pair(data *PairStruct, instance *instance_model.Instance) (*PairReturnStruct, error) { logger := i.loggerWrapper.GetLogger(instance.Id) + whatsmeow_service.ClientMapsMu.RLock() client := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() if client == nil || !client.IsConnected() { if client != nil && client.IsLoggedIn() { @@ -525,7 +554,9 @@ func (i instances) Pair(data *PairStruct, instance *instance_model.Instance) (*P // Wait for the WA websocket connection and initial QR generation to establish. // PairPhone must be called after the QR event is received per whatsmeow docs. time.Sleep(3 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() if client == nil { return nil, fmt.Errorf("failed to initialize client for pairing") } @@ -551,7 +582,10 @@ func (i instances) GetAll() ([]*instance_model.Instance, error) { } for _, instance := range instances { - if client := i.clientPointer[instance.Id]; client != nil { + whatsmeow_service.ClientMapsMu.RLock() + client := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + if client != nil { instance.Connected = client.IsLoggedIn() } else { instance.Connected = false @@ -570,7 +604,10 @@ func (i instances) Info(instanceId string) (*instance_model.Instance, error) { } // Atualiza o status connected com base no estado real do cliente - if client := i.clientPointer[instance.Id]; client != nil { + whatsmeow_service.ClientMapsMu.RLock() + client := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + if client != nil { instance.Connected = client.IsLoggedIn() } else { instance.Connected = false @@ -587,19 +624,31 @@ func (i instances) Delete(id string) error { return err } - if i.clientPointer[instance.Id] != nil && i.clientPointer[instance.Id].IsConnected() { - if i.clientPointer[instance.Id].IsLoggedIn() { - i.clientPointer[instance.Id].Logout(context.Background()) + whatsmeow_service.ClientMapsMu.RLock() + deleteClient := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + + if deleteClient != nil && deleteClient.IsConnected() { + if deleteClient.IsLoggedIn() { + deleteClient.Logout(context.Background()) } - i.clientPointer[instance.Id].Disconnect() + deleteClient.Disconnect() } - // Limpar todos os recursos da instância antes de deletar + // Limpar todos os recursos da instância antes de deletar. Read-then-delete + // on killChannel happens under the same Lock as the clientPointer delete so + // a concurrent Delete()/cleanup can't double-close the channel. + whatsmeow_service.ClientMapsMu.Lock() delete(i.clientPointer, instance.Id) - if i.killChannel[instance.Id] != nil { - close(i.killChannel[instance.Id]) + deleteKillChan := i.killChannel[instance.Id] + if deleteKillChan != nil { delete(i.killChannel, instance.Id) } + whatsmeow_service.ClientMapsMu.Unlock() + + if deleteKillChan != nil { + close(deleteKillChan) + } // Limpar cache via whatsmeow service err = i.whatsmeowService.ClearInstanceCache(instance.Id, instance.Token) @@ -697,7 +746,10 @@ func (i instances) RemoveProxy(id string) error { } func (i instances) ForceReconnect(instanceId string, number string) error { - if i.clientPointer[instanceId].IsConnected() && i.clientPointer[instanceId].IsLoggedIn() { + whatsmeow_service.ClientMapsMu.RLock() + forceReconnectClient := i.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() + if forceReconnectClient.IsConnected() && forceReconnectClient.IsLoggedIn() { return fmt.Errorf("client already connected") } @@ -713,7 +765,9 @@ func (i instances) ForceReconnect(instanceId string, number string) error { subscribedEvents := strings.Split(instance.Events, ",") + whatsmeow_service.ClientMapsMu.Lock() i.killChannel[instance.Id] = make(chan bool) + whatsmeow_service.ClientMapsMu.Unlock() clientData := &whatsmeow_service.ClientData{ Instance: instance, @@ -735,29 +789,41 @@ func (i instances) ForceReconnect(instanceId string, number string) error { } } - if i.clientPointer[instance.Id] != nil { - client := i.clientPointer[instance.Id] - client.Disconnect() + whatsmeow_service.ClientMapsMu.RLock() + existingForceReconnectClient := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + if existingForceReconnectClient != nil { + existingForceReconnectClient.Disconnect() + + whatsmeow_service.ClientMapsMu.RLock() + forceReconnectKillChan := i.killChannel[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() select { - case i.killChannel[instance.Id] <- true: + case forceReconnectKillChan <- true: case <-time.After(5 * time.Second): } + whatsmeow_service.ClientMapsMu.Lock() delete(i.clientPointer, instance.Id) delete(i.killChannel, instance.Id) + whatsmeow_service.ClientMapsMu.Unlock() } go i.whatsmeowService.StartClient(clientData) time.Sleep(2 * time.Second) - if i.clientPointer[instance.Id] != nil { - if !i.clientPointer[instance.Id].IsConnected() { + whatsmeow_service.ClientMapsMu.RLock() + newForceReconnectClient := i.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + + if newForceReconnectClient != nil { + if !newForceReconnectClient.IsConnected() { return fmt.Errorf("failed to connect") } - if !i.clientPointer[instance.Id].IsLoggedIn() { + if !newForceReconnectClient.IsLoggedIn() { return fmt.Errorf("failed to login") } } else { diff --git a/pkg/label/service/label_service.go b/pkg/label/service/label_service.go index b0a9070d..5afd35a6 100644 --- a/pkg/label/service/label_service.go +++ b/pkg/label/service/label_service.go @@ -50,7 +50,9 @@ type EditLabelStruct struct { } func (l *labelService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := l.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -64,7 +66,9 @@ func (l *labelService) ensureClientConnected(instanceId string) (*whatsmeow.Clie l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = l.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() l.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/message/service/message_service.go b/pkg/message/service/message_service.go index f8f13920..79cadbed 100644 --- a/pkg/message/service/message_service.go +++ b/pkg/message/service/message_service.go @@ -95,7 +95,9 @@ type MessageSendStruct struct { } func (m *messageService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := m.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -109,7 +111,9 @@ func (m *messageService) ensureClientConnected(instanceId string) (*whatsmeow.Cl m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = m.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() m.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/newsletter/service/newsletter_service.go b/pkg/newsletter/service/newsletter_service.go index 6a8f6779..23423131 100644 --- a/pkg/newsletter/service/newsletter_service.go +++ b/pkg/newsletter/service/newsletter_service.go @@ -47,7 +47,9 @@ type GetNewsletterMessagesStruct struct { } func (n *newsletterService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := n.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -61,7 +63,9 @@ func (n *newsletterService) ensureClientConnected(instanceId string) (*whatsmeow n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = n.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() n.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/sendMessage/service/send_service.go b/pkg/sendMessage/service/send_service.go index b8850089..8f45d90f 100644 --- a/pkg/sendMessage/service/send_service.go +++ b/pkg/sendMessage/service/send_service.go @@ -380,7 +380,9 @@ type MessageSendStruct struct { } func (s *sendService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := s.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -394,7 +396,9 @@ func (s *sendService) ensureClientConnected(instanceId string) (*whatsmeow.Clien s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = s.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() s.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, @@ -2358,9 +2362,17 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. s.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Recipient validated: %s (Server: %s)", instance.Id, recipient.String(), recipient.Server) + // Fetch the client once for this whole send: the map read itself is + // guarded, but a single send is expected to run against one client, so we + // don't want to re-look-up the map (and thus potentially race a concurrent + // reconnect mid-send) on every one of the calls below. + whatsmeow_service.ClientMapsMu.RLock() + sendMessageClient := s.clientPointer[instance.Id] + whatsmeow_service.ClientMapsMu.RUnlock() + var message string if data.Id == "" { - message = s.clientPointer[instance.Id].GenerateMessageID() + message = sendMessageClient.GenerateMessageID() } else { message = data.Id } @@ -2371,14 +2383,14 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. media = "audio" } - err := s.clientPointer[instance.Id].SendChatPresence(context.Background(), recipient, types.ChatPresence("composing"), types.ChatPresenceMedia(media)) + err := sendMessageClient.SendChatPresence(context.Background(), recipient, types.ChatPresence("composing"), types.ChatPresenceMedia(media)) if err != nil { return nil, err } time.Sleep(time.Duration(data.Delay) * time.Millisecond) - err = s.clientPointer[instance.Id].SendChatPresence(context.Background(), recipient, types.ChatPresence("paused"), types.ChatPresenceMedia(media)) + err = sendMessageClient.SendChatPresence(context.Background(), recipient, types.ChatPresence("paused"), types.ChatPresenceMedia(media)) if err != nil { return nil, err } @@ -2631,7 +2643,7 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. // Only try to get participants for actual groups, not newsletters if isGroup && !isNewsletter { if data.MentionAll { - groupInfo, err := s.clientPointer[instance.Id].GetGroupInfo(context.Background(), recipient) + groupInfo, err := sendMessageClient.GetGroupInfo(context.Background(), recipient) if err != nil { return nil, err } @@ -2770,7 +2782,7 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. sendExtra.AdditionalNodes = data.AdditionalNodes } - response, err := s.clientPointer[instance.Id].SendMessage(context.Background(), recipient, msg, sendExtra) + response, err := sendMessageClient.SendMessage(context.Background(), recipient, msg, sendExtra) if err != nil { s.loggerWrapper.GetLogger(instance.Id).LogError("[%s] Error sending message: %v", instance.Id, err) return nil, err @@ -2781,7 +2793,7 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. messageInfo := types.MessageInfo{ MessageSource: types.MessageSource{ Chat: recipient, - Sender: *s.clientPointer[instance.Id].Store.ID, + Sender: *sendMessageClient.Store.ID, IsFromMe: true, IsGroup: isGroup, }, @@ -2835,15 +2847,15 @@ func (s *sendService) SendMessage(instance *instance_model.Instance, msg *waE2E. sticker := msg.GetStickerMessage() if img != nil { - data, err = s.clientPointer[instance.Id].Download(context.Background(), img) + data, err = sendMessageClient.Download(context.Background(), img) } else if audio != nil { - data, err = s.clientPointer[instance.Id].Download(context.Background(), audio) + data, err = sendMessageClient.Download(context.Background(), audio) } else if document != nil { - data, err = s.clientPointer[instance.Id].Download(context.Background(), document) + data, err = sendMessageClient.Download(context.Background(), document) } else if video != nil { - data, err = s.clientPointer[instance.Id].Download(context.Background(), video) + data, err = sendMessageClient.Download(context.Background(), video) } else if sticker != nil { - data, err = s.clientPointer[instance.Id].Download(context.Background(), sticker) + data, err = sendMessageClient.Download(context.Background(), sticker) webpReader := bytes.NewReader(data) img, err := webp.Decode(webpReader) diff --git a/pkg/user/service/user_service.go b/pkg/user/service/user_service.go index f3011606..3d6f14b9 100644 --- a/pkg/user/service/user_service.go +++ b/pkg/user/service/user_service.go @@ -109,7 +109,9 @@ type PrivacyStruct struct { } func (u *userService) ensureClientConnected(instanceId string) (*whatsmeow.Client, error) { + whatsmeow_service.ClientMapsMu.RLock() client := u.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking client connection status - Client exists: %v", instanceId, client != nil) if client == nil { @@ -123,7 +125,9 @@ func (u *userService) ensureClientConnected(instanceId string) (*whatsmeow.Clien u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Instance started, waiting 2 seconds...", instanceId) time.Sleep(2 * time.Second) + whatsmeow_service.ClientMapsMu.RLock() client = u.clientPointer[instanceId] + whatsmeow_service.ClientMapsMu.RUnlock() u.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Checking new client - Exists: %v, Connected: %v", instanceId, client != nil, diff --git a/pkg/whatsmeow/service/client_maps.go b/pkg/whatsmeow/service/client_maps.go new file mode 100644 index 00000000..38618a31 --- /dev/null +++ b/pkg/whatsmeow/service/client_maps.go @@ -0,0 +1,31 @@ +package whatsmeow_service + +import "sync" + +// ClientMapsMu guards the three instance-keyed maps created once in +// cmd/evolution-go/main.go — killChannel (map[string]chan bool), clientPointer +// (map[string]*whatsmeow.Client) and myClientPointer (map[string]*MyClient) — +// and shared by reference across every service package in this module +// (whatsmeow, instance, sendMessage, chat, call, community, group, label, +// message, newsletter, user) as well as every *MyClient value, which keeps its +// own copies of the same three map references. +// +// Because all of those packages read and write the very same underlying maps +// concurrently (HTTP handlers reading a client to act on it while +// ReconnectClient/StartClient add or remove entries from a background +// goroutine), any indexed access — assignment, delete, membership check, +// range, or len — MUST hold this mutex. Every read (`v := m[k]`, `v, ok := +// m[k]`, `range m`, `len(m)`) takes RLock/RUnlock; every write (`m[k] = v`, +// `delete(m, k)`) takes Lock/Unlock. Without this, Go's runtime fatals the +// whole process on a concurrent map read/write or concurrent map writes — it +// is not a panic that can be recovered from. +// +// Rule of thumb: never hold the lock across a long or blocking call — +// Disconnect/Connect/Logout, StartInstance/ReconnectClient/StartClient, a send +// on a kill channel, or any network I/O. Copy the map value you need into a +// local variable while holding the lock, release the lock, then operate on +// the local copy. This also avoids deadlocks: several of the guarded +// functions call into each other (ReconnectClient -> StartInstance -> +// StartClient), and none of them may still be holding ClientMapsMu when they +// do. +var ClientMapsMu sync.RWMutex diff --git a/pkg/whatsmeow/service/whatsmeow.go b/pkg/whatsmeow/service/whatsmeow.go index 366f0edb..960e89c2 100644 --- a/pkg/whatsmeow/service/whatsmeow.go +++ b/pkg/whatsmeow/service/whatsmeow.go @@ -174,8 +174,16 @@ type ProxyConfig struct { func (w whatsmeowService) ReconnectClient(instanceId string) error { w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Starting reconnection process - simulating restart", instanceId) - // Passo 1: Limpar conexão existente se houver - if client, exists := w.clientPointer[instanceId]; exists { + // Passo 1: Limpar conexão existente se houver. + // Copy the client/mycli references out under RLock, then release before + // doing anything network-related (IsConnected/Disconnect/RemoveEventHandler + // can block) — see ClientMapsMu doc. + ClientMapsMu.RLock() + client, exists := w.clientPointer[instanceId] + mycli, hasMycli := w.myClientPointer[instanceId] + ClientMapsMu.RUnlock() + + if exists { w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Disconnecting existing client", instanceId) // Desconectar o cliente WebSocket @@ -185,7 +193,7 @@ func (w whatsmeowService) ReconnectClient(instanceId string) error { } // Remover event handler se existir - if mycli, ok := w.myClientPointer[instanceId]; ok { + if hasMycli { if mycli.eventHandlerID != 0 { client.RemoveEventHandler(mycli.eventHandlerID) w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Event handler removed", instanceId) @@ -196,8 +204,13 @@ func (w whatsmeowService) ReconnectClient(instanceId string) error { // Passo 2: Limpar todos os recursos da instância w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Cleaning up resources", instanceId) - // Enviar sinal de kill se o canal existir - if killChan, exists := w.killChannel[instanceId]; exists { + // Enviar sinal de kill se o canal existir. Copy the channel under RLock and + // send/select on the local copy only after releasing the lock. + ClientMapsMu.RLock() + killChan, hasKillChan := w.killChannel[instanceId] + ClientMapsMu.RUnlock() + + if hasKillChan { select { case killChan <- true: w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Kill signal sent", instanceId) @@ -206,10 +219,12 @@ func (w whatsmeowService) ReconnectClient(instanceId string) error { } } - // Remover das estruturas + // Remover das estruturas — the three deletes happen together under a single Lock. + ClientMapsMu.Lock() delete(w.clientPointer, instanceId) delete(w.myClientPointer, instanceId) delete(w.killChannel, instanceId) + ClientMapsMu.Unlock() // Limpar cache de userInfo para esta instância if instance, err := w.instanceRepository.GetInstanceByID(instanceId); err == nil { @@ -308,8 +323,12 @@ func (w whatsmeowService) StartClient(cd *ClientData) { var deviceStore *store.Device var err error - if w.clientPointer[cd.Instance.Id] != nil { - if w.clientPointer[cd.Instance.Id].IsConnected() { + ClientMapsMu.RLock() + existingClient := w.clientPointer[cd.Instance.Id] + ClientMapsMu.RUnlock() + + if existingClient != nil { + if existingClient.IsConnected() { return } } @@ -412,7 +431,9 @@ func (w whatsmeowService) StartClient(cd *ClientData) { clientLog := waLog.Stdout("Client", minLevel, true) client := whatsmeow.NewClient(deviceStore, clientLog) + ClientMapsMu.Lock() w.clientPointer[cd.Instance.Id] = client + ClientMapsMu.Unlock() if cd.IsProxy { var proxyConfig ProxyConfig @@ -500,7 +521,9 @@ func (w whatsmeowService) StartClient(cd *ClientData) { mycli.eventHandlerID = mycli.WAClient.AddEventHandler(mycli.myEventHandler) // Armazena o MyClient no map para permitir atualizações posteriores + ClientMapsMu.Lock() w.myClientPointer[cd.Instance.Id] = mycli + ClientMapsMu.Unlock() if client.Store.ID != nil { w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("[%s] Already logged in with JID: %s", cd.Instance.Id, client.Store.ID.String()) @@ -573,13 +596,24 @@ func (w whatsmeowService) StartClient(cd *ClientData) { // Removed auto-reconnect logic to prevent infinite loops for { + // Re-read the kill channel from the shared map on every iteration (it may + // be replaced by a concurrent ReconnectClient/StartInstance for this same + // instance): copy it under RLock, release, then select on the local copy. + // A missing entry yields a nil channel, which never fires — same as the + // original unguarded map read. + ClientMapsMu.RLock() + killChan := w.killChannel[cd.Instance.Id] + ClientMapsMu.RUnlock() + select { - case <-w.killChannel[cd.Instance.Id]: + case <-killChan: w.loggerWrapper.GetLogger(cd.Instance.Id).LogInfo("Received kill signal for user '%s'", cd.Instance.Id) client.Disconnect() + ClientMapsMu.Lock() delete(w.clientPointer, cd.Instance.Id) delete(w.myClientPointer, cd.Instance.Id) + ClientMapsMu.Unlock() // Limpar cache de userInfo para esta instância w.userInfoCache.Delete(cd.Instance.Token) @@ -639,6 +673,12 @@ func schedulePresenceUpdates(mycli *MyClient) { defer ticker.Stop() for { + // Copy the kill channel out of the shared map under RLock before + // entering select — see ClientMapsMu doc. + ClientMapsMu.RLock() + killChan := mycli.killChannel[mycli.userID] + ClientMapsMu.RUnlock() + select { case <-ticker.C: // Verificar se a instância ainda existe @@ -654,7 +694,7 @@ func schedulePresenceUpdates(mycli *MyClient) { randomInterval := time.Duration(1+rand.Intn(3)) * time.Hour ticker = time.NewTicker(randomInterval) - case <-mycli.killChannel[mycli.userID]: + case <-killChan: mycli.loggerWrapper.GetLogger(mycli.userID).LogInfo("[%s] Received kill signal, stopping presence updates", mycli.userID) return // Encerra a goroutine quando receber sinal de kill } @@ -801,13 +841,14 @@ func (mycli *MyClient) handleQRCodes(codes []string) { // teardownQR clears the QR state and emits a QRTimeout event, then signals the // kill channel so StartClient's select loop performs the actual disconnect and // map cleanup. IMPORTANT: this method must NOT delete from the shared -// clientPointer/myClientPointer/killChannel maps itself — those are unsynchronized -// service-wide maps and this runs in the handleQRCodes goroutine; doing the -// delete()s here (concurrent with other instances' goroutines and the whatsmeow -// dispatch) risks a `fatal error: concurrent map writes`. The kill-channel send -// is blocking (like the original GetQRChannel timeout branch) so the signal is -// never dropped and the socket can't be orphaned. Cleanup happens in the -// StartClient goroutine, the single writer of those maps for this instance. +// clientPointer/myClientPointer/killChannel maps itself. Reads/writes on those +// maps are guarded by ClientMapsMu (see client_maps.go), but this still runs +// concurrently with other instances' goroutines and the whatsmeow dispatch — +// deleting here as well as in StartClient would just move the race to "who +// deletes twice", not remove it. The kill-channel send is blocking (like the +// original GetQRChannel timeout branch) so the signal is never dropped and the +// socket can't be orphaned. Cleanup happens in the StartClient goroutine, the +// single writer of those maps for this instance. // If reason is non-empty it is included in the QRTimeout payload (max-count path). func (mycli *MyClient) teardownQR(reason string, forceLogout bool) { instanceID := mycli.userID @@ -848,7 +889,10 @@ func (mycli *MyClient) teardownQR(reason string, forceLogout bool) { // maps (it is the single writer for this instance). Blocking send mirrors // the original timeout branch so the signal is never dropped. mycli.loggerWrapper.GetLogger(instanceID).LogWarn("[%s] QR timeout — signaling kill channel", instanceID) - if killChan, exists := mycli.killChannel[instanceID]; exists { + ClientMapsMu.RLock() + killChan, exists := mycli.killChannel[instanceID] + ClientMapsMu.RUnlock() + if exists { killChan <- true } } @@ -1277,7 +1321,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { fmt.Printf("[POLL DEBUG] ✅ mycli.WAClient is initialized: %s\n", mycli.WAClient.Store.ID) } - decrypted, err := mycli.clientPointer[mycli.userID].DecryptPollVote(context.Background(), evt) + ClientMapsMu.RLock() + pollVoteClient := mycli.clientPointer[mycli.userID] + ClientMapsMu.RUnlock() + decrypted, err := pollVoteClient.DecryptPollVote(context.Background(), evt) if err != nil { mycli.loggerWrapper.GetLogger(mycli.userID).LogError("[%s] Failed to decrypt vote: %v", mycli.userID, err) } else { @@ -1897,7 +1944,10 @@ func (mycli *MyClient) myEventHandler(rawEvt interface{}) { } // Agora mata o canal DEPOIS de enviar o evento - mycli.killChannel[mycli.userID] <- true + ClientMapsMu.RLock() + loggedOutKillChan := mycli.killChannel[mycli.userID] + ClientMapsMu.RUnlock() + loggedOutKillChan <- true case *events.ChatPresence: doWebhook = true postMap["event"] = "ChatPresence" @@ -2382,7 +2432,9 @@ func (w whatsmeowService) StartInstance(instanceId string) error { } } + ClientMapsMu.Lock() w.killChannel[instance.Id] = make(chan bool) + ClientMapsMu.Unlock() clientData := &ClientData{ Instance: instance, @@ -2683,7 +2735,9 @@ func (w whatsmeowService) UpdateInstanceSettings(instanceId string) error { } // Verifica se o MyClient existe + ClientMapsMu.RLock() myClient, exists := w.myClientPointer[instanceId] + ClientMapsMu.RUnlock() if !exists { w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] MyClient not found in runtime, instance may not be connected", instanceId) return fmt.Errorf("instance %s not found in runtime", instanceId) @@ -2742,7 +2796,9 @@ func (w whatsmeowService) UpdateInstanceAdvancedSettings(instanceId string) erro } // Verifica se o MyClient existe + ClientMapsMu.RLock() myClient, exists := w.myClientPointer[instanceId] + ClientMapsMu.RUnlock() if !exists { w.loggerWrapper.GetLogger(instanceId).LogWarn("[%s] MyClient not found in runtime, instance may not be connected", instanceId) return fmt.Errorf("instance %s not found in runtime", instanceId) @@ -2761,20 +2817,32 @@ func (w whatsmeowService) ClearInstanceCache(instanceId string, token string) er // Limpar userInfoCache w.userInfoCache.Delete(token) - // Limpar myClientPointer se existir - if _, exists := w.myClientPointer[instanceId]; exists { + // All three map check-and-delete operations happen under a single Lock so + // a concurrent reader/writer never observes a half-cleared instance. The + // kill-channel send/close happens afterwards, on the copied local value, + // with the lock released (see ClientMapsMu doc). + ClientMapsMu.Lock() + _, hadMyClient := w.myClientPointer[instanceId] + if hadMyClient { delete(w.myClientPointer, instanceId) - w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] MyClient pointer cleared", instanceId) } - - // Limpar clientPointer se existir - if _, exists := w.clientPointer[instanceId]; exists { + _, hadClient := w.clientPointer[instanceId] + if hadClient { delete(w.clientPointer, instanceId) - w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client pointer cleared", instanceId) } + killChan, hadKillChan := w.killChannel[instanceId] + if hadKillChan { + delete(w.killChannel, instanceId) + } + ClientMapsMu.Unlock() - // Limpar killChannel se existir - if killChan, exists := w.killChannel[instanceId]; exists { + if hadMyClient { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] MyClient pointer cleared", instanceId) + } + if hadClient { + w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Client pointer cleared", instanceId) + } + if hadKillChan { select { case killChan <- true: // Canal recebeu o sinal @@ -2782,7 +2850,6 @@ func (w whatsmeowService) ClearInstanceCache(instanceId string, token string) er // Canal pode estar bloqueado, apenas fecha } close(killChan) - delete(w.killChannel, instanceId) w.loggerWrapper.GetLogger(instanceId).LogInfo("[%s] Kill channel cleared", instanceId) } @@ -2848,7 +2915,9 @@ func (w *whatsmeowService) PasskeyCeremonyStore() *ceremony.Store { // SubmitPasskeyResponse forwards the browser's WebAuthn assertion to WhatsApp // for the given instance. Called by POST /passkey-ceremony/{token}/response. func (w *whatsmeowService) SubmitPasskeyResponse(instanceId string, resp *types.WebAuthnResponse) error { + ClientMapsMu.RLock() client, ok := w.clientPointer[instanceId] + ClientMapsMu.RUnlock() if !ok || client == nil { return fmt.Errorf("no active client for instance %s", instanceId) } @@ -2867,7 +2936,9 @@ func (w *whatsmeowService) SubmitPasskeyResponse(instanceId string, resp *types. // ConfirmPasskey finishes the pairing after the user verified the code. // Called by POST /passkey-ceremony/{token}/confirm. func (w *whatsmeowService) ConfirmPasskey(instanceId string) error { + ClientMapsMu.RLock() client, ok := w.clientPointer[instanceId] + ClientMapsMu.RUnlock() if !ok || client == nil { return fmt.Errorf("no active client for instance %s", instanceId) }