Skip to content

Debug window (not for merging)#38

Open
Strogoo wants to merge 6 commits into
FAForever:mainfrom
Strogoo:main
Open

Debug window (not for merging)#38
Strogoo wants to merge 6 commits into
FAForever:mainfrom
Strogoo:main

Conversation

@Strogoo

@Strogoo Strogoo commented Oct 30, 2025

Copy link
Copy Markdown

The code is bad but it works :) Tested on test server but I'll use it in GW to make sure all run smooth. There is no Reconnect button yet but I'm planning to add it later. If this ever goes to prod, it will require a serious polishing by some GO pro :)

https://pkg.go.dev/modernc.org/tk9.0
Pretty lightweight package (final exe is ~22MB) and it doesn't require C compiler.

Final version:

изображение

Summary by CodeRabbit

  • New Features

    • Desktop GUI for monitoring connections, logs, relays and issuing reconnects — launches alongside the adapter.
  • Improvements

    • Exposes active log file path for easier diagnostics.
    • Real-time per-peer stats, nickname caching, and clearer peer listings.
    • Enhanced reconnection/backoff behavior with manual and no-relay reconnect controls.
    • Improved TURN/relay visibility with region-based filtering and per-relay info.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai

coderabbitai Bot commented Oct 30, 2025

Copy link
Copy Markdown

Walkthrough

Starts the adapter in a background goroutine and launches a Tk9.0 debug GUI; exposes package-level PeerManager and applog.LogFilePath; caches user nicknames; filters a specific TURN URL and adjusts ICE/backoff logic; and extends Peer and PeerManager with TURN-aware manual/manual-remote reconnection, per-peer watcher scaffolding, and aggregated stats/turn exposure.

Changes

Cohort / File(s) Change Summary
Adapter — global PeerManager, nicknames & ICE backoff
adapter/adapter.go
Adds exported PeerManager *webrtc.PeerManager and accessors GetPeerManager(), GetNicknames(), GetRegion(); introduces userNicknames map with mutex and caches nicknames from parsed messages (JoinGame, ConnectToPeer); assigns created PeerManager to package variable; modifies backoff logic and filters a specific TURN URL while preserving per-server credentials/fields.
Application logging accessor
applog/applog.go
Adds exported LogFilePath string, sets it when opening/creating the log file, and exposes GetLogFilePath() string.
Main startup & GUI launch
cmd/faf-adapter/main.go
Moves adapter initialization/start into a background goroutine and calls dbgwnd.CreateMainWindow() on the main thread; goroutine executes initialization/validation/start sequence and exits with os.Exit(0).
Debug GUI module (new)
dbgwnd/dbgwnd.go
New Tk9.0-based GUI: main window with Logs / Connection info / Relays tabs, raw/formatted logs view and filtering, connections tree with reconnect/no-relay buttons, event handlers, log formatting/redaction, per-peer rendering, relay display, and a periodic refresh loop; exposes CreateMainWindow and several UI handlers.
Peer manager — TURN, reconnection, watchers & stats
webrtc/peer_manager.go
Adds TURN metadata collections (allTurnServersUrls, turnsNameById) and saveTurnsURLs(), forces TURN-relay in constructor, exposes GetTurnURLs(), implements manual/manual-remote reconnection flows and scheduling, per-peer initialConnectionWatcher scaffolding with global watchers map, candidate-triggered manual-reconnect hooks, reconnection gating/skip logic, and GetAllPeersStats().
Peer struct — manual reconnection state & TURN fields
webrtc/peer.go
Adds peer fields (creationTimeSeconds, numOfManualReconns, manualReconnIsActive, remoteManualRRequest, peerSpecificTurn, specTurnIdLocal, specTurnIdRemote, numOfDroppedPackets, dropPacketsLastWarn), initializes them in CreatePeer(), and changes data-channel send-failure logging to use a drop-counter-based warn policy.
Module dependencies
go.mod
Adds numerous indirect dependencies (tk9.0/modernc libs, imaging, x/image, helper libraries) to support the new GUI and utilities.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Main as main()
    participant GUI as dbgwnd.CreateMainWindow()
    participant BG as Adapter Goroutine
    participant Adapter as adapter.Start()
    participant PM as adapter.PeerManager
    participant Log as applog

    Main->>BG: spawn goroutine (init, NewPeerManager, adapter.Start)
    Main->>GUI: CreateMainWindow() (main thread)
    BG->>Adapter: adapter.Start() builds ICE servers, filters TURNs, creates PeerManager
    Adapter->>PM: assign package-level PeerManager
    Note right of PM: accessible via GetPeerManager(), GetTurnURLs(), GetAllPeersStats()
    GUI->>PM: request stats / trigger manual reconnect
    GUI->>Log: query GetLogFilePath()
    GUI->>GUI: refreshUI() loop (periodic logs + stats)

    alt UI-triggered manual reconnect
        GUI->>PM: HandleManualReconnectRequest(playerId)
        PM->>PM: scheduleManualReconnection / preparePeerForManualReconn
    else Automatic watcher-driven reconnect
        PM->>PM: initialConnectionWatcher -> handleReconnection(forceReconnect=false)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–75 minutes

  • Focus areas:
    • dbgwnd/dbgwnd.go: large new UI surface, event wiring, concurrency in periodic refresh and I/O.
    • webrtc/peer_manager.go & webrtc/peer.go: manual vs automatic reconnection logic, TURN selection/path, watcher lifecycle, and mutex/state correctness.
    • adapter/adapter.go & cmd/faf-adapter/main.go: background startup, package-level PeerManager assignment, nickname caching, and potential race conditions with GUI access.
    • applog/applog.go: concurrent access to LogFilePath.

Poem

🐇 I hop where TURN carrots glimmer bright,
I stash small names and nudge peers back to light.
Buttons blink, logs hum, watchers tap the beat,
A rabbit's wink — reconnections neat.
Hop on, little network, our debug garden's sweet.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Debug window (not for merging)' is partially related to the changeset as it refers to the new debug window UI feature, but the explicit '(not for merging)' qualifier makes it unsuitable as a production pull request title. Revise the title to focus on the primary change: 'Add debug window UI with Tk9.0-based FAF Pioneer interface' or 'Implement debug window for peer and relay monitoring' for a production-ready pull request.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread cmd/faf-adapter/main.go
}
}()

dbgwnd.CreateMainWindow()

@Draiget Draiget Oct 30, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want to wrap regular faf-adapter with some UI related things, if we want to have debug window - let's introduce separate cmd or flag instead. IMHO this approach is very bad.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, cmd flag is needed for sure. I didn't add it so far as I don't want to modify FAF client for testing

Comment thread applog/applog.go
Comment on lines +375 to +377
func GetLogFilePath () string {
return LogFilePath
} No newline at end of file

@Draiget Draiget Oct 30, 2025

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For logging we have --log-path (info.go) argument that could set logging directory and well defined formatting for file names (applog.go), which we can just use without adding new functions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32a00d6 and a75016b.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (6)
  • adapter/adapter.go (3 hunks)
  • applog/applog.go (3 hunks)
  • cmd/faf-adapter/main.go (1 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • go.mod (2 hunks)
  • webrtc/peer_manager.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
cmd/faf-adapter/main.go (4)
launcher/info.go (2)
  • NewInfoFromFlags (22-63)
  • Info (8-20)
util/common.go (1)
  • WrapAppContextCancelExitMessage (19-27)
adapter/adapter.go (1)
  • New (33-47)
dbgwnd/dbgwnd.go (1)
  • CreateMainWindow (57-134)
webrtc/peer_manager.go (1)
adapter/adapter.go (1)
  • PeerManager (19-19)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (45-58)
dbgwnd/dbgwnd.go (2)
applog/applog.go (1)
  • GetLogFilePath (375-377)
adapter/adapter.go (1)
  • GetPeerManager (181-183)

Comment thread cmd/faf-adapter/main.go
Comment thread dbgwnd/dbgwnd.go
Comment thread webrtc/peer_manager.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
adapter/adapter.go (1)

65-76: Fix broken exponential backoff logic in ICE-Breaker retry loop

The backoff condition at line 74 will never be true. Since backoff starts at time.Second (positive), comparing it to 0*time.Second prevents the exponential backoff from ever activating, leaving retry delays stuck at 1 second indefinitely.

Replace the condition with a ceiling check:

             case <-time.After(backoff):
-                if backoff < 0*time.Second {
+                const maxBackoff = 30 * time.Second
+                if backoff < maxBackoff {
                     backoff *= 2
+                    if backoff > maxBackoff {
+                        backoff = maxBackoff
+                    }
                 }
♻️ Duplicate comments (2)
webrtc/peer_manager.go (1)

266-279: Guard GetAllPeersStats against nil peers / nil PeerConnections (still panics)

Peers that are just being created (or have been closed) can leave peer or peer.connection nil; GetAllPeersStats still dereferences p.connection unconditionally, so the debug window will panic as soon as it asks for stats on such a peer.

You can safely skip those entries:

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string) {
     p.peersMu.Lock()
     defer p.peersMu.Unlock()

     allPeersStats := make(map[string]webrtc.StatsReport)
     connectionStates := make(map[string]string)

-    for id,p := range(p.peers) {
-        allPeersStats[fmt.Sprintf("%d", id)] = p.connection.GetStats()
-        connectionStates[fmt.Sprintf("%d", id)] = p.connection.ConnectionState().String()
-    }
+    for id, peer := range p.peers {
+        if peer == nil || peer.connection == nil {
+            continue
+        }
+
+        peerID := fmt.Sprintf("%d", id)
+        allPeersStats[peerID] = peer.connection.GetStats()
+        connectionStates[peerID] = peer.connection.ConnectionState().String()
+    }

     return allPeersStats, connectionStates
 }
dbgwnd/dbgwnd.go (1)

319-395: Don’t stop parsing logs on short/blank lines; use continue instead of break

Both branches of formatLogLines do:

if len(st) < 5 {
    break
}

After strings.Split(fileContent, "\n"), the last entry is usually an empty string, so this break stops processing permanently—new log lines appended to the file will never be added to rawLogs / formattedLogs.

Change these to continue so short/malformed lines are skipped but later entries are still handled:

-            if len(st) < 5 {
-                break
-            }
+            if len(st) < 5 {
+                continue
+            }
@@
-            if len(st) < 5 {
-                break
-            }
+            if len(st) < 5 {
+                continue
+            }
🧹 Nitpick comments (5)
webrtc/peer.go (1)

23-50: Peer creation timestamp looks correct; consider using time.Time for clarity

Storing creationTimeSeconds as time.Now().Unix() at construction is fine for the 120s gating logic; if this becomes long‑lived, using a time.Time and time.Since would make the code more idiomatic and self‑documenting, but that’s purely optional.

Also applies to: 125-138

webrtc/peer_manager.go (3)

46-60: Default TURN relay is now always forced; launcher setting is ignored

NewPeerManager hard‑codes forceTurnRelay: true and adds defaultTurnServer from the initial turnServer slice, which means launcherInfo.ForceTurnRelay no longer has any effect.

If this is only for the debug build that ships with the window, that’s fine; otherwise consider threading the launcher flag through, e.g.:

-        forceTurnRelay:       true,
+        forceTurnRelay:       launcherInfo.ForceTurnRelay,
         defaultTurnServer:    turnServer,

Also applies to: 72-85


297-320: Age‑based reconnection gating works; could be simplified with time.Since

Using time.Now().Unix() - peer.creationTimeSeconds and a hard 120s threshold correctly avoids fighting with the startup watcher, but it’s a bit manual. An equivalent, slightly clearer variant would be:

peerAge := time.Since(time.Unix(peer.creationTimeSeconds, 0))
if peerAge > 2*time.Minute {
    // schedule reconnection
} else {
    // log skip
}

Not a correctness issue, just a readability tweak.


402-405: Commented‑out forceTurnRelay fallback is dead code

The commented block switching peer.forceTurnRelay off on PeerConnectionStateFailed is now dead code. If this behavior is no longer desired, consider deleting it; if it’s a planned fallback, adding a brief TODO comment with the intended condition would make the intent clearer.

dbgwnd/dbgwnd.go (1)

264-317: Log autoscroll logic is fine; consider guarding against unexpected Yview formats

textYpos is computed via strings.Split(logView.Yview(), " ")[1] without length checks; if tk9 ever changes Yview’s format, this will panic. Optional, but you could defensively handle missing parts:

parts := strings.Split(logView.Yview(), " ")
if len(parts) == 2 {
    textYpos, _ := strconv.ParseFloat(parts[1], 64)
    if textYpos > 0.99 || logViewLinesCount < 30 {
        logView.Yviewmoveto(1)
    }
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 70ae283 and 4274433.

📒 Files selected for processing (4)
  • adapter/adapter.go (4 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer.go (2 hunks)
  • webrtc/peer_manager.go (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (46-60)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (19-19)
applog/applog.go (2)
  • Info (79-93)
  • Debug (110-123)
launcher/info.go (1)
  • Info (8-20)
🔇 Additional comments (3)
webrtc/peer_manager.go (1)

120-129: Reconnection orchestration via handleReconnection / reconnectPeer / UI hook looks coherent

The new flow—queue‑driven reconnection via runReconnectionManagement, explicit handleReconnection with calledFromUI / forceReconnect flags, and the direct UI entrypoint in HandleReconnectRequestFromUI—is logically consistent and reuses the same ConnectOnce path. Skipping automatic reconnection when a peer is already Connected or Connecting also makes sense.

Also applies to: 131-173, 367-381, 638-643

dbgwnd/dbgwnd.go (2)

96-104: The closure pattern is safe and correct for Go 1.25.0 — no fix needed

The review incorrectly applies pre-Go 1.22 closure semantics. Go 1.22+ (which includes the project's Go 1.25.0) changed loop variable capture behavior so that each iteration gets its own i variable. The code as written will correctly call reconnectBtnClicked with the proper iteration index for each button.

Likely an incorrect or invalid review comment.


373-395: Review comment is inapplicable to this codebase—no action needed

The review assumes a compatibility concern with min(), but the project already specifies go 1.25.0 in go.mod. Since Go 1.21 (released August 8, 2023) added predeclared min and max functions, and this codebase targets Go 1.25.0 (well past Go 1.21), the min() call at line 389 will compile without issue. A local helper is unnecessary when the project's declared Go version guarantees support for the built-in function.

Likely an incorrect or invalid review comment.

Comment thread adapter/adapter.go Outdated
Comment thread dbgwnd/dbgwnd.go
Comment thread dbgwnd/dbgwnd.go
Comment thread webrtc/peer_manager.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webrtc/peer_manager.go (1)

545-575: Package-level manualReconnectionMessage introduces shared mutable state.

manualReconnectionMessage is a package-level variable populated lazily on first candidate gathering. This creates issues:

  1. It's shared across all peers/games
  2. The RecipientID pointer captured during initialization will be stale for subsequent peers
  3. No synchronization for concurrent access

Consider making this a field on PeerManager and constructing fresh messages per peer rather than reusing a global template.

♻️ Duplicate comments (5)
webrtc/peer_manager.go (2)

328-333: Guard against nil peer.connection — previous review not addressed.

This code still dereferences p.connection without checking for nil. Peers that are still starting up have connection == nil, which will cause a panic when GetStats() or ConnectionState() is called.

 	for id, p := range(p.peers) {
+		if p == nil || p.connection == nil {
+			continue
+		}
 		idAsString := fmt.Sprintf("%d", id)
 		allPeersStats[idAsString] = p.connection.GetStats()
 		connectionStates[idAsString] = p.connection.ConnectionState().String()
 		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
 	}

675-682: Race condition on p.peers access — previous review not addressed.

p.peers[playerId] is read without holding peersMu, and peer.connection.ConnectionState() is called without nil checks. Both issues were flagged in a previous review.

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
-		
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
+
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var state pionwebrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == pionwebrtc.PeerConnectionStateConnected {
+			peerConnnectedSecondsTotal++
+		}
dbgwnd/dbgwnd.go (3)

196-212: Checkbutton Variable() comparison uses wrong string value — previous review not addressed.

Tcl/Tk stores boolean values as "1" and "0", not "true" and "false". These comparisons will always evaluate to false.

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }
 
 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

349-364: Log parsing breaks on blank lines — previous review not addressed.

Using break when encountering short lines (< 5 chars) stops processing permanently. Since strings.Split produces empty strings from trailing newlines, this prevents new log lines from being displayed after the first blank line.

 			if len(st) < 5 {
-				break
+				continue
 			}

Apply the same fix at line 362-364.


556-565: reconnectButtonsEnabled is never updated — previous review not addressed.

The comparison reconnectButtonsEnabled != numOfIds always succeeds because reconnectButtonsEnabled is never assigned after initialization. This causes redundant button reconfiguration on every refresh.

 	numOfIds := len(sortedIds)
 	if reconnectButtonsEnabled != numOfIds {
 		for i,b := range(reconnectButtons){
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds
 	}
🧹 Nitpick comments (5)
webrtc/peer.go (2)

154-165: Duplicate IsDisabled() check is redundant.

Lines 155-157 and 160-162 perform the identical check back-to-back with no intervening logic that could change the state. The second check and its comment ("If peed are disconnected/died/disabled while reconnecting") don't make sense here since no reconnection attempt has occurred yet.

 func (p *Peer) ConnectOnce(iceServers []webrtc.ICEServer) error {
 	if p.IsDisabled() {
 		return errors.New("peer is disabled")
 	}
 
-	// If peed are disconnected/died/disabled while reconnecting, just gave up.
-	if p.IsDisabled() {
-		return errors.New("peer is disabled during reconnection")
-	}
-
 	return p.reconnect(iceServers)
 }

401-404: Downgrading send failures from Error to Debug may hide connectivity issues.

Changing the log level from Error to Debug means data channel send failures won't appear in default log views. While this reduces noise, it could make debugging connection problems harder. Consider using Warn as a middle ground, or adding a counter/metric for failed sends.

webrtc/peer_manager.go (1)

21-22: Package-level mutable state creates concurrency risks.

allTurnServersUrls and turnsNameById are package-level variables written in saveTurnsURLs and read in GetTurnURLs. If multiple PeerManager instances are created or if these are accessed concurrently, there's a data race. For a debug tool this may be acceptable, but consider making these instance fields of PeerManager.

dbgwnd/dbgwnd.go (2)

285-298: Reading entire log file on every 1-second refresh is inefficient.

os.ReadFile(logFilePath) reads the complete file contents every second. For long-running sessions, the log file can grow large, causing increased memory allocation and I/O overhead.

Consider tracking file position and reading only new content:

// Track last read position
var logFileOffset int64

func refreshLogs() {
    file, err := os.Open(logFilePath)
    if err != nil {
        return
    }
    defer file.Close()
    
    file.Seek(logFileOffset, 0)
    // Read new content only
    // Update logFileOffset after reading
}

20-43: Extensive global mutable state makes the module difficult to test and reason about.

The module uses 24 package-level variables for UI state. While acceptable for a debug tool, this design prevents multiple window instances, complicates testing, and creates implicit coupling between functions.

For future maintainability, consider encapsulating state in a struct:

type DebugWindow struct {
    logStrings      []string
    formattedLogs   []string
    // ... other fields
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2eb9147 and 5a9256e.

📒 Files selected for processing (4)
  • adapter/adapter.go (4 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer.go (3 hunks)
  • webrtc/peer_manager.go (14 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • adapter/adapter.go
🧰 Additional context used
🧬 Code graph analysis (3)
webrtc/peer_manager.go (2)
adapter/adapter.go (1)
  • PeerManager (19-19)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
dbgwnd/dbgwnd.go (2)
adapter/adapter.go (1)
  • GetPeerManager (184-186)
applog/applog.go (1)
  • GetLogFilePath (375-377)
webrtc/peer.go (2)
applog/context.go (1)
  • FromContext (10-12)
applog/applog.go (1)
  • Debug (110-123)
🔇 Additional comments (1)
webrtc/peer.go (1)

49-56: New peer fields for manual reconnection look reasonable.

The new fields properly support the manual reconnection workflow. However, these fields (manualReconnIsActive, remoteManualRRequest, numOfManualReconns) are accessed from multiple goroutines in peer_manager.go without consistent synchronization. While some accesses use reconnectMu, others don't (e.g., reads in handleReconnection at lines 174, 183).

Consider whether all accesses to these fields should be guarded by reconnectMu for thread-safety, or document which goroutine owns each field.

Comment thread dbgwnd/dbgwnd.go
Comment on lines +107 to +114
for i := 0; i < 15; i++ {
but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
reconnectButtons = append(reconnectButtons, but)

padyFloat := 8 + float64(i)*4.5
Grid(but, Row(1), Column(0), Sticky("NW"), Pady(fmt.Sprintf("%.1f", padyFloat)+"m 0m"),Padx("80m 0m"))
but.Configure(State("disabled"))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Closure captures loop variable by reference — all buttons will trigger with i=15.

The closure func() { reconnectBtnClicked(i) } captures i by reference. Since Go closures capture variables, not values, all 15 buttons will call reconnectBtnClicked(15) (the final value of i after the loop).

 	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
     	reconnectButtons = append(reconnectButtons, but)
🤖 Prompt for AI Agents
In dbgwnd/dbgwnd.go around lines 107 to 114 the loop closure uses the loop
variable i directly, causing all buttons to call reconnectBtnClicked(15); fix by
capturing the current loop index into a new local variable inside the loop
(e.g., idx := i) and use that variable in the Command closure (or alternatively
define the closure to accept the index as a parameter and pass i), so each
button's closure references its own copy of the index instead of the shared loop
variable.

Comment thread webrtc/peer_manager.go
Comment thread webrtc/peer_manager.go
Comment thread webrtc/peer_manager.go
Comment thread webrtc/peer_manager.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webrtc/peer_manager.go (1)

545-575: Shared manualReconnectionMessage has incorrect RecipientID for multiple peers.

The global manualReconnectionMessage is initialized once with the first remotePeer (line 569) and reused for all subsequent manual reconnections. When HandleManualReconnectRequest sends this message (line 731), the RecipientID will be wrong for any peer other than the first one.

Consider making this per-peer or constructing a fresh message with the correct RecipientID in HandleManualReconnectRequest:

-var manualReconnectionMessage icebreaker.CandidatesMessage
-
 func (p *PeerManager) onPeerCandidatesGathered(remotePeer uint) onPeerCandidatesGatheredCallback {
 	return func(description *webrtc.SessionDescription, candidates []webrtc.ICECandidate) {
 		err := p.icebreakerClient.SendEvent(
 			icebreaker.CandidatesMessage{
 				// ... existing message ...
 			})
-
-		// A fake candidate message that is used in manual reconnection process
-		// Will be fixed in future
-		if manualReconnectionMessage.GameID == 0 {
-			manualReconnectionMessage = icebreaker.CandidatesMessage{
-				BaseEvent: icebreaker.BaseEvent{
-					EventType:   icebreaker.EventKindCandidates,
-					GameID:      p.gameId,
-					SenderID:    p.localUserId,
-					RecipientID: &remotePeer,
-				},
-				Session:    description,
-				Candidates: candidates,
-			}
-			manualReconnectionMessage.Candidates = make([]webrtc.ICECandidate, 0)
-		}
-
 		if err != nil {
 			// ... error handling ...
 		}
 	}
 }

Then in HandleManualReconnectRequest, construct the message with the correct playerId:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
 	// ...
-	err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+	err := p.icebreakerClient.SendEvent(icebreaker.CandidatesMessage{
+		BaseEvent: icebreaker.BaseEvent{
+			EventType:   icebreaker.EventKindCandidates,
+			GameID:      p.gameId,
+			SenderID:    p.localUserId,
+			RecipientID: &playerId,
+		},
+		Session:    nil,
+		Candidates: make([]webrtc.ICECandidate, 0),
+	})
 	// ...
 }
♻️ Duplicate comments (8)
webrtc/peer_manager.go (8)

21-22: Global TURN metadata risks cross-session contamination.

Similar to the watchers map issue (lines 642-643), these package-level variables persist across adapter restarts or multiple game sessions. Stale TURN URLs could leak between sessions or cause unexpected behavior if the TURN configuration changes.

Consider moving these into the PeerManager struct as instance fields and initializing them in the constructor.


107-132: Fragile TURN URL parsing with hardcoded .com assumption.

This issue was previously flagged: the function assumes TURN server URLs contain .com (line 113), which will fail for servers using other TLDs (.org, .io, .net, country codes, etc.).

Based on past review comments, consider using the net/url package to properly parse URLs instead of string slicing with hardcoded assumptions.


320-336: Guard against nil PeerConnection instances.

This critical issue was previously flagged: peers that are still starting up leave peer.connection nil. Dereferencing it at lines 330-331 will panic as soon as the debug window requests stats.

Based on past review comments, skip peers without an active connection before calling GetStats()/ConnectionState():

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int) {
 	p.peersMu.Lock()
 	defer p.peersMu.Unlock()
 
 	allPeersStats := make(map[string]webrtc.StatsReport)
 	connectionStates := make(map[string]string)
 	turnIds := make(map[string][]int)
 
-	for id,p := range(p.peers) {
+	for id, peer := range p.peers {
+		if peer == nil || peer.connection == nil {
+			continue
+		}
+
 		idAsString := fmt.Sprintf("%d", id)
-		allPeersStats[idAsString] = p.connection.GetStats()
-		connectionStates[idAsString] = p.connection.ConnectionState().String()
-		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
+		allPeersStats[idAsString] = peer.connection.GetStats()
+		connectionStates[idAsString] = peer.connection.ConnectionState().String()
+		turnIds[idAsString] = []int{peer.specTurnIdLocal, peer.specTurnIdRemote}
 	}
 
 	return allPeersStats, connectionStates, turnIds
 }

652-718: Race condition and nil pointer dereference in initialConnectionWatcher.

This critical issue was previously flagged: at lines 678-682, p.peers[playerId] is accessed without holding peersMu, causing concurrent map access. Additionally, peer.connection may be nil during setup or after closure but is dereferenced without checking, causing a panic.

Based on past review comments, acquire the mutex before reading, check all nil conditions, and use the typed constant:

 func (p *PeerManager) initialConnectionWatcher(playerId uint) {
 	// ... setup code ...
 	
 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var state webrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == webrtc.PeerConnectionStateConnected {
+			peerConnnectedSecondsTotal++
+		}
 		
 		reconnectDelaySec -= 1
 		// ... rest of logic ...
 	}
 	// ...
 }

642-643: Global watchers map risks cross-session contamination.

This issue was previously flagged: the watchers map and watcherMutex are package-level. If the adapter is restarted or multiple games run, stale entries persist and could prevent new watchers from being created for the same player IDs.

Based on past review comments, consider making watchers an instance field of PeerManager or clearing it when a new PeerManager is created.


721-745: HandleManualReconnectRequest doesn't unlock mutex on all paths.

This critical issue was previously flagged: when peer.manualReconnIsActive || peer.remoteManualRRequest is true (line 726 condition fails), the else branch at lines 737-739 logs but never unlocks peersMu.

Based on past review comments, use defer to ensure the mutex is always released:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
 	peerAsString := fmt.Sprintf("%d", playerId)
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+
 	peer, ok := p.peers[playerId]
 	if ok {
 		if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
 			peer.manualReconnIsActive = true
-			p.peersMu.Unlock()
 			p.preparePeerForManualReconn(playerId)
 
 			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
 
 			if err != nil {
 				applog.Error("Failed to send manualReconnectionMessage to peer " + peerAsString, zap.Error(err))
 			}
 			go p.scheduleManualReconnection(playerId)
 		} else {
 			applog.Debug("Manual reconnection is already in process. Skipping local request. Peer: " + peerAsString)
 		}
 	} else {
 		applog.Error("Can't initiate manual reconnection: No such peer " + peerAsString)
-		p.peersMu.Unlock()
 	}
-	
 }

Note: After this fix, you'll need to ensure preparePeerForManualReconn doesn't also try to acquire peersMu (it does at line 782), so you'll need to refactor to avoid deadlock (see separate comment on that function).


763-778: Mutex not released on all code paths in handleRemoteManualReconnRequest.

This critical issue was previously flagged: when peer.manualReconnIsActive is true (line 767 condition fails), the function returns without unlocking peersMu.

Based on past review comments, use defer to ensure proper cleanup:

 func (p *PeerManager) handleRemoteManualReconnRequest(playerId uint) {
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+
 	peer, ok := p.peers[playerId]
 
 	if ok {
 		if !peer.manualReconnIsActive {
 			peer.remoteManualRRequest = true
-			p.peersMu.Unlock()
 
 			// No need to initiate reconn process when remote request is recieved
 			// We prepare specific turn and then wait for reconn from other side
 			p.preparePeerForManualReconn(playerId)	
-		}
-	} else {
-		p.peersMu.Unlock()
+		}
 	}
 }

Note: After this fix, you'll need to ensure preparePeerForManualReconn doesn't also try to acquire peersMu (it does at line 782), so you'll need to refactor to avoid deadlock (see separate comment on that function).


780-837: preparePeerForManualReconn acquires mutex that caller may already hold.

This issue was previously flagged: this function locks peersMu at line 782, but it's called from HandleManualReconnectRequest and handleRemoteManualReconnRequest. If those functions are fixed to use defer p.peersMu.Unlock() (as suggested in separate comments), this will deadlock.

Based on past review comments, you have two options:

Option 1 (recommended): Make this function assume the caller holds the lock:

-func (p *PeerManager) preparePeerForManualReconn(playerId uint) {
+// preparePeerForManualReconnLocked prepares peer for manual reconnection.
+// Caller must hold p.peersMu.
+func (p *PeerManager) preparePeerForManualReconnLocked(playerId uint) {
 	peerAsString := fmt.Sprintf("%d", playerId)
-	p.peersMu.Lock()
 	peer, ok := p.peers[playerId]
 
 	if ok {
 		// ... existing logic ...
 	}
-	p.peersMu.Unlock()
 }

Then update callers to call the Locked variant while holding the mutex.

Option 2: Ensure all callers explicitly unlock before calling this function (requires NOT using defer in the calling functions, which contradicts the other suggested fixes).

🧹 Nitpick comments (2)
webrtc/peer_manager.go (2)

85-85: Hardcoded forceTurnRelay overrides launcher configuration.

This unconditionally sets forceTurnRelay to true, ignoring launcherInfo.ForceTurnRelay. While acceptable for debug/testing purposes, ensure this doesn't make it to production as it prevents users from choosing their relay policy.


256-262: "Dirty hook" using empty candidates for manual reconnection signaling.

The use of an empty Candidates array to signal manual reconnection is a workaround. While functional for debugging, consider adding a proper ManualReconnectionMessage event type to the icebreaker protocol for cleaner semantics if this feature is promoted.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a9256e and cea2f1b.

📒 Files selected for processing (1)
  • webrtc/peer_manager.go (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
webrtc/peer_manager.go (2)
launcher/info.go (1)
  • Info (8-20)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (1)
webrtc/peer_manager.go (1)

163-219: LGTM: Reconnection logic with manual TURN support.

The addition of the forceReconnect parameter and the conditional logic to handle manual reconnection with specific TURN servers vs. normal reconnection is well-structured. The approach of resetting TURN IDs for normal connections (lines 186-191) is appropriate.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (5)
webrtc/peer_manager.go (5)

107-132: Fragile TURN URL parsing assumes all URLs contain .com.

This logic (line 113) will fail for servers using .org, .io, .net, or country-code TLDs. The string slicing on lines 116-125 is also error-prone.

Use net/url to parse URLs properly, extract the host, and derive a meaningful name from the hostname components.

As per past review comments, this approach is more robust and handles edge cases correctly.


320-336: Nil pointer dereference risk in GetAllPeersStats.

Lines 330-331 dereference p.connection without checking for nil. Peers that are still starting up or have been closed will have nil connections, causing a panic when the debug window requests stats.

Add nil checks before accessing connection:

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int) {
 	p.peersMu.Lock()
 	defer p.peersMu.Unlock()
 
 	allPeersStats := make(map[string]webrtc.StatsReport)
 	connectionStates := make(map[string]string)
 	turnIds := make(map[string][]int)
 
-	for id,p := range(p.peers) {
+	for id, peer := range p.peers {
+		if peer == nil || peer.connection == nil {
+			continue
+		}
+
 		idAsString := fmt.Sprintf("%d", id)
-		allPeersStats[idAsString] = p.connection.GetStats()
-		connectionStates[idAsString] = p.connection.ConnectionState().String()
-		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
+		allPeersStats[idAsString] = peer.connection.GetStats()
+		connectionStates[idAsString] = peer.connection.ConnectionState().String()
+		turnIds[idAsString] = []int{peer.specTurnIdLocal, peer.specTurnIdRemote}
 	}
 
 	return allPeersStats, connectionStates, turnIds
 }

As per past review comments, this prevents panics when peers are in transitional states.


642-718: Race condition and nil pointer risks in initialConnectionWatcher.

Two critical issues identified by past review:

  1. Race condition (lines 678-682): p.peers[playerId] is accessed without holding peersMu, causing concurrent map access when other goroutines modify the map.

  2. Nil pointer (line 679): peer.connection.ConnectionState() is called without checking if peer or peer.connection is nil, which will panic during startup or after closure.

Acquire the mutex before reading, check for nil, release immediately:

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var state webrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == webrtc.PeerConnectionStateConnected {
+			peerConnnectedSecondsTotal++
+		}

As per past review comments, this prevents both the race and the panic.

Additionally, the global watchers map (lines 642-643) persists across adapter restarts, which past review flagged as a major issue. Consider moving it into PeerManager as documented in prior feedback.


721-745: Mutex not unlocked in one code path.

Line 738's else branch (when manual reconnection is already active) logs but never unlocks peersMu, causing a deadlock. Line 742 correctly unlocks for the "no such peer" case.

Use defer to ensure the mutex is always released:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
 	peerAsString := fmt.Sprintf("%d", playerId)
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+
 	peer, ok := p.peers[playerId]
 	if ok {
 		if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
 			peer.manualReconnIsActive = true
-			p.peersMu.Unlock()
 			p.preparePeerForManualReconn(playerId)
 
 			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
 
 			if err != nil {
 				applog.Error("Failed to send manualReconnectionMessage to peer " + peerAsString, zap.Error(err))
 			}
 			go p.scheduleManualReconnection(playerId)
 		} else {
 			applog.Debug("Manual reconnection is already in process. Skipping local request. Peer: " + peerAsString)
 		}
 	} else {
 		applog.Error("Can't initiate manual reconnection: No such peer " + peerAsString)
-		p.peersMu.Unlock()
 	}
-	
 }

Note: After this fix, preparePeerForManualReconn (line 729) will deadlock because it also tries to acquire peersMu (line 784). See separate comment on that function.

As per past review comments, using defer simplifies the control flow and prevents missed unlocks.


782-839: preparePeerForManualReconn will deadlock when callers hold peersMu.

This function locks peersMu (line 784), but its callers (HandleManualReconnectRequest and handleRemoteManualReconnRequest) may already hold the lock. If you apply the recommended defer unlock pattern to those functions, calling this will deadlock.

Either:

  1. Refactor to assume caller holds lock (recommended): Rename to preparePeerForManualReconnLocked, remove lines 784 and 838, and document that callers must hold peersMu.
  2. Ensure callers unlock before calling: Explicitly unlock in callers before calling this function.

Option 1 is cleaner and prevents accidental misuse.

As per past review comments, clarifying mutex ownership prevents these issues.

🧹 Nitpick comments (5)
webrtc/peer_manager.go (5)

21-22: Global TURN state may cause cross-session issues.

These package-level variables persist across PeerManager instances. If the adapter restarts or multiple games run, stale TURN data could interfere.

Consider moving allTurnServersUrls and turnsNameById into the PeerManager struct and initializing them in NewPeerManager or saveTurnsURLs.


85-85: forceTurnRelay hardcoded, ignoring launcher configuration.

The value is always set to true, bypassing launcherInfo.ForceTurnRelay. If this is intentional for debugging, consider adding a comment. Otherwise, restore the launcher value.


256-262: Empty candidates used as manual reconnection signal (acknowledged workaround).

The comment indicates this is temporary. While functional for debugging, consider adding a dedicated ManualReconnectionMessage event type to the icebreaker protocol for clarity and type safety.


545-575: Global manualReconnectionMessage initialized lazily on first use.

Line 563 checks GameID == 0 to determine first use. This works but adds overhead on every call. Consider initializing this in NewPeerManager or a dedicated setup function.

Also, as with other globals, this could cause issues if multiple PeerManager instances exist.


763-780: Mutex is now unlocked on all paths (past issue addressed).

Lines 769, 775, and 778 ensure peersMu is released in all branches. This addresses the past review concern.

However, using defer p.peersMu.Unlock() after line 764 would simplify the code and prevent future mistakes:

 func (p *PeerManager) handleRemoteManualReconnRequest(playerId uint) {
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+
 	peer, ok := p.peers[playerId]
 	if ok {
 		if !peer.manualReconnIsActive {
 			peer.remoteManualRRequest = true
-			p.peersMu.Unlock()
 
 			p.preparePeerForManualReconn(playerId)	
-		} else {
-			p.peersMu.Unlock()
 		}
-	} else {
-		p.peersMu.Unlock()
 	}
 }

Note: preparePeerForManualReconn also locks peersMu (see separate comment), so you'll need to refactor that function to avoid deadlock.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cea2f1b and 98c4f5a.

📒 Files selected for processing (1)
  • webrtc/peer_manager.go (14 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
webrtc/peer_manager.go (4)
adapter/adapter.go (1)
  • PeerManager (19-19)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
launcher/info.go (1)
  • Info (8-20)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (4)
webrtc/peer_manager.go (4)

163-219: Manual reconnection flow separation looks functional.

The logic correctly differentiates between normal reconnection (using default TURN list) and manual reconnection (using peerSpecificTurn). The flag management and error handling appear sound.

The complexity is reasonable given the dual-path requirement.


364-378: Startup reconnection skip logic is a reasonable workaround.

The 2-minute threshold prevents reconnection attempts from interfering with initial connection establishment. The comment acknowledges this needs a proper fix later, which is appropriate for this debug window context.


452-477: Manual reconnection state handling prevents conflicting reconnection attempts.

The logic correctly skips automatic reconnection scheduling when manual reconnection is active, avoiding race conditions between the two flows.


747-761: scheduleManualReconnection mutex usage is correct.

The function properly acquires and releases peersMu around the peer existence check before calling handleReconnection.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (4)
dbgwnd/dbgwnd.go (4)

108-116: Fix closure over loop variable so each RECONNECT button uses its own index

All 15 buttons currently capture i by reference, so they will all call reconnectBtnClicked(15) instead of their own index. Capture i into a local variable inside the loop and use that in the closure.

-	// Reconnect buttons (15 buttons as max players count is 16)
-	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+	// Reconnect buttons (15 buttons as max players count is 16)
+	for i := 0; i < 15; i++ {
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 		reconnectButtons = append(reconnectButtons, but)

142-147: Checkbutton state handling is brittle; .Variable() comparison may never flip flags

The checkbuttons bind Variable(rawLogsChButtonVar) / Variable(hideDebugButtonVar) but the callbacks compare ...Variable() == "true", which may not match the actual string values tk9.0 stores for the on/off states. That can leave showRawLogs and hideDebug effectively stuck.

Consider either:

  • Binding real Go bools (if supported by tk9.0) and reading those, or
  • Using explicit on/off strings and comparing against those, e.g.:
-rawLogsChkButton = logFrame.Checkbutton(Txt("Show unformatted logs"), Variable(rawLogsChButtonVar), 
-Onvalue(true), Offvalue(false), Command(func() { rawLogsChkButtonPressed() }))
+rawLogsChkButton = logFrame.Checkbutton(
+	Txt("Show unformatted logs"),
+	Variable("rawLogs"),          // tk variable name or similar
+	Onvalue("true"),
+	Offvalue("false"),
+	Command(func() { rawLogsChkButtonPressed() }),
+)

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
-		showRawLogs = true
-	} else {
-		showRawLogs = false
-	}
+	showRawLogs = (rawLogsChkButton.Variable() == "true")
 }

Apply the same pattern to hideDebugChkButtonPressed().

Also applies to: 198-214


287-340: Log parsing stops at the first short/blank line; later log entries are skipped

In formatLogLines() both branches break the loop when len(st) < 5. If a blank or very short line appears anywhere in the log (not just the trailing split-empty), all subsequent entries are ignored on every refresh, which can make the log view silently stale.

Replace the break with continue so short lines are skipped but later lines are still processed:

-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}
@@
-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}

Also applies to: 342-419


561-570: reconnectButtonsEnabled is never updated, so the check is redundant

reconnectButtonsEnabled is always 0, so whenever len(sortedIds) > 0 this block runs on every refreshConnStats() call. Either update the variable after configuring the buttons, or remove the comparison.

To make the optimization effective:

 	numOfIds := len(sortedIds)
 	if reconnectButtonsEnabled != numOfIds {
 		for i,b := range(reconnectButtons){
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds
 	}
🧹 Nitpick comments (4)
dbgwnd/dbgwnd.go (4)

226-255: Be explicit about Listbox.Get return type and indexing for readability/safety

Both turnServerSelected() and infoListIdSelected() assume Listbox.Get(...) returns a slice and index into [0]. If tk9.0 ever changes this API or someone unfamiliar reads the code, it’s easy to misinterpret this as string indexing.

To make intent clear and safer, consider naming the element and avoiding double len(...) > 0 checks:

-	selectedURL := turnServersList.Get(strconv.Itoa(selected[0]))
-	if len(selectedURL) > 0 {
-		selectedTurnServer = selectedURL[0]
+	urls := turnServersList.Get(strconv.Itoa(selected[0]))
+	if len(urls) > 0 {
+		selectedTurnServer = urls[0]
 		selectedTurnURL.Configure(Txt(selectedTurnServer))
 	}

Same idea for connIdListBox.Get(...) / selectedId[0].


287-301: Reading the whole log file every second can be expensive on large logs

refreshLogs() re-reads the entire log file (os.ReadFile) each second, then re-splits and re-formats. For long-running sessions or big logs this can become a noticeable I/O and CPU hit.

Consider tailing from the last offset or using os.File and Seek to read only appended data; keep the full-file read as a fallback for initial load or error recovery.


378-415: Log formatting assumes a very specific JSON layout and can panic on unexpected lines

The timestamp replacement and user-id stripping assume fixed positions:

  • formattedLogs[i][:index-27] will panic if index < 27.
  • The "localUserId" / "localGameId" removal logic assumes both keys are present and ordered; malformed or different log formats can break this.

For a debug tool this may be acceptable, but to make it more robust you could:

  • Guard index >= 27 before slicing.
  • Use encoding/json into a small struct/map and re-emit the fields you want instead of manual string surgery.

422-444: Avoid dummy element in sortedIdsInt to simplify logic

sortedIdsInt := make([]int, 1) seeds the slice with an extra zero that you then filter out (if id != 0). This works but is confusing.

Simpler and clearer:

-		sortedIds = nil
-		sortedIdsInt := make([]int, 1)
+		sortedIds = nil
+		var sortedIdsInt []int
 		for id := range allPeersStats {
 			i, err := strconv.Atoi(id)
 			if err == nil {
 				sortedIdsInt = append(sortedIdsInt, i)
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98c4f5a and d06d754.

📒 Files selected for processing (1)
  • dbgwnd/dbgwnd.go (1 hunks)

Comment thread dbgwnd/dbgwnd.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (11)
webrtc/peer_manager.go (6)

787-844: Lock ordering in preparePeerForManualReconn is fragile.

This function acquires peersMu while callers sometimes hold it before calling. Current callers do unlock first, but this pattern is error-prone. Consider either documenting the lock contract clearly or making it a Locked variant that assumes the caller holds the lock.


331-336: Nil pointer dereference risk in GetAllPeersStats.

p.connection may be nil during peer setup or after closure. Dereferencing it without a guard will panic.

-	for id,p := range(p.peers) {
+	for id, peer := range(p.peers) {
+		if peer == nil || peer.connection == nil {
+			continue
+		}
 		idAsString := fmt.Sprintf("%d", id)
-		allPeersStats[idAsString] = p.connection.GetStats()
-		connectionStates[idAsString] = p.connection.ConnectionState().String()
-		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
+		allPeersStats[idAsString] = peer.connection.GetStats()
+		connectionStates[idAsString] = peer.connection.ConnectionState().String()
+		turnIds[idAsString] = []int{peer.specTurnIdLocal, peer.specTurnIdRemote}
 	}

Note: The loop variable p shadows the receiver p *PeerManager. Rename it to peer for clarity.


108-133: Fragile TURN URL parsing assumes .com TLD.

The string parsing at line 114 looks for .com, which will fail for servers using other TLDs (.org, .io, .net, country codes). Consider using net/url for proper parsing.


645-646: Global watchers map risks cross-session state leakage.

The package-level watchers map and mutex persist across adapter restarts. Stale entries could prevent new watchers from being created for the same player IDs in a new session. Consider making this an instance field of PeerManager.


678-685: Race condition and nil pointer risk in initialConnectionWatcher.

p.peers[playerId] is accessed without holding peersMu, racing with other goroutines. Additionally, peer.connection may be nil and is dereferenced without checking.

 for i := 0; i < keepAliveSec; i++ {
 	time.Sleep(time.Second)
-	
-	if peer, ok := p.peers[playerId]; ok {
-		if peer.connection.ConnectionState().String() == "connected"{
-			peerConnnectedSecondsTotal += 1
-		}
-	}
+
+	p.peersMu.Lock()
+	peer, ok := p.peers[playerId]
+	var state webrtc.PeerConnectionState
+	if ok && peer != nil && peer.connection != nil {
+		state = peer.connection.ConnectionState()
+	}
+	p.peersMu.Unlock()
+
+	if state == webrtc.PeerConnectionStateConnected {
+		peerConnnectedSecondsTotal++
+	}

724-749: Mutex not released when manual reconnection is already active.

When peer.manualReconnIsActive || peer.remoteManualRRequest is true (line 729 condition fails), the else branch at lines 741-743 logs but never unlocks peersMu, causing a deadlock. Use defer for cleaner control flow.

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
 	peerAsString := fmt.Sprintf("%d", playerId)
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+
 	peer, ok := p.peers[playerId]
 	if ok {
 		if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
 			peer.manualReconnIsActive = true
 			p.disableRecBtnPeers = append(p.disableRecBtnPeers, peerAsString)
-			p.peersMu.Unlock()
-			p.preparePeerForManualReconn(playerId)
+			// Note: preparePeerForManualReconn also acquires peersMu - refactor needed
+			go func() {
+				p.preparePeerForManualReconn(playerId)
+			}()
 
 			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
 			// ... rest unchanged
 		} else {
 			applog.Debug("Manual reconnection is already in process. Skipping local request. Peer: " + peerAsString)
 		}
 	} else {
 		applog.Error("Can't initiate manual reconnection: No such peer " + peerAsString)
-		p.peersMu.Unlock()
 	}
-	
 }

Warning: preparePeerForManualReconn also acquires peersMu (line 789), so with defer, you'll need to refactor to avoid deadlock. Either make preparePeerForManualReconn assume the lock is held, or call it after unlocking.

dbgwnd/dbgwnd.go (5)

117-124: Closure captures loop variable — all buttons trigger with i=15.

The loop variable i is captured by reference in the closure. After the loop completes, all 15 buttons will call reconnectBtnClicked(15).

 for i := 0; i < 15; i++ {
-	but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+	idx := i // capture current value
+	but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 	reconnectButtons = append(reconnectButtons, but)

206-222: Checkbutton value comparisons will always fail.

Tcl/Tk stores boolean values as "1" and "0", not "true" and "false". The comparisons at lines 208 and 217 will never match, so showRawLogs and hideDebug will always be set to false.

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }

 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

359-361: Log parsing stops on first blank line.

strings.Split yields empty strings from trailing newlines. The break statements stop processing entirely on the first short entry, preventing new log lines from reaching the UI. Replace with continue.

 		if len(st) < 5 {
-			break
+			continue
 		}

Apply this fix in both branches (lines 359-361 and 372-374).

Also applies to: 372-374


548-555: Missing bounds check on turnIds slice access.

Accessing tIds[0] and tIds[1] without verifying slice length can panic if the slice is shorter than expected.

 if tIds, ok := turnIds[id]; ok {
-	if tIds[0] != 0 {
+	if len(tIds) > 0 && tIds[0] != 0 {
 		localTurnName = turnsNameById[tIds[0]]
 	}
-	if tIds[1] != 0 {
+	if len(tIds) > 1 && tIds[1] != 0 {
 		remoteTurnName = turnsNameById[tIds[1]]
 	}
 }

581-593: reconnectButtonsEnabled is incorrectly updated inside the loop.

The variable is incremented inside the loop (line 586) instead of being set to numOfIds after the loop. This causes the enable/disable logic to run on every refresh and produces incorrect state tracking.

 func refreshButtons() {
 	numOfIds := len(sortedIds)

 	if reconnectButtonsEnabled != numOfIds {
 		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds
 	}
🧹 Nitpick comments (2)
dbgwnd/dbgwnd.go (2)

25-53: Extensive mutable global state reduces testability and risks unintended side effects.

The file relies heavily on package-level mutable state. While acceptable for a debug tool, consider encapsulating this state in a struct if this code is ever promoted to production. This would improve testability and allow multiple instances if needed.


295-308: File reads entire log on every refresh.

os.ReadFile(logFilePath) reads the entire log file every second. For large log files, this becomes expensive. Consider using os.Seek and reading only new content, or limiting the displayed lines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d06d754 and 5981de0.

📒 Files selected for processing (2)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (19-19)
applog/applog.go (1)
  • Info (79-93)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
dbgwnd/dbgwnd.go (3)
adapter/adapter.go (2)
  • PeerManager (19-19)
  • GetPeerManager (184-186)
moho/packet.go (1)
  • State (28-28)
applog/applog.go (1)
  • GetLogFilePath (375-377)
🔇 Additional comments (1)
webrtc/peer_manager.go (1)

86-86: forceTurnRelay hardcoded to true.

This forces all connections through TURN relay servers. Verify this is intentional for this debug branch, as it will prevent direct peer-to-peer connections and increase latency/server load.

Comment thread dbgwnd/dbgwnd.go
Comment thread webrtc/peer_manager.go
Comment thread webrtc/peer_manager.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (10)
webrtc/peer_manager.go (4)

164-220: Manual reconnection flags are accessed under inconsistent/no locking

manualReconnIsActive and remoteManualRRequest are:

  • Written under peersMu in HandleManualReconnectRequest / handleRemoteManualReconnRequest.
  • Written under peer.reconnectMu in the manual branch of handleReconnection.
  • Read with no lock in handleReconnection and onPeerStateChanged.

This inconsistent locking creates data races on these fields and on peer.connection state, especially with concurrent reconnection attempts and state changes.

Pick a single mutex to protect these flags (preferably peer.reconnectMu for reconnection‑related state) and use it consistently:

 func (p *PeerManager) handleReconnection(playerId uint, forceReconnect bool) {
@@
-	if !forceReconnect && !peer.manualReconnIsActive && !peer.remoteManualRRequest{
-		if peer.IsActive() || (peer.connection != nil &&
-			peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnecting) {
+	peer.reconnectMu.Lock()
+	manualActive := peer.manualReconnIsActive
+	remoteReq := peer.remoteManualRRequest
+	conn := peer.connection
+	peer.reconnectMu.Unlock()
+
+	if !forceReconnect && !manualActive && !remoteReq {
+		if peer.IsActive() || (conn != nil &&
+			conn.ConnectionState() == webrtc.PeerConnectionStateConnecting) {
@@
-	if !peer.manualReconnIsActive && !peer.remoteManualRRequest{
+	if !manualActive && !remoteReq {
@@
-		if err := peer.ConnectOnce(peer.peerSpecificTurn); err != nil {
+		if err := peer.ConnectOnce(peer.peerSpecificTurn); err != nil {
@@
-		peer.reconnectMu.Lock()
-		peer.manualReconnIsActive = false
-		peer.remoteManualRRequest = false
-		peer.reconnectMu.Unlock()
+		peer.reconnectMu.Lock()
+		peer.manualReconnIsActive = false
+		peer.remoteManualRRequest = false
+		peer.reconnectMu.Unlock()

And in onPeerStateChanged / manual‑reconn handlers, read/update these flags only while holding the same mutex.

Also applies to: 447-476, 724-787


21-23: TURN URL/name handling is fragile and can panic on short hostnames

saveTurnsURLs still assumes .com and slices strings by fixed indices. Besides being brittle for non‑.com TLDs, the branch:

preparedName = preparedName[len(preparedName)-8:]

will panic if len(preparedName) < 8 (e.g. turn:aa.com), crashing the adapter. This also feeds into turnsNameById / GetTurnURLs used by the debug UI.

At minimum, guard against short strings and .com absence; ideally use net/url to parse the host:

-import "strings"
+import (
+    "net/url"
+    "strings"
+)
@@
 func (p *PeerManager) saveTurnsURLs() {
-	for i, _ := range(p.turnServer) {
-		for _,url := range(p.turnServer[i].URLs) {
-			_, ok := turnsNameById[i+1]
-			if !ok {
-				preparedName := " "
-				index := strings.Index(url, ".com")
-
-				if index != -1 {
-					preparedName = url[:index]
-					index = strings.LastIndex(preparedName, ".")
-					if index != -1 {
-						preparedName = preparedName[index+1:]
-						if len(preparedName) > 6 {
-							preparedName = preparedName[:6]
-						}
-					} else {
-						preparedName = preparedName[len(preparedName) - 8:]
-					}
-				}
-				turnsNameById[i+1] = preparedName
-			}
-			allTurnServersUrls = append(allTurnServersUrls, url)
-		}
-	}
+	for i := range p.turnServer {
+		for _, raw := range p.turnServer[i].URLs {
+			if _, ok := turnsNameById[i+1]; !ok {
+				name := "-"
+				if u, err := url.Parse(raw); err == nil {
+					host := u.Host
+					if idx := strings.Index(host, ":"); idx != -1 {
+						host = host[:idx]
+					}
+					parts := strings.Split(host, ".")
+					if len(parts) >= 2 {
+						name = parts[len(parts)-2] // second‑level domain
+					} else if host != "" {
+						name = host
+					}
+					if len(name) > 6 {
+						name = name[:6]
+					}
+				}
+				turnsNameById[i+1] = name
+			}
+			allTurnServersUrls = append(allTurnServersUrls, raw)
+		}
+	}

Also applies to: 86-92, 108-133, 349-351


321-339: GetAllPeersStats can panic on peers without an active connection

This method locks peersMu but then unconditionally dereferences p.connection (the per‑peer *webrtc.PeerConnection) for every entry. For peers that are still starting up or already closed, peer.connection can be nil, leading to a panic when the debug window calls this.

Skip peers with nil connections before calling GetStats/ConnectionState:

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int, []string) {
@@
-	for id,p := range(p.peers) {
-		idAsString := fmt.Sprintf("%d", id)
-		allPeersStats[idAsString] = p.connection.GetStats()
-		connectionStates[idAsString] = p.connection.ConnectionState().String()
-		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
-	}
+	for id, peer := range p.peers {
+		if peer == nil || peer.connection == nil {
+			continue
+		}
+
+		idAsString := fmt.Sprintf("%d", id)
+		allPeersStats[idAsString] = peer.connection.GetStats()
+		connectionStates[idAsString] = peer.connection.ConnectionState().String()
+		turnIds[idAsString] = []int{peer.specTurnIdLocal, peer.specTurnIdRemote}
+	}

645-721: initialConnectionWatcher races on p.peers and can nil‑deref peer.connection

Inside initialConnectionWatcher:

  • p.peers[playerId] is read without peersMu, while other goroutines modify p.peers, risking “concurrent map read and map write”.
  • peer.connection.ConnectionState() is called without checking peer/peer.connection for nil, which is possible early in setup or after closure.

Wrap map access in peersMu and snapshot the connection state safely:

 func (p *PeerManager) initialConnectionWatcher(playerId uint) {
@@
-	for i := 0; i < keepAliveSec; i++ {
-		time.Sleep(time.Second)
-		
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
+	for i := 0; i < keepAliveSec; i++ {
+		time.Sleep(time.Second)
+
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var connected bool
+		if ok && peer != nil && peer.connection != nil &&
+			peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnected {
+			connected = true
+		}
+		p.peersMu.Unlock()
+
+		if connected {
+			peerConnnectedSecondsTotal++
+		}
adapter/adapter.go (1)

17-23: Global PeerManager is still racy between adapter startup and the debug UI

PeerManager is written in Start() (likely from a background goroutine) and read from the debug window via GetPeerManager() with no synchronization. Even a single flip from nil→non‑nil without a happens‑before edge is a data race in Go and can break under the race detector.

Either avoid the package‑level variable and pass *PeerManager into the UI, or guard access with a mutex/atomic.Value. For example:

+import "sync"
+
-var PeerManager     *webrtc.PeerManager
+var (
+    peerManagerMu sync.RWMutex
+    peerManager   *webrtc.PeerManager
+)
@@
-    peerManager := webrtc.NewPeerManager(
+    peerManager := webrtc.NewPeerManager(
@@
-    PeerManager = peerManager
+    peerManagerMu.Lock()
+    peerManager = peerManager
+    peerManagerMu.Unlock()
@@
-func GetPeerManager() *webrtc.PeerManager {
-	return PeerManager
-}
+func GetPeerManager() *webrtc.PeerManager {
+	peerManagerMu.RLock()
+	defer peerManagerMu.RUnlock()
+	return peerManager
+}

Also applies to: 120-132, 211-213

dbgwnd/dbgwnd.go (5)

117-125: Reconnect button closures capture the loop index by reference

Each button’s Command(func() { reconnectBtnClicked(i) }) closes over the loop variable i. In Go, all closures created in this loop will see the final value i == 15, so every button will trigger the same buttonID.

Capture the index in a new local per iteration:

-	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+	for i := 0; i < 15; i++ {
+		idx := i
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 		reconnectButtons = append(reconnectButtons, but)

151-156: Checkbutton Variable handling and string comparison are likely wrong

You pass plain bools to Variable(...) and later compare rawLogsChkButton.Variable() / hideDebugChkButton.Variable() to "true". Tk/Tk9 typically uses "1"/"0" (or a Tcl var name) under the hood, so these comparisons may never be true and raw/debug toggles won’t behave as expected.

Either wire real Go bools, or at least compare to "1":

Preferred (bind Go bools):

-var rawLogsChButtonVar  bool
-var hideDebugButtonVar  bool
+var rawLogsChButtonVar  bool
+var hideDebugButtonVar  bool
@@
-	rawLogsChkButton = logFrame.Checkbutton(Txt("Show unformatted logs"), Variable(rawLogsChButtonVar), 
+	rawLogsChkButton = logFrame.Checkbutton(Txt("Show unformatted logs"), Variable(&rawLogsChButtonVar),
 		Onvalue(true), Offvalue(false), Command(func() { rawLogsChkButtonPressed() }))
@@
-	hideDebugChkButton = logFrame.Checkbutton(Txt("Hide DEBUG"), Variable(hideDebugButtonVar), 
+	hideDebugChkButton = logFrame.Checkbutton(Txt("Hide DEBUG"), Variable(&hideDebugButtonVar),
 		Onvalue(true), Offvalue(false), Command(func() { hideDebugChkButtonPressed() }))
@@
 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
-		showRawLogs = true
-	} else {
-		showRawLogs = false
-	}
+	showRawLogs = rawLogsChButtonVar
 }
@@
 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
-		hideDebug = true
-	} else {
-		hideDebug = false
-	}
+	hideDebug = hideDebugButtonVar
 }

Also applies to: 207-223


296-309: Log parsing stops permanently on the first short/blank line

Both branches of formatLogLines break the loop when len(st) < 5. Because the file is split on \n, you’ll always see empty entries, and the first one stops processing forever—new log lines never get added to rawLogs / formattedLogs.

Change the break to continue so short/blank lines are skipped but later entries still process:

 func formatLogLines() {
 	if showRawLogs {
 		for i, st := range logStrings {
@@
-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}
@@
 	} else {
 		for i, st := range logStrings {
@@
-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}

Also applies to: 351-365, 367-379


549-556: Guard against short turnIds slices when resolving TURN names

turnIds[id] is indexed at [0] and [1] without checking the slice length. If GetAllPeersStats() ever returns a shorter slice for a peer (bug, partial data, or future change), this will panic and bring down the debug UI.

Defensively check lengths before indexing:

	if tIds, ok := turnIds[id]; ok {
-		if tIds[0] != 0 {
-			localTurnName = turnsNameById[tIds[0]]
-		}
-		if tIds[1] != 0 {
-			remoteTurnName = turnsNameById[tIds[1]]
-		}
+		if len(tIds) > 0 && tIds[0] != 0 {
+			localTurnName = turnsNameById[tIds[0]]
+		}
+		if len(tIds) > 1 && tIds[1] != 0 {
+			remoteTurnName = turnsNameById[tIds[1]]
+		}
	}

589-601: Reconnect button state tracking is incorrect; idsToDisable cleared too early

Two issues in refreshButtons:

  1. reconnectButtonsEnabled is incremented inside the loop instead of being set to numOfIds, so it drifts away from the actual number of active IDs and the guard becomes meaningless.
  2. idsToDisable is set to nil inside the loop, so only the first sortedIds[i] is ever matched; subsequent IDs in idsToDisable are ignored.

Suggested fix:

 func refreshButtons() {
 	numOfIds := len(sortedIds)

 	if reconnectButtonsEnabled != numOfIds {
-		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
+		for i, b := range reconnectButtons {
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds
 	}
@@
-	if len(idsToDisable) > 0 || len(disabledBtnsTimers) > 0 {
-		for i,_ := range(disabledBtnsTimers) {
+	if len(idsToDisable) > 0 || len(disabledBtnsTimers) > 0 {
+		for id := range disabledBtnsTimers {
-			if disabledBtnsTimers[i] > 0 {
-				disabledBtnsTimers[i] -= 1
+			if disabledBtnsTimers[id] > 0 {
+				disabledBtnsTimers[id] -= 1
 			} else {
-				delete(disabledBtnsTimers, i)
+				delete(disabledBtnsTimers, id)
 			}
 		}
 
-		for i,_ := range(sortedIds) {
-			if slices.Contains(idsToDisable, sortedIds[i]){
-				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnManualRec
+		for i := range sortedIds {
+			if slices.Contains(idsToDisable, sortedIds[i]) {
+				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnManualRec
 			}
 
 			if _, ok := disabledBtnsTimers[sortedIds[i]]; ok {
 				reconnectButtons[i].Configure((State("disabled")))
 			} else {
 				reconnectButtons[i].Configure((State("normal")))
 			}
-			idsToDisable = nil
 		}
+		idsToDisable = nil
 	}
 }

Also applies to: 611-632

🧹 Nitpick comments (1)
adapter/adapter.go (1)

68-82: Backoff condition is dead; simplify to explicit fixed delay

The comment says you want a fixed 1s reconnect delay, but if backoff < 0*time.Second { backoff *= 2 } is never true with backoff := time.Second, so this code is effectively dead and confusing.

Either remove the unused growth logic or just use a literal in time.After:

-		backoff := time.Second
+		backoff := time.Second
@@
-			case <-time.After(backoff):
-				// Better don't increase delay and just use fixed 1 sec value
-				// Might be critical in some situations when you want to restore conn as fast as possible
-				if backoff < 0*time.Second {
-					backoff *= 2
-				}
+			case <-time.After(backoff):
+				// Fixed 1s delay; no exponential backoff on reconnect.
 			}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5981de0 and 4e4f647.

📒 Files selected for processing (3)
  • adapter/adapter.go (5 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (2)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
dbgwnd/dbgwnd.go (5)
adapter/adapter.go (3)
  • PeerManager (20-20)
  • GetPeerManager (211-213)
  • GetNicknames (215-220)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
webrtc/peer.go (1)
  • Peer (23-56)
moho/packet.go (1)
  • State (28-28)
applog/applog.go (1)
  • GetLogFilePath (375-377)

Comment thread adapter/adapter.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webrtc/peer_manager.go (1)

548-578: Global manualReconnectionMessage still risks stale data.

While RecipientID is now updated at line 757 before sending, the global manualReconnectionMessage is populated with the first peer's session and then reused. If the session description becomes stale or the first peer disconnects, subsequent manual reconnection requests may send invalid data.

Consider constructing a fresh message for each request:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
     // ...
-    manualReconnectionMessage.BaseEvent.RecipientID = &playerId
-    err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+    msg := icebreaker.CandidatesMessage{
+        BaseEvent: icebreaker.BaseEvent{
+            EventType:   icebreaker.EventKindCandidates,
+            GameID:      p.gameId,
+            SenderID:    p.localUserId,
+            RecipientID: &playerId,
+        },
+        Candidates: []webrtc.ICECandidate{}, // empty = manual reconnect signal
+    }
+    err := p.icebreakerClient.SendEvent(msg)
♻️ Duplicate comments (13)
adapter/adapter.go (2)

20-22: Global PeerManager still lacks synchronization with readers.

The past review comment about the race condition between Start() writing PeerManager and GetPeerManager() reading it remains unaddressed. While pointer assignment is atomic at the machine level, this is still a data race under Go's memory model.

Consider using atomic.Value or a mutex as previously suggested:

+import "sync/atomic"
+
-var PeerManager     *webrtc.PeerManager
+var peerManager atomic.Value // stores *webrtc.PeerManager

211-213: GetPeerManager returns the global without synchronization.

This accessor reads the global PeerManager that is written from a different goroutine in Start(). Without synchronization, this is a data race.

dbgwnd/dbgwnd.go (6)

104-111: Closure captures loop variable i by reference — all buttons will call reconnectBtnClicked(15).

The closure func() { reconnectBtnClicked(i) } captures i by reference. After the loop completes, all 15 button callbacks will use the final value of i (15).

 	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 		reconnectButtons = append(reconnectButtons, but)

199-215: Checkbutton Variable() returns "1"/"0", not "true"/"false".

Tcl/Tk stores boolean values as "1" and "0". The comparisons against "true" will always fail.

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }

 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

345-360: Using break instead of continue stops log parsing on first short line.

Log files often have blank lines (especially the last line after a newline). Using break stops all further processing when a short line is encountered, preventing new log entries from reaching the UI.

 			if len(st) < 5 {
-				break
+				continue
 			}

Apply this fix in both the raw logs branch (line 346) and formatted logs branch (line 359).


540-547: No bounds check on tIds slice before indexing.

If turnIds[id] returns a slice shorter than 2 elements, accessing tIds[0] or tIds[1] will panic.

 			if tIds, ok := turnIds[id]; ok {
-				if tIds[0] != 0 {
+				if len(tIds) > 0 && tIds[0] != 0 {
 					localTurnName = turnsNameById[tIds[0]]
 				}
-				if tIds[1] != 0 {
+				if len(tIds) > 1 && tIds[1] != 0 {
 					remoteTurnName = turnsNameById[tIds[1]]
 				}
 			}

580-602: reconnectButtonsEnabled is incremented but never reset, breaking the optimization.

Line 583 checks reconnectButtonsEnabled != numOfIds, but line 586 increments reconnectButtonsEnabled inside the loop without ever resetting it. After the first run, this counter will exceed numOfIds and the logic breaks.

 func refreshButtons() {
 	numOfIds := len(sortedIds)

 	if reconnectButtonsEnabled != numOfIds {
 		// normal buttons
 		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds

621-641: idsToDisable = nil inside loop clears the slice after first iteration.

Setting idsToDisable = nil on line 640 inside the loop prevents subsequent iterations from checking against idsToDisable.

 		for i,_ := range(sortedIds) {
 			if slices.Contains(idsToDisable, sortedIds[i]){
 				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnManualRec
 			}
 			// ... button state logic ...
-			idsToDisable = nil
 		}
+		idsToDisable = nil
 	}
webrtc/peer_manager.go (5)

108-133: TURN URL parsing assumes .com TLD.

The hardcoded .com check will fail for servers using other TLDs (.org, .io, .net, country codes, etc.), producing incorrect or empty names.

Consider using net/url for proper parsing or at minimum handle the missing .com case more gracefully.


331-336: GetAllPeersStats can panic on nil peer.connection.

Peers that are still starting up have connection == nil. The loop dereferences p.connection without a nil check, which will panic when the debug UI requests stats.

 	for id,p := range(p.peers) {
+		if p == nil || p.connection == nil {
+			continue
+		}
 		idAsString := fmt.Sprintf("%d", id)
 		allPeersStats[idAsString] = p.connection.GetStats()
 		connectionStates[idAsString] = p.connection.ConnectionState().String()
 		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
 	}

645-646: Global watchers map risks cross-session contamination.

The watchers map is package-level and is not cleared when a new PeerManager is created. If the adapter restarts or multiple games run, stale entries could prevent new watchers from being created for the same player IDs.

Consider making watchers an instance field of PeerManager or clearing it in the constructor.


678-685: initialConnectionWatcher reads p.peers without holding peersMu.

This causes a data race with goroutines that modify p.peers (e.g., addPeerIfMissing, removePeer). Additionally, peer.connection may be nil and is dereferenced without checking.

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
+		p.peersMu.Lock()
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
+		peer, ok := p.peers[playerId]
+		var state webrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == webrtc.PeerConnectionStateConnected {
 				peerConnnectedSecondsTotal += 1
-			}
 		}

827-829: Modulo logic can yield 0 and cause out-of-range index panic.

When numOfReconns is a multiple of totalCombinations (e.g., 8 when there are 4 combinations), the normalization produces numOfReconns == 0. This then causes p.turnServer[numOfReconns - 1] to index at -1, panicking.

Use 1-based modulo:

 		if numOfReconns > totalCombinations {
-			numOfReconns = numOfReconns - totalCombinations * (numOfReconns / totalCombinations)
+			// Keep numOfReconns in [1, totalCombinations]
+			numOfReconns = ((numOfReconns - 1) % totalCombinations) + 1
 		}
🧹 Nitpick comments (3)
adapter/adapter.go (2)

77-81: Dead code: backoff multiplier never executes.

The condition backoff < 0*time.Second is always false since backoff starts at time.Second (positive) and is never decremented. The backoff *= 2 on line 80 will never run.

If the intent is to keep a fixed 1-second delay (as the comment suggests), remove the dead code:

 			case <-time.After(backoff):
-				// Better don't increase delay and just use fixed 1 sec value
-				// Might be critical in some situations when you want to restore conn as fast as possible
-				if backoff < 0*time.Second {
-					backoff *= 2
-				}
+				// Using fixed 1 sec delay for fast reconnection
 			}

155-177: Nickname storage occurs inside the loop, potentially overwriting on each iteration.

The if playerId != "" && nickname != "" check and storage happens inside the for i, item := range loop. Once both values are set, subsequent iterations still execute the storage block repeatedly. Consider breaking out of the loop or moving the storage after the loop:

 							if i == 1 {
 								nickname = v
 							}
 						}
-						if playerId != "" && nickname != "" {
-							mu.Lock()
-							userNicknames[playerId] = nickname
-							mu.Unlock()
-						}
 					}
+					if playerId != "" && nickname != "" {
+						mu.Lock()
+						userNicknames[playerId] = nickname
+						mu.Unlock()
+					}
 				}
dbgwnd/dbgwnd.go (1)

25-50: Global mutable state creates tight coupling and testing difficulties.

All UI state is held in package-level variables. This makes the module hard to test and could cause issues if the debug window is ever closed and reopened.

Consider encapsulating state in a struct for cleaner lifecycle management, though this may be acceptable for a debug-only tool.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e4f647 and 35086c7.

📒 Files selected for processing (3)
  • adapter/adapter.go (5 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
dbgwnd/dbgwnd.go (4)
adapter/adapter.go (3)
  • PeerManager (20-20)
  • GetPeerManager (211-213)
  • GetNicknames (215-223)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
moho/packet.go (1)
  • State (28-28)
applog/applog.go (1)
  • GetLogFilePath (375-377)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (4)
adapter/adapter.go (1)

215-223: GetNicknames now correctly returns a snapshot copy.

The implementation now creates a copy of the map while holding the mutex, addressing the previous review concern about exposing the live map.

webrtc/peer_manager.go (3)

748-775: Mutex is now properly unlocked on all paths.

The HandleManualReconnectRequest function correctly unlocks peersMu in all branches (lines 762, 767, 772), addressing the previous review concern.


793-811: Mutex handling in handleRemoteManualReconnRequest is now correct.

All code paths properly unlock peersMu (lines 800, 806, 809), fixing the previously flagged issue.


813-870: preparePeerForManualReconn now manages its own mutex correctly.

The function acquires and releases peersMu internally (lines 815, 869), which is safe since callers now release the lock before calling this function.

Comment thread dbgwnd/dbgwnd.go
Comment thread webrtc/peer_manager.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
webrtc/peer_manager.go (5)

321-339: Critical: Unguarded nil peer.connection dereference.

The past review comment about guarding against nil peer.connection was not addressed. At lines 333-334, the code unconditionally calls p.connection.GetStats() and p.connection.ConnectionState() without checking if peer or peer.connection is nil. Peers still starting up or closing can have nil connections, causing a panic.

Apply this fix:

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int, []string) {
 	p.peersMu.Lock()
 	defer p.peersMu.Unlock()
 
 	allPeersStats := make(map[string]webrtc.StatsReport)
 	connectionStates := make(map[string]string)
 	turnIds := make(map[string][]int)
 	idsToDisable := p.disableRecBtnPeers
 	p.disableRecBtnPeers = nil
 
-	for id,p := range(p.peers) {
+	for id, peer := range p.peers {
+		if peer == nil || peer.connection == nil {
+			continue
+		}
+		
 		idAsString := fmt.Sprintf("%d", id)
-		allPeersStats[idAsString] = p.connection.GetStats()
-		connectionStates[idAsString] = p.connection.ConnectionState().String()
-		turnIds[idAsString] = []int{p.specTurnIdLocal, p.specTurnIdRemote}
+		allPeersStats[idAsString] = peer.connection.GetStats()
+		connectionStates[idAsString] = peer.connection.ConnectionState().String()
+		turnIds[idAsString] = []int{peer.specTurnIdLocal, peer.specTurnIdRemote}
 	}
 
 	return allPeersStats, connectionStates, turnIds, idsToDisable
 }

21-22: Global TURN state still accumulates across restarts.

The past review comment about clearing allTurnServersUrls and turnsNameById at the start of saveTurnsURLs() was not addressed. If the adapter restarts within the same process, these package-level variables will retain old entries and append duplicates.

Apply this fix at the start of saveTurnsURLs():

 func (p *PeerManager) saveTurnsURLs() {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
 	for i, _ := range(p.turnServer) {

649-725: Critical: Race condition and nil dereference in initialConnectionWatcher.

The past review comment about the race condition on p.peers and nil peer.connection dereference was not addressed. At lines 685-688, the code reads p.peers[playerId] without holding peersMu (causing concurrent map access races with other goroutines that modify peers), and calls peer.connection.ConnectionState() without checking for nil (which will panic during peer setup or closure).

Apply this fix:

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var state webrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == webrtc.PeerConnectionStateConnected {
+			peerConnnectedSecondsTotal++
+		}

649-650: Global watchers map risks cross-session contamination.

The past review comment about moving watchers to be an instance field of PeerManager was not addressed. These package-level variables persist across adapter restarts or multiple games, causing stale entries to block new watchers for the same player IDs.

Consider making watchers an instance field:

 type PeerManager struct {
 	// ... existing fields ...
+	watchers      map[uint]bool
+	watcherMutex  sync.Mutex
 }

Then initialize in NewPeerManager:

 	peerManager := PeerManager{
 		// ... existing initialization ...
+		watchers:             make(map[uint]bool),
 	}

And update all references to use p.watchers and p.watcherMutex instead of the global variables.


213-217: Mutex inconsistency for reconnection flags persists.

The past review comment about inconsistent mutex usage was marked as addressed, but the issue remains. At lines 213-216, peer.manualReconnIsActive and peer.remoteManualRRequest are modified under reconnectMu, but at lines 758 and 802 in HandleManualReconnectRequest and handleRemoteManualReconnRequest, these same fields are accessed under peersMu. This creates a race condition.

Consistently use peersMu for these fields. Apply this fix:

 	} else {
 		applog.Info("Connecting to peer with specific TURN", zap.Uint("playerId", playerId))
 		if err := peer.ConnectOnce(peer.peerSpecificTurn); err != nil {
 			applog.Error("Peer connection with specific TURN failed", zap.Uint("peer", playerId), zap.Error(err))
 		}
-		peer.reconnectMu.Lock()
+		p.peersMu.Lock()
 		peer.manualReconnIsActive = false
 		peer.remoteManualRRequest = false
-		peer.reconnectMu.Unlock()
+		p.peersMu.Unlock()
 	}
🧹 Nitpick comments (5)
webrtc/peer_manager.go (5)

752-779: Approve explicit mutex unlocks, but defer would be safer.

The explicit Unlock() calls at lines 766, 771, and 776 correctly handle all code paths, addressing the past critical mutex deadlock issue. However, the current pattern is error-prone if future modifications add early returns.

Consider using defer for more robust cleanup:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
 	peerAsString := fmt.Sprintf("%d", playerId)
 	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
+	
 	peer, ok := p.peers[playerId]
 	if ok {
 		if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
 			peer.manualReconnIsActive = true
 			p.disableRecBtnPeers = append(p.disableRecBtnPeers, peerAsString)
 			
 			manualReconnectionMessage.BaseEvent.RecipientID = &playerId
+			p.peersMu.Unlock()  // Explicitly unlock before calling preparePeerForManualReconn
+			
 			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
 			if err != nil {
 				applog.Error("Failed to send manualReconnectionMessage to peer " + peerAsString, zap.Error(err))
 			}
-			p.peersMu.Unlock()
 			
 			p.preparePeerForManualReconn(playerId)
 			go p.scheduleManualReconnection(playerId)
-		} else {
-			p.peersMu.Unlock()
-			applog.Debug("Manual reconnection is already in process. Skipping local request. Peer: " + peerAsString)
 		}
-	} else {
-		applog.Error("Can't initiate manual reconnection: No such peer " + peerAsString)
-		p.peersMu.Unlock()
 	}
-	
 }

Note: With this pattern, you'd need to ensure preparePeerForManualReconn has its own locking since the defer will re-unlock when the function returns (which would be a double-unlock without the explicit unlock before the call).


797-815: Explicit unlocks correctly handle all paths.

The explicit Unlock() calls at lines 804, 810, and 813 correctly address all code paths, resolving the past critical mutex issue. The implementation is correct.

As with HandleManualReconnectRequest, consider using defer for more maintainable code.


367-381: Time-based reconnection guard is a documented workaround.

The 120-second guard before scheduling reconnection (lines 369-381) prevents interference with initial connection attempts. This is explicitly documented as a workaround alongside initialConnectionWatcher. While functional, this indicates the underlying connection startup race should be properly resolved in the future.

Consider investigating and fixing the root cause of the initial connection race so these workarounds can be removed.


256-263: Empty candidates as reconnection signal is a protocol hack.

The code at lines 258-263 uses an empty Candidates array as an implicit signal for manual reconnection requests. This is explicitly documented as "a dirty hook" (line 257) and should be replaced with a proper message type.

Add a dedicated EventKindManualReconnect message type to icebreaker/events.go and update both the sender and receiver to use explicit message types rather than overloading the CandidatesMessage semantics.


727-750: Non-functional feature included in codebase.

The HandleManualNoRelayReconnRequest method is documented as not working (line 730: "Doesn't work"). Including non-functional code in the codebase increases maintenance burden and confusion.

Either remove this method entirely if it's confirmed non-functional, or if it's experimental/work-in-progress, add a clear TODO comment explaining what needs to be fixed and consider returning an error to callers indicating the feature is not yet implemented.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 35086c7 and 1544f94.

📒 Files selected for processing (1)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (4)
webrtc/peer_manager.go (4)

552-582: Baseline message correctly updated before each send.

The initialization of manualReconnectionMessage as a baseline template is correct, and line 761 in HandleManualReconnectRequest properly updates the RecipientID before each send, addressing the past issue about stale recipients.


164-220: Clear separation of normal and manual reconnection paths.

The forceReconnect parameter and the conditional logic at lines 175-181 and 184-217 cleanly separate forced/manual reconnection from normal reconnection. The TURN ID reset at lines 191-192 for normal reconnection is appropriately documented.


454-484: State change handling correctly guards against concurrent reconnection.

The guard at line 455 that checks !peer.manualReconnIsActive prevents the automatic reconnection logic from interfering with manual reconnection flows. The logic is sound.

However, this depends on the manualReconnIsActive flag being correctly synchronized (see the mutex inconsistency issue at lines 213-217). Ensure that issue is resolved to guarantee this guard works reliably across goroutines.


817-875: Index calculations are mathematically sound and produce no out-of-bounds indices.

The verification confirms that the complex TURN server index calculations at lines 847-858 are correct. Testing across 2, 3, and 4 TURN server configurations with multiple reconnection attempts shows all leader and secondary server indices remain within valid bounds. The modulo normalization at line 832 correctly cycles through the valid range, and the mixed-server calculation logic properly maps all theoretical combinations to actual server indices. No edge cases produce invalid indices.

Comment thread webrtc/peer_manager.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
adapter/adapter.go (1)

20-22: Global PeerManager access is still racy between adapter startup and the debug UI

PeerManager is assigned in Start() (Line 138) and read via GetPeerManager() (Lines 218-220) without any synchronization. Since Start() runs in its own goroutine while the debug UI calls GetPeerManager() concurrently, this is a Go data race even though pointer assignment is atomic, and it can yield undefined behavior.

Either avoid the package-level global and inject *webrtc.PeerManager into the UI, or guard the global with synchronization (e.g. an RWMutex or atomic.Value), for example:

@@
-import (
+import (
@@
-	"time"
-	"sync"
+	"time"
+	"sync"
 )
@@
-var PeerManager     *webrtc.PeerManager
+var (
+	peerManagerMu sync.RWMutex
+	peerManager   *webrtc.PeerManager
+)
@@
-	peerManager := webrtc.NewPeerManager(
+	pm := webrtc.NewPeerManager(
@@
-	PeerManager = peerManager
+	peerManagerMu.Lock()
+	peerManager = pm
+	peerManagerMu.Unlock()
@@
-	peerManager.Start()
+	pm.Start()
@@
 func GetPeerManager() *webrtc.PeerManager {
-	return PeerManager
+	peerManagerMu.RLock()
+	defer peerManagerMu.RUnlock()
+	return peerManager
 }

Please double-check call sites to ensure GetPeerManager() is never used before initialization or from unexpected goroutines after applying the synchronization.

Also applies to: 138-139, 218-220

🧹 Nitpick comments (4)
adapter/adapter.go (4)

77-82: Backoff condition is effectively dead code; simplify for clarity

backoff is initialized to time.Second and never made negative, so the condition if backoff < 0*time.Second { backoff *= 2 } will never be true. The current effect is a fixed 1s delay, which matches the comment but makes the condition misleading.

You can simplify to make intent explicit, e.g.:

-		backoff := time.Second
+		const backoff = time.Second
@@
-			case <-time.After(backoff):
-				// Better don't increase delay and just use fixed 1 sec value
-				// Might be critical in some situations when you want to restore conn as fast as possible
-				if backoff < 0*time.Second {
-					backoff *= 2
-				}
+			case <-time.After(backoff):
+				// Fixed 1s delay to reconnect as fast as possible
 			}

Verify that no previous behavior depended on exponential backoff before this change.


87-96: Filtering a TURN server with continue leaves zero-value entries and can mask “no usable servers”

When you continue on a filtered server (Lines 89-95), the corresponding slot in turnServer := make([]pionwebrtc.ICEServer, len(sessionGameResponse.Servers)) is left as a zero-value ICEServer. This means:

  • You may pass entries with empty URLs to NewPeerManager.
  • If all servers are filtered (e.g. only the hard-coded IP is present), len(turnServer) is still non-zero even though you effectively have no usable TURN/STUN servers, and you never log that situation.

Consider building turnServer with append and tracking only allowed servers, and explicitly logging when the filtered list is empty, e.g.:

-	turnServer := make([]pionwebrtc.ICEServer, len(sessionGameResponse.Servers))
-	for i, server := range sessionGameResponse.Servers {
+	var turnServer []pionwebrtc.ICEServer
+	for _, server := range sessionGameResponse.Servers {
 		// Hardcoded until we remove FAF's cotrun from the list
 		if len(server.Urls) > 0 {
 			if strings.Contains(server.Urls[0], "139.162.142.250") {
 				continue
 			}
 		}
 
-		turnServer[i] = pionwebrtc.ICEServer{
+		ts := pionwebrtc.ICEServer{
@@
-		for j, url := range server.Urls {
+		for j, url := range server.Urls {
 			// for Java being Java reasons we unfortunately raped the URLs and need to convert it back.
-			turnServer[i].URLs[j] = strings.ReplaceAll(url, "://", ":")
+			ts.URLs[j] = strings.ReplaceAll(url, "://", ":")
 		}
+		turnServer = append(turnServer, ts)
 	}
+
+	if len(turnServer) == 0 {
+		applog.Error("No usable stun/turn servers after filtering (all filtered or misconfigured)")
+	}

Confirm that pion/webrtc tolerates zero-value ICEServer entries today and that you won’t rely on their behavior if this remains unrefactored.


162-184: Tighten nickname extraction loop and minimize repeated locking

The nickname caching logic works, but it does extra work:

  • Once both playerId and nickname are found, mu.Lock() / mu.Unlock() and the map write still run for every remaining arg in the loop.
  • The reliance on positional indices (i == 1 for nickname, i == 2 for playerId) is implicit; a brief comment would help future readers.

You can simplify and lock only once, after the loop:

-						if cmd == "JoinGame" || cmd == "ConnectToPeer"{
+						if cmd == "JoinGame" || cmd == "ConnectToPeer" {
 							nickname := ""
 							playerId := ""
 							for i, item := range parsedMsg.GetArgs() {
@@
 								case string:
 									if i == 1 {
 										nickname = v
 									}
 								}
-								if playerId != "" && nickname != "" {
-									mu.Lock()
-									userNicknames[playerId] = nickname
-									mu.Unlock()
-								}
 							}
+							// Args layout: [cmd, nickname, playerId, ...]
+							if playerId != "" && nickname != "" {
+								mu.Lock()
+								userNicknames[playerId] = nickname
+								mu.Unlock()
+							}
 						}

This keeps behavior the same but avoids repeated locking and clarifies the assumed arg ordering.

Please confirm the arg positions for JoinGame / ConnectToPeer are stable and documented somewhere, or add a reference comment if they are.


222-230: GetNicknames snapshot approach looks correct; consider minor style tweak

GetNicknames now copies userNicknames under mu and returns the copy, which resolves the earlier map race against concurrent writers. The logic is sound.

For minor style/robustness, you could use defer for the unlock:

 func GetNicknames() map[string]string {
-	mu.Lock()
-	nnames  := make(map[string]string, len(userNicknames))
+	mu.Lock()
+	defer mu.Unlock()
+
+	nnames := make(map[string]string, len(userNicknames))
 	for k, v := range userNicknames {
-        nnames[k] = v
-    }
-	mu.Unlock()
+		nnames[k] = v
+	}
 	return nnames
 }

Run with -race when exercising the debug UI nickname view to confirm there are no remaining map races.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1544f94 and 201e0b5.

📒 Files selected for processing (1)
  • adapter/adapter.go (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webrtc/peer_manager.go (1)

164-220: Manual reconnection flags are not consistently synchronized, leading to data races

manualReconnIsActive and remoteManualRRequest are read and written under different or no locks:

  • handleReconnection (lines 164–220) reads both flags and peer.connection after releasing peersMu.
  • onPeerStateChanged (451–487) reads peer.manualReconnIsActive without any lock.
  • HandleManualReconnectRequest, HandleManualNoRelayReconnRequest, handleRemoteManualReconnRequest, and preparePeerForManualReconn update these fields under peersMu or reconnectMu.

This inconsistent locking introduces races on the same fields and on peer.connection state, especially under heavy reconnect churn or manual reconnect spam.

  • Decide on a single mutex (preferably peersMu) that protects manualReconnIsActive, remoteManualRRequest, and any associated peer TURN-selection fields.
  • In handleReconnection, keep peersMu long enough to snapshot the relevant booleans and connection state into locals, then operate on those locals after unlocking:
 func (p *PeerManager) handleReconnection(playerId uint, forceReconnect bool) {
@@
-	p.peersMu.Lock()
-	peer, ok := p.peers[playerId]
-	if !ok || peer.IsDisabled() {
-		p.peersMu.Unlock()
-		return
-	}
-	p.peersMu.Unlock()
-
-	if !forceReconnect && !peer.manualReconnIsActive && !peer.remoteManualRRequest{
-		if peer.IsActive() || (peer.connection != nil &&
-			peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnecting) {
+	p.peersMu.Lock()
+	peer, ok := p.peers[playerId]
+	if !ok || peer.IsDisabled() {
+		p.peersMu.Unlock()
+		return
+	}
+	manualReconn := peer.manualReconnIsActive
+	remoteManual := peer.remoteManualRRequest
+	isActive := peer.IsActive()
+	conn := peer.connection
+	p.peersMu.Unlock()
+
+	if !forceReconnect && !manualReconn && !remoteManual {
+		if isActive || (conn != nil &&
+			conn.ConnectionState() == webrtc.PeerConnectionStateConnecting) {
 			applog.Info("Peer already active/connecting, skipping reconnection", zap.Uint("peer", playerId))
 			return
 		}
 	}
  • In onPeerStateChanged and the manual recon handlers, always read or modify these flags while holding the same mutex, and avoid mutating them under reconnectMu if you standardize on peersMu.

This will remove races on these flags and on peer.connection usage.

Also applies to: 451-487, 758-785, 803-821, 823-881

♻️ Duplicate comments (7)
dbgwnd/dbgwnd.go (5)

103-111: Fix closure capture so each RECONNECT button uses its own index

The Command(func() { reconnectBtnClicked(i) }) closure captures i by reference, so all buttons will call reconnectBtnClicked with the final loop value. Capture the index into a new local per iteration.

-	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+	for i := 0; i < 15; i++ {
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 		reconnectButtons = append(reconnectButtons, but)

147-152: Checkbutton value handling is broken; Variable() never equals "true"

Tk checkbuttons with Onvalue(true), Offvalue(false) end up using "1"/"0" (and here Variable is also wired incorrectly), so the comparisons against "true" never succeed and the toggles don't work.

-	rawLogsChkButton = logFrame.Checkbutton(Txt("Show unformatted logs"), Variable(rawLogsChButtonVar), 
-	Onvalue(true), Offvalue(false), Command(func() { rawLogsChkButtonPressed() }))
-
-	hideDebugChkButton = logFrame.Checkbutton(Txt("Hide DEBUG"), Variable(hideDebugButtonVar), 
-	Onvalue(true), Offvalue(false), Command(func() { hideDebugChkButtonPressed() }))
+	rawLogsChkButton = logFrame.Checkbutton(
+		Txt("Show unformatted logs"),
+		Variable(&rawLogsChButtonVar),
+		Onvalue(true),
+		Offvalue(false),
+		Command(func() { rawLogsChkButtonPressed() }),
+	)
+
+	hideDebugChkButton = logFrame.Checkbutton(
+		Txt("Hide DEBUG"),
+		Variable(&hideDebugButtonVar),
+		Onvalue(true),
+		Offvalue(false),
+		Command(func() { hideDebugChkButtonPressed() }),
+	)
@@
 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
-		showRawLogs = true
-	} else {
-		showRawLogs = false
-	}
+	showRawLogs = rawLogsChButtonVar
 }
 
 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
-		hideDebug = true
-	} else {
-		hideDebug = false
-	}
+	hideDebug = hideDebugButtonVar
 }

Also applies to: 199-215


336-364: Blank/short log lines stop processing future logs

Using break on len(st) < 5 means the first empty/short entry (e.g. final split from strings.Split) permanently stops log ingestion; later lines never reach the UI.

-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}
@@
-			if len(st) < 5 {
-				break
-			}
+			if len(st) < 5 {
+				continue
+			}

582-645: Reconnect button state book‑keeping is wrong; idsToDisable cleared too early

Two issues here:

  • reconnectButtonsEnabled is incremented inside the loop, so the guard != numOfIds never stabilizes.
  • idsToDisable = nil inside the for loop clears the slice after the first ID, so only the first matching peer gets its timers set.
 func refreshButtons() {
 	numOfIds := len(sortedIds)
 
-	if reconnectButtonsEnabled != numOfIds {
-		// normal buttons
-		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
-			if i < numOfIds {
-				b.Configure((State("normal")))
-			} else {
-				b.Configure((State("disabled")))
-			}
-		}
+	if reconnectButtonsEnabled != numOfIds {
+		// normal buttons
+		for i, b := range reconnectButtons {
+			if i < numOfIds {
+				b.Configure(State("normal"))
+			} else {
+				b.Configure(State("disabled"))
+			}
+		}
+		reconnectButtonsEnabled = numOfIds
 	}
@@
-	if len(idsToDisable) > 0 || len(disabledBtnsTimers) > 0 {
-		for i,_ := range(disabledBtnsTimers) {
-			if disabledBtnsTimers[i] > 0 {
-				disabledBtnsTimers[i] -= 1
+	if len(idsToDisable) > 0 || len(disabledBtnsTimers) > 0 {
+		for id := range disabledBtnsTimers {
+			if disabledBtnsTimers[id] > 0 {
+				disabledBtnsTimers[id] -= 1
 			} else {
-				delete(disabledBtnsTimers, i)
+				delete(disabledBtnsTimers, id)
 			}
 		}
 
-		for i,_ := range(sortedIds) {
-			if slices.Contains(idsToDisable, sortedIds[i]){
-				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnManualRec
+		for i := range sortedIds {
+			id := sortedIds[i]
+			if slices.Contains(idsToDisable, id) {
+				disabledBtnsTimers[id] = disableRecButnOnManualRec
 			}
 
 			// normal buttons
-			if _, ok := disabledBtnsTimers[sortedIds[i]]; ok {
+			if _, ok := disabledBtnsTimers[id]; ok {
 				reconnectButtons[i].Configure((State("disabled")))
 			} else {
 				reconnectButtons[i].Configure((State("normal")))
 			}
-
-			idsToDisable = nil
 		}
+		idsToDisable = nil
 	}
 }

217-243: Handle Atoi errors instead of silently treating bad IDs as 0

Both reconnect handlers ignore strconv.Atoi errors and may end up calling reconnection APIs with player ID 0 if parsing ever fails.

 func reconnNoRelaytBtnClicked(buttonID int){
 	if len(sortedIds) > buttonID{
 		if pm != nil {
@@
-			if answer == "yes"{
-				playerID, _ := strconv.Atoi(sortedIds[buttonID])
-				pm.HandleManualNoRelayReconnRequest(uint(playerID))
-			}
+			if answer == "yes"{
+				playerID, err := strconv.Atoi(sortedIds[buttonID])
+				if err != nil {
+					return
+				}
+				pm.HandleManualNoRelayReconnRequest(uint(playerID))
+			}
@@
 func reconnectBtnClicked(buttonID int){
 	if len(sortedIds) > buttonID{
 		if pm != nil {
-			playerID, _ := strconv.Atoi(sortedIds[buttonID])
-			pm.HandleManualReconnectRequest(uint(playerID))
+			playerID, err := strconv.Atoi(sortedIds[buttonID])
+			if err != nil {
+				return
+			}
+			pm.HandleManualReconnectRequest(uint(playerID))
 		}
 	}
 }
webrtc/peer_manager.go (2)

21-23: Harden TURN URL parsing and reset global TURN caches per PeerManager

saveTurnsURLs currently assumes .com and does manual slicing that can panic (e.g. "turn:a.com" makes preparedName shorter than 8, so preparedName[len(preparedName)-8:] underflows). It also never clears allTurnServersUrls/turnsNameById, so multiple NewPeerManager instances accumulate stale/duplicate entries.

-import (
-	"context"
-	"faf-pioneer/applog"
-	"faf-pioneer/gpgnet"
-	"faf-pioneer/icebreaker"
-	"faf-pioneer/launcher"
-	"faf-pioneer/util"
-	"github.com/pion/webrtc/v4"
-	"go.uber.org/zap"
-	"net"
-	"sync"
-	"time"
-	"fmt"
-	"strconv"
-	"math"
-	"strings"
-)
+import (
+	"context"
+	"faf-pioneer/applog"
+	"faf-pioneer/gpgnet"
+	"faf-pioneer/icebreaker"
+	"faf-pioneer/launcher"
+	"faf-pioneer/util"
+	"github.com/pion/webrtc/v4"
+	"go.uber.org/zap"
+	"net"
+	"net/url"
+	"sync"
+	"time"
+	"fmt"
+	"strconv"
+	"math"
+	"strings"
+)
@@
-func (p *PeerManager) saveTurnsURLs() {
-	for i, _ := range(p.turnServer) {
-		for _,url := range(p.turnServer[i].URLs) {
-			_, ok := turnsNameById[i+1]
-			if !ok {
-				preparedName := " "
-				index := strings.Index(url, ".com")
-
-				if index != -1 {
-					preparedName = url[:index]
-					index = strings.LastIndex(preparedName, ".")
-					if index != -1 {
-						preparedName = preparedName[index+1:]
-						if len(preparedName) > 6 {
-							preparedName = preparedName[:6]
-						}
-					} else {
-						preparedName = preparedName[len(preparedName) - 8:]
-					}
-				}
-				turnsNameById[i+1] = preparedName
-			}
-			allTurnServersUrls = append(allTurnServersUrls, url)
-		}
-	}
-}
+func (p *PeerManager) saveTurnsURLs() {
+	// Reset global TURN state per manager instance.
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
+
+	for i := range p.turnServer {
+		for _, urlStr := range p.turnServer[i].URLs {
+			if _, ok := turnsNameById[i+1]; !ok {
+				name := "unknown"
+				if u, err := url.Parse(urlStr); err == nil && u.Host != "" {
+					host := strings.Split(u.Host, ":")[0]
+					parts := strings.Split(host, ".")
+					if len(parts) >= 2 {
+						name = parts[len(parts)-2]
+					} else if len(parts) == 1 {
+						name = parts[0]
+					}
+					if len(name) > 6 {
+						name = name[:6]
+					}
+				}
+				turnsNameById[i+1] = name
+			}
+			allTurnServersUrls = append(allTurnServersUrls, urlStr)
+		}
+	}
+}

Also applies to: 108-133, 353-355


653-731: initialConnectionWatcher can still nil‑deref peer.connection

You now guard access with peersMu, but peer.connection can still be nil for peers that are starting up or already torn down, so peer.connection.ConnectionState() can panic.

-	for i := 0; i < keepAliveSec; i++ {
-		time.Sleep(time.Second)
-		
-		p.peersMu.Lock()
-		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
-				peerConnnectedSecondsTotal += 1
-			}
-		}
-		p.peersMu.Unlock()
+	for i := 0; i < keepAliveSec; i++ {
+		time.Sleep(time.Second)
+
+		p.peersMu.Lock()
+		peer, ok := p.peers[playerId]
+		var state webrtc.PeerConnectionState
+		if ok && peer != nil && peer.connection != nil {
+			state = peer.connection.ConnectionState()
+		}
+		p.peersMu.Unlock()
+
+		if state == webrtc.PeerConnectionStateConnected {
+			peerConnnectedSecondsTotal++
+		}
🧹 Nitpick comments (3)
webrtc/peer_manager.go (3)

321-343: Holding peersMu across GetStats may block other operations

GetAllPeersStats holds peersMu while calling peer.connection.GetStats() for every peer. If GetStats is slow or blocks internally, it will stall any other goroutine that needs peersMu (peer add/remove, reconnections).

Consider snapshotting the peer pointers under the lock, then calling GetStats after unlocking:

 func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int, []string) {
-	p.peersMu.Lock()
-	defer p.peersMu.Unlock()
-
-	allPeersStats := make(map[string]webrtc.StatsReport)
-	connectionStates := make(map[string]string)
-	turnIds := make(map[string][]int)
-	idsToDisable := p.disableRecBtnPeers
-	p.disableRecBtnPeers = nil
-
-	for id,peer := range(p.peers) {
+	p.peersMu.Lock()
+	snapshot := make(map[uint]*Peer, len(p.peers))
+	for id, peer := range p.peers {
+		snapshot[id] = peer
+	}
+	idsToDisable := p.disableRecBtnPeers
+	p.disableRecBtnPeers = nil
+	p.peersMu.Unlock()
+
+	allPeersStats := make(map[string]webrtc.StatsReport)
+	connectionStates := make(map[string]string)
+	turnIds := make(map[string][]int)
+
+	for id, peer := range snapshot {
 		if peer == nil || peer.connection == nil{
 			continue
 		}
@@
 	return allPeersStats, connectionStates, turnIds, idsToDisable
 }

371-385: Reconnection skip-on-start logic is reasonable but deserves a guardrail

The “don’t reconnect within the first 120s” workaround avoids fighting initial connection attempts, but it’s hard-coded and silently skips reconnection. If this ever misfires, debugging will be painful.

  • Consider making the 120 seconds threshold a named constant with a brief comment, or log the exact creation time and threshold so it’s obvious in logs why reconnection was skipped.

556-587: Global manualReconnectionMessage is a brittle protocol hack

Using a single package-global manualReconnectionMessage as a template, then mutating RecipientID before each send, is fragile and non-obvious, especially if you ever support multiple concurrent manual reconnects.

  • Instead of a global, build a fresh empty-candidates message per request in HandleManualReconnectRequest (and drop manualReconnectionMessage entirely), using the same BaseEvent fields and RecipientID for the target peer.
  • Leave onPeerCandidatesGathered to only send real candidates and not manage manual recon state; keep the “len(candidates)==0 → manual recon” hook only on the receiving side.

This will make the manual recon protocol easier to reason about and safer to evolve.

Also applies to: 758-776, 803-815

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 201e0b5 and 382a408.

📒 Files selected for processing (2)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
dbgwnd/dbgwnd.go (4)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
adapter/adapter.go (3)
  • PeerManager (20-20)
  • GetPeerManager (218-220)
  • GetNicknames (222-230)
moho/packet.go (1)
  • State (28-28)
applog/applog.go (1)
  • GetLogFilePath (375-377)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)

Comment thread dbgwnd/dbgwnd.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (10)
adapter/adapter.go (1)

231-233: Global PeerManager access is racy.

This was flagged in a past review and remains unaddressed. PeerManager is written in Start() and read via GetPeerManager() without synchronization.

dbgwnd/dbgwnd.go (5)

109-116: Closure captures loop variable by reference — all buttons trigger with i=15.

This was flagged in a past review and remains unaddressed. All 15 buttons will call reconnectBtnClicked(15).

 	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 	}

205-221: Checkbutton Variable() comparison uses wrong string value.

This was flagged in a past review. Tcl/Tk stores boolean as "1"/"0", not "true"/"false". The comparisons at lines 207 and 216 will always be false.

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }

 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

242-249: Strconv error is silently ignored.

This was flagged in a past review. If strconv.Atoi fails, playerID will be 0, triggering unintended behavior.


251-268: Listbox selection uses only the first character of the ID.

This was flagged in a past review. selectedId[0] indexes a single byte, not the full string.

 func infoListIdSelected(){
 	selected := connIdListBox.Curselection()

 	if len(selected) > 0 {
 		selectedId := connIdListBox.Get( strconv.Itoa(selected[0]))
 		if len(selectedId) > 0 {
-			stat, ok := allPeersStats[selectedId[0]]
+			stat, ok := allPeersStats[selectedId]

 			if ok {
 				connInfoView.Clear()
-				title := "Connection ID: " + selectedId[0] + " Time: " + time.Now().Format("15:04:05")
+				title := "Connection ID: " + selectedId + " Time: " + time.Now().Format("15:04:05")

351-366: Don't bail out of log parsing on blank lines.

This was flagged in a past review. Using break instead of continue stops processing forever on the first blank entry.

 			if len(st) < 5 {
-				break
+				continue
 			}

Apply the same fix at line 364-366.

webrtc/peer_manager.go (4)

21-22: Global TURN state will persist across adapter restarts.

This was flagged in a past review. Consider clearing these at the start of saveTurnsURLs().


108-133: TURN URL parsing remains fragile with hardcoded .com assumption.

This was flagged in a past review. The code will fail for other TLDs (.org, .io, .net, etc.).


653-654: Global watchers map risks cross-session contamination.

This was flagged in a past review. Consider making watchers an instance field of PeerManager.


686-695: Potential nil pointer dereference on peer.connection.

While the mutex is now acquired (addressing the race), peer.connection can still be nil during peer setup. Line 691 calls peer.connection.ConnectionState() without a nil check.

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
 		p.peersMu.Lock()
 		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
+			if peer.connection != nil && peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnected {
 				peerConnnectedSecondsTotal += 1
 			}
 		}
 		p.peersMu.Unlock()
🧹 Nitpick comments (1)
adapter/adapter.go (1)

94-99: Hardcoded IP exclusion is fragile.

Filtering by hardcoded IP 139.162.142.250 will break if the server IP changes. Consider using a hostname or configuration-based approach.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d18839e and 3809034.

📒 Files selected for processing (3)
  • adapter/adapter.go (5 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
webrtc/peer_manager.go (2)
adapter/adapter.go (1)
  • PeerManager (20-20)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (7)
adapter/adapter.go (2)

235-243: GetNicknames correctly returns a snapshot copy.

The previous review concern about returning the live map has been addressed—this now returns a deep copy while holding the mutex.


175-196: The code snippet referenced does not exist in the repository.

The review comment references code at adapter/adapter.go lines 175-196, but adapter.go is only 175 lines long and ends with return nil. The nickname extraction logic with the userNicknames map, mutex operations, and the loop parsing both playerId and nickname from message arguments is not present anywhere in the current codebase.

This suggests the review was created for code that either has not been committed, was removed, or is located in a different file than specified.

Likely an incorrect or invalid review comment.

dbgwnd/dbgwnd.go (2)

551-560: Guard against short turnIds slices improved but still has edge case.

The length check len(tIds) > 1 at line 552 is good, but if tIds has exactly 1 element, the code skips both assignments. Consider checking len(tIds) > 0 for the first element access.

The fix addresses the past review concern. The current logic safely handles short slices.


653-654: idsToDisable = nil correctly moved outside the loop.

The past review concern has been addressed—the assignment is now after the loop completes.

webrtc/peer_manager.go (3)

758-785: Mutex handling in HandleManualReconnectRequest is now correct.

The previous review concerns about missing unlocks have been addressed—all code paths now properly release peersMu.


803-821: Mutex handling in handleRemoteManualReconnRequest is now correct.

All code paths properly release peersMu.


823-881: preparePeerForManualReconn mutex and modulo logic are now correct.

The modulo normalization ((numOfReconns - 1) % totalCombinations) + 1 correctly keeps values in [1, totalCombinations]. The mutex is acquired at the start and released at the end.

Comment thread adapter/adapter.go
Comment thread dbgwnd/dbgwnd.go
Comment thread webrtc/peer_manager.go
Comment on lines +767 to +768
manualReconnectionMessage.BaseEvent.RecipientID = &playerId
err := p.icebreakerClient.SendEvent(manualReconnectionMessage)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

manualReconnectionMessage is mutated in place, causing race conditions.

The global manualReconnectionMessage has its RecipientID modified at line 767, then sent at line 768. If two manual reconnections happen concurrently for different peers, the RecipientID can be overwritten before SendEvent is called.

Create a local copy instead of mutating the global:

-			manualReconnectionMessage.BaseEvent.RecipientID = &playerId
-			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+			msg := manualReconnectionMessage
+			msg.BaseEvent.RecipientID = &playerId
+			err := p.icebreakerClient.SendEvent(msg)
🤖 Prompt for AI Agents
In webrtc/peer_manager.go around lines 767-768, the global
manualReconnectionMessage is being mutated (its BaseEvent.RecipientID set) right
before SendEvent which creates a race if multiple reconnections run
concurrently; instead, make a local copy of manualReconnectionMessage (clone the
struct and its BaseEvent) and set RecipientID on that copy (use a new local
playerId variable and pointer) and pass the copy to p.icebreakerClient.SendEvent
so the global template is never modified.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (15)
dbgwnd/dbgwnd.go (7)

109-116: Closure captures loop variable by reference — all buttons will trigger with i=15.

The closure func() { reconnectBtnClicked(i) } captures i by reference. All 15 buttons will call reconnectBtnClicked(15).

Apply this fix:

 	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 	reconnectButtons = append(reconnectButtons, but)

205-221: Checkbutton Variable() returns "1"/"0", not "true"/"false".

Tcl/Tk stores boolean values as "1" and "0". The comparisons rawLogsChkButton.Variable() == "true" and hideDebugChkButton.Variable() == "true" will always fail.

 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }

 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

242-249: Strconv error is silently ignored.

If strconv.Atoi fails, playerID will be 0, which could trigger unintended behavior.

 func reconnectBtnClicked(buttonID int){
 	if len(sortedIds) > buttonID{
 		if pm != nil {
-			playerID, _ := strconv.Atoi(sortedIds[buttonID])
+			playerID, err := strconv.Atoi(sortedIds[buttonID])
+			if err != nil {
+				return
+			}
 			pm.HandleManualReconnectRequest(uint(playerID))
 		}
 	}
 }

251-268: Listbox selection uses only the first character of the ID.

selectedId[0] indexes a single byte of the string (e.g., "12"'1'), causing lookups and display for wrong IDs.

 func infoListIdSelected(){
 	selected := connIdListBox.Curselection()

 	if len(selected) > 0 {
 		selectedId := connIdListBox.Get( strconv.Itoa(selected[0]))
 		if len(selectedId) > 0 {
-			stat, ok := allPeersStats[selectedId[0]]
+			stat, ok := allPeersStats[selectedId]

 			if ok {
 				connInfoView.Clear()
-				title := "Connection ID: " + selectedId[0] + " Time: " + time.Now().Format("15:04:05")
+				title := "Connection ID: " + selectedId + " Time: " + time.Now().Format("15:04:05")

 				connInfoView.Insert(END,  title, "", "\n")
 				connInfoView.Insert(END,  godump.DumpJSONStr(stat) +" ", "", "\n")
 			}
 		}
 	}
 }

342-419: Don't bail out of log parsing on blank lines.

Every log line ends with a newline, so strings.Split yields empty strings. Because the loop breaks when len(st) < 5, processing stops permanently. Replace break with continue.

 		if len(st) < 5 {
-			break
+			continue
 		}

Apply this fix at both line 352 and line 365.


593-615: reconnectButtonsEnabled is incremented incorrectly.

The variable is incremented on every iteration instead of being set to the final count after the loop.

 func refreshButtons() {
 	numOfIds := len(sortedIds)

 	if reconnectButtonsEnabled != numOfIds {
 		// normal buttons
 		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds

634-655: idsToDisable = nil inside loop clears the slice prematurely.

Setting idsToDisable = nil on line 653 inside the loop clears it after the first iteration, preventing other IDs from being checked.

 		for i,_ := range(sortedIds) {
 			if slices.Contains(idsToDisable, sortedIds[i]){
 				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnManualRec
 			}

 			// normal buttons
 			if _, ok := disabledBtnsTimers[sortedIds[i]]; ok {
 				reconnectButtons[i].Configure((State("disabled")))
 			} else {
 				reconnectButtons[i].Configure((State("normal")))
 			}
-
-			idsToDisable = nil
 		}
+		idsToDisable = nil
 	}
 }
cmd/faf-adapter/main.go (1)

18-55: SIGINT no longer shuts the process down properly.

By moving startup into a goroutine while the main goroutine blocks in dbgwnd.CreateMainWindow(), signal handling is broken:

  1. Pressing Ctrl+C cancels ctx but the Tk main loop keeps running
  2. Closing the window doesn't call cancel(), so defers (applog.Shutdown) won't run
  3. os.Exit(0) at line 52 will abruptly terminate when the adapter finishes, potentially before the UI is closed

Consider restructuring so that:

  • The signal context is in the main goroutine
  • The UI loop can be interrupted when ctx.Done() fires
  • Window close triggers cancel() for graceful shutdown
adapter/adapter.go (2)

20-22: Global PeerManager is racy between adapter.Start and the debug UI.

PeerManager is written in Start() at line 151 and read from GetPeerManager() at line 232 without synchronization. This is a data race in Go's memory model.

Guard with a mutex or atomic.Value:

+var peerManagerMu sync.RWMutex
+
 func GetPeerManager() *webrtc.PeerManager {
-	return PeerManager
+	peerManagerMu.RLock()
+	defer peerManagerMu.RUnlock()
+	return PeerManager
 }

And wrap the assignment at line 151:

+	peerManagerMu.Lock()
 	PeerManager = peerManager
+	peerManagerMu.Unlock()

82-86: Dead code: backoff growth condition is always false.

The condition backoff < 0*time.Second can never be true since backoff is initialized to time.Second.

 			case <-time.After(backoff):
-				// Better don't increase delay and just use fixed 1 sec value
-				// Might be critical in some situations when you want to restore conn as fast as possible
-				if backoff < 0*time.Second {
-					backoff *= 2
-				}
+				// Use fixed 1 sec delay for faster reconnection
 			}
webrtc/peer_manager.go (5)

21-22: Global TURN state will persist across adapter restarts.

allTurnServersUrls and turnsNameById are package-level and accumulate data. If the adapter restarts, stale entries persist.

Clear these at the start of saveTurnsURLs():

 func (p *PeerManager) saveTurnsURLs() {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
 	for i, _ := range(p.turnServer) {

108-133: TURN URL parsing remains fragile with hardcoded .com assumption.

The code assumes .com TLD and will fail for .org, .io, .net, country codes, etc.

Consider using net/url for proper URL parsing as suggested in the past review.


686-695: initialConnectionWatcher can panic on nil peer.connection.

At line 691, peer.connection.ConnectionState() is called without checking if peer.connection is nil. The connection may not be initialized yet.

 	for i := 0; i < keepAliveSec; i++ {
 		time.Sleep(time.Second)
 		
 		p.peersMu.Lock()
 		if peer, ok := p.peers[playerId]; ok {
-			if peer.connection.ConnectionState().String() == "connected"{
+			if peer.connection != nil && peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnected {
 				peerConnnectedSecondsTotal += 1
 			}
 		}
 		p.peersMu.Unlock()

653-654: Global watchers map risks cross-session contamination.

The watchers map is package-level. If the adapter restarts, stale entries persist.

Consider making watchers an instance field of PeerManager or clearing it when a new PeerManager is created.


767-768: manualReconnectionMessage is mutated in place, causing race conditions.

The global message has its RecipientID modified at line 767, then sent at line 768. Concurrent manual reconnections can overwrite the recipient.

Create a local copy:

-		manualReconnectionMessage.BaseEvent.RecipientID = &playerId
-		err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+		msg := manualReconnectionMessage
+		msg.BaseEvent.RecipientID = &playerId
+		err := p.icebreakerClient.SendEvent(msg)
🧹 Nitpick comments (2)
webrtc/peer.go (1)

154-165: Duplicate IsDisabled() check in ConnectOnce.

Lines 155-157 and 159-162 both check IsDisabled() with the same effect. The second check with the "during reconnection" comment appears redundant since no reconnection has started yet at this point.

 func (p *Peer) ConnectOnce(iceServers []webrtc.ICEServer) error {
 	if p.IsDisabled() {
 		return errors.New("peer is disabled")
 	}

-	// If peed are disconnected/died/disabled while reconnecting, just gave up.
-	if p.IsDisabled() {
-		return errors.New("peer is disabled during reconnection")
-	}
-
 	return p.reconnect(iceServers)
 }
adapter/adapter.go (1)

94-99: Hardcoded IP filter for TURN server removal.

The comment indicates this is temporary until FAF's coturn is removed from the list. Consider adding a TODO or tracking issue reference.

Is there a tracking issue for removing this hardcoded filter?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3809034 and 6379b6d.

📒 Files selected for processing (5)
  • adapter/adapter.go (5 hunks)
  • cmd/faf-adapter/main.go (1 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer.go (3 hunks)
  • webrtc/peer_manager.go (15 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
webrtc/peer.go (2)
applog/context.go (1)
  • FromContext (10-12)
applog/applog.go (1)
  • Debug (110-123)
webrtc/peer_manager.go (2)
adapter/adapter.go (1)
  • PeerManager (20-20)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
dbgwnd/dbgwnd.go (4)
adapter/adapter.go (4)
  • PeerManager (20-20)
  • GetPeerManager (231-233)
  • GetRegion (245-247)
  • GetNicknames (235-243)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
moho/packet.go (1)
  • State (28-28)
applog/applog.go (1)
  • GetLogFilePath (375-377)
🔇 Additional comments (10)
dbgwnd/dbgwnd.go (1)

551-560: LGTM - turnIds slice length check added.

The length check if len(tIds) > 1 properly guards against short slices before accessing indices 0 and 1.

webrtc/peer.go (3)

49-56: New peer fields for manual reconnection support look good.

The new fields (creationTimeSeconds, numOfManualReconns, manualReconnIsActive, remoteManualRRequest, peerSpecificTurn, specTurnIdLocal, specTurnIdRemote) are appropriately scoped for tracking per-peer reconnection state.


143-149: Field initializations are correct.

The new fields are initialized with sensible defaults: creationTimeSeconds with current Unix time, and reconnection flags/counters to 0/false.


399-405: Log level change from Error to Debug is appropriate.

Send failures on the data channel can be transient (e.g., channel closing during shutdown). Using Debug level avoids log noise.

adapter/adapter.go (1)

235-243: LGTM - GetNicknames now returns a snapshot copy.

The function properly copies the map while holding the mutex, preventing race conditions.

webrtc/peer_manager.go (5)

86-86: forceTurnRelay is hardcoded to true.

This overrides the sessionInfo.MapForceTurnRelay configuration. If this is intentional for debugging, consider adding a comment. Otherwise, this should use the session configuration.

Is forcing TURN relay intentional for this debug window PR?


321-343: LGTM - GetAllPeersStats properly guards against nil peers/connections.

The nil checks at line 332-334 prevent panics when peers are still initializing.


758-785: LGTM - Mutex handling in HandleManualReconnectRequest is correct.

All code paths now properly release peersMu (lines 772, 777, 782).


803-821: LGTM - Mutex handling in handleRemoteManualReconnRequest is correct.

All code paths now properly release peersMu.


837-839: LGTM - Modulo normalization fix is correct.

The formula ((numOfReconns - 1) % totalCombinations) + 1 properly keeps numOfReconns in the range [1, totalCombinations], preventing the out-of-range index issue.

Comment thread dbgwnd/dbgwnd.go
Comment on lines +223 to +240
func reconnNoRelaytBtnClicked(buttonID int){
if len(sortedIds) > buttonID{
if pm != nil {
name := ""
if n, ok := playerNames[sortedIds[buttonID]]; ok {
name = n
}
answer := MessageBox(Detail("Are you sure you want to try to connect directly? This will expose your real IP address to peer: " +
sortedIds[buttonID] + " " + name), Type("yesno"))
connInfoView.Insert(END, answer +" ", "", "\n")

if answer == "yes"{
playerID, _ := strconv.Atoi(sortedIds[buttonID])
pm.HandleManualNoRelayReconnRequest(uint(playerID))
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Strconv error ignored in reconnNoRelaytBtnClicked.

Same issue as reconnectBtnClicked - if parsing fails, playerID will be 0.

 func reconnNoRelaytBtnClicked(buttonID int){
 	if len(sortedIds) > buttonID{
 		if pm != nil {
 			// ... message box code ...
 			if answer == "yes"{
-				playerID, _ := strconv.Atoi(sortedIds[buttonID])
+				playerID, err := strconv.Atoi(sortedIds[buttonID])
+				if err != nil {
+					return
+				}
 				pm.HandleManualNoRelayReconnRequest(uint(playerID))
 			}
 		}
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func reconnNoRelaytBtnClicked(buttonID int){
if len(sortedIds) > buttonID{
if pm != nil {
name := ""
if n, ok := playerNames[sortedIds[buttonID]]; ok {
name = n
}
answer := MessageBox(Detail("Are you sure you want to try to connect directly? This will expose your real IP address to peer: " +
sortedIds[buttonID] + " " + name), Type("yesno"))
connInfoView.Insert(END, answer +" ", "", "\n")
if answer == "yes"{
playerID, _ := strconv.Atoi(sortedIds[buttonID])
pm.HandleManualNoRelayReconnRequest(uint(playerID))
}
}
}
}
func reconnNoRelaytBtnClicked(buttonID int){
if len(sortedIds) > buttonID{
if pm != nil {
name := ""
if n, ok := playerNames[sortedIds[buttonID]]; ok {
name = n
}
answer := MessageBox(Detail("Are you sure you want to try to connect directly? This will expose your real IP address to peer: " +
sortedIds[buttonID] + " " + name), Type("yesno"))
connInfoView.Insert(END, answer +" ", "", "\n")
if answer == "yes"{
playerID, err := strconv.Atoi(sortedIds[buttonID])
if err != nil {
return
}
pm.HandleManualNoRelayReconnRequest(uint(playerID))
}
}
}
}
🤖 Prompt for AI Agents
In dbgwnd/dbgwnd.go around lines 223 to 240, the call to strconv.Atoi ignores
the returned error so a malformed sortedIds[buttonID] will produce playerID 0
and proceed incorrectly; change the code to capture and check the error from
strconv.Atoi, handle the failure by logging or showing an error message and
returning early instead of calling pm.HandleManualNoRelayReconnRequest with an
invalid ID, and only convert and call the handler when strconv.Atoi succeeds.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (8)
webrtc/peer_manager.go (8)

164-217: Manual reconnection flags are accessed under inconsistent locking

handleReconnection reads peer.manualReconnIsActive and peer.remoteManualRRequest without any lock, but elsewhere these fields are mutated under either peersMu (e.g. HandleManualReconnectRequest, handleRemoteManualReconnRequest) or peer.reconnectMu (lines 213-215).

This reintroduces the inconsistent-locking/data-race problem previously flagged for these fields.

To avoid races and hard-to-reason-about behavior, pick a single mutex to protect manualReconnIsActive and remoteManualRRequest (either peersMu or peer.reconnectMu) and use it consistently for all reads and writes, including here and in onPeerStateChanged.

For example, if you choose peer.reconnectMu:

func (p *PeerManager) handleReconnection(playerId uint, forceReconnect bool) {
    // ... find peer ...
-   if !forceReconnect && !peer.manualReconnIsActive && !peer.remoteManualRRequest {
+   peer.reconnectMu.Lock()
+   manualActive := peer.manualReconnIsActive
+   remoteReq := peer.remoteManualRRequest
+   peer.reconnectMu.Unlock()
+
+   if !forceReconnect && !manualActive && !remoteReq {
        if peer.IsActive() || (peer.connection != nil &&
            peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnecting) {
            applog.Info("Peer already active/connecting, skipping reconnection", zap.Uint("peer", playerId))
            return
        }
    }
-   if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
+   if !manualActive && !remoteReq {
       // normal path...
    } else {
       // manual TURN path, where you already hold reconnectMu later
    }
}

561-592: Global manualReconnectionMessage is a shared mutable template and introduces races

manualReconnectionMessage is initialized once from the first onPeerCandidatesGathered call and then reused for all manual reconnection requests.

Issues:

  • It’s package-global; HandleManualReconnectRequest later mutates BaseEvent.RecipientID in place. Concurrent manual reconnections for different peers can race on that field.
  • The template is only initialized when manualReconnectionMessage.GameID == 0. If a user triggers a manual reconnect before any candidates have been gathered, you’ll send a partially zero-valued event (GameID and SenderID both 0).
  • Using a hidden “fake candidate” message as control signaling is brittle and hard to reason about.

Given the logic in handleIceBreakerEvent only cares that len(Candidates) == 0 for manual reconnection, you can avoid the shared global entirely and construct a fresh CandidatesMessage per manual request.

See concrete suggestion in the comment on HandleManualReconnectRequest below.


21-22: Global TURN URL state persists across sessions and can accumulate stale entries

allTurnServersUrls and turnsNameById are package-level and never cleared, so repeated adapter runs in the same process will append duplicate URLs and retain stale TURN name mappings.

Either:

  • Reset these in saveTurnsURLs (e.g., allTurnServersUrls = nil; turnsNameById = make(map[int]string)), or
  • Make them fields on PeerManager so they are naturally scoped to one instance.

108-133: TURN URL parsing is fragile and can panic on short hostnames

The .com-based parsing has two concrete problems:

  • If the TURN URL host doesn’t contain .com (e.g. .net, .io, country-code TLDs), preparedName remains a single space, so UI labels will be empty/odd.
  • The else branch uses preparedName[len(preparedName)-8:]. If preparedName length is < 8 (e.g. turn.com:3478turn), this panics with a negative slice bound.

Additionally, globals allTurnServersUrls and turnsNameById are never reset, so calling saveTurnsURLs multiple times accumulates duplicates.

Consider:

  • Reinitializing globals at the start of this function.
  • Replacing the ad-hoc string slicing with net/url + hostname parsing (as suggested in the earlier review) to support arbitrary TLDs and avoid panics.
func (p *PeerManager) saveTurnsURLs() {
-	for i, _ := range(p.turnServer) {
-		for _,url := range(p.turnServer[i].URLs) {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
+
+	for i := range p.turnServer {
+		for _, url := range p.turnServer[i].URLs {
 			_, ok := turnsNameById[i+1]
 			if !ok {
-				preparedName := " "
-				index := strings.Index(url, ".com")
-				// ...
+				// TODO: parse url with net/url, extract second-level domain, truncate to 6 chars.
+				preparedName := "unknown"
 				turnsNameById[i+1] = preparedName
 			}
 			allTurnServersUrls = append(allTurnServersUrls, url)
 		}
 	}
}

353-355: TURN URL accessor exposes package-level mutable state

GetTurnURLs returns the package-level allTurnServersUrls and turnsNameById. In combination with saveTurnsURLs not clearing these, callers may see stale entries across adapter restarts in the same process.

If you keep the globals, consider documenting that the data is effectively process-global and possibly duplicated. Otherwise, prefer moving these into PeerManager so you can guarantee they’re tied to a single game/session and easily reset.


464-493: Manual reconnection flag in onPeerStateChanged has same locking inconsistency

Here peer.manualReconnIsActive is read without any synchronization, while other paths mutate it under peersMu or peer.reconnectMu.

This is the same race/concurrency-ownership problem as in handleReconnection. Once you decide which mutex should own manualReconnIsActive (and remoteManualRRequest), this check should be performed under that mutex as well, e.g.:

case webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed:
-    if !peer.manualReconnIsActive {
+    peer.reconnectMu.Lock()
+    manualActive := peer.manualReconnIsActive
+    peer.reconnectMu.Unlock()
+
+    if !manualActive {
        // schedule reconnection...
    } else {
        applog.Debug("Peer is already reconnecting manually. Skipping 'failed or closed' case")
    }

658-736: initialConnectionWatcher still has nil-pointer risk and uses global watchers map

Even though the watcher goroutine is currently disabled, the implementation has two issues if it’s ever re-enabled:

  • Inside the loop, you dereference peer.connection without nil checks under peersMu:
if peer, ok := p.peers[playerId]; ok {
    if peer.connection.ConnectionState().String() == "connected" {
        peerConnnectedSecondsTotal++
    }
}

If peer exists but peer.connection is nil (early in setup or after close), this will panic.

  • watchers is a package-level map keyed by player ID. It will retain state across adapter restarts/games in the same process and can block watcher creation for a reused player ID.

A safer pattern:

for i := 0; i < keepAliveSec; i++ {
    time.Sleep(time.Second)

-   p.peersMu.Lock()
-   if peer, ok := p.peers[playerId]; ok {
-       if peer.connection.ConnectionState().String() == "connected"{
-           peerConnnectedSecondsTotal += 1
-       }
-   }
-   p.peersMu.Unlock()
+   p.peersMu.Lock()
+   peer, ok := p.peers[playerId]
+   var state webrtc.PeerConnectionState
+   if ok && peer != nil && peer.connection != nil {
+       state = peer.connection.ConnectionState()
+   }
+   p.peersMu.Unlock()
+
+   if state == webrtc.PeerConnectionStateConnected {
+       peerConnnectedSecondsTotal++
+   }

And consider scoping watchers to PeerManager or clearing it when starting a new game.


763-790: Manual reconnect request mutates global template and does I/O under peersMu

Two concerns here:

  1. Shared mutable manualReconnectionMessage

    HandleManualReconnectRequest sets manualReconnectionMessage.BaseEvent.RecipientID = &playerId and then sends that struct. As noted earlier, this introduces a data race if two manual reconnections run concurrently for different peers and also relies on manualReconnectionMessage having been initialized via onPeerCandidatesGathered.

    You can remove the shared global entirely and construct the control message on demand:

    func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
        peerAsString := fmt.Sprintf("%d", playerId)
        p.peersMu.Lock()
        peer, ok := p.peers[playerId]
        if ok {
            if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
                peer.manualReconnIsActive = true
                p.disableRecBtnPeers = append(p.disableRecBtnPeers, peerAsString)
  •          manualReconnectionMessage.BaseEvent.RecipientID = &playerId
    
  •          err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
    
  •          // Send an explicit "manual reconnect" candidates message with no candidates.
    
  •          msg := icebreaker.CandidatesMessage{
    
  •              BaseEvent: icebreaker.BaseEvent{
    
  •                  EventType:   icebreaker.EventKindCandidates,
    
  •                  GameID:      p.gameId,
    
  •                  SenderID:    p.localUserId,
    
  •                  RecipientID: &playerId,
    
  •              },
    
  •              Session:    nil,
    
  •              Candidates: nil, // empty slice → manual reconnect signal
    
  •          }
    
  •          p.peersMu.Unlock()
    
  •          err := p.icebreakerClient.SendEvent(msg)
             if err != nil {
                 applog.Error("Failed to send manualReconnectionMessage to peer "+peerAsString, zap.Error(err))
             }
    
  •          p.peersMu.Unlock()
             p.preparePeerForManualReconn(playerId)
             go p.scheduleManualReconnection(playerId)
    
    
    You can then delete `manualReconnectionMessage` and its initialization entirely.
    
    
  1. Holding peersMu during network I/O

    In the current code, SendEvent is called while peersMu is still held. If SendEvent blocks (network hiccup, backpressure), it can stall other peer operations and reconnection scheduling. The diff above also addresses this by unlocking before the send.

🧹 Nitpick comments (6)
dbgwnd/dbgwnd.go (2)

422-424: Empty placeholder function.

The setRegion() function is defined but empty. If region setting is needed, this should be implemented before merging.

Do you want me to help implement the region setting logic, or should this function be removed if not needed?


551-560: Overly conservative length check may skip valid data.

The check if len(tIds) > 1 ensures both indices are safe, but skips processing entirely when len == 1. If the slice can have length 1 (e.g., only local TURN is set), that information won't be displayed.

Consider processing available data:

 	if tIds, ok := turnIds[id]; ok {
-		if len(tIds) > 1 {
-			if tIds[0] != 0 {
-				localTurnName = turnsNameById[tIds[0]]
-			}
-			if tIds[1] != 0 {
-				remoteTurnName = turnsNameById[tIds[1]]
-			}
+		if len(tIds) > 0 && tIds[0] != 0 {
+			localTurnName = turnsNameById[tIds[0]]
+		}
+		if len(tIds) > 1 && tIds[1] != 0 {
+			remoteTurnName = turnsNameById[tIds[1]]
 		}
 	}
webrtc/peer_manager.go (4)

321-343: Stats collection now guards nil PeerConnections correctly; consider minimizing lock scope

The nil checks on peer and peer.connection fix the earlier panic risk when the debug window requests stats for peers still starting up. That part looks good.

One improvement: you hold peersMu while calling peer.connection.GetStats(), which is potentially non-trivial work and calls into Pion. To reduce contention on peersMu, you can snapshot the peers and their connections under the lock, then compute stats outside:

func (p *PeerManager) GetAllPeersStats() (map[string]webrtc.StatsReport, map[string]string, map[string][]int, []string) {
-    p.peersMu.Lock()
-    defer p.peersMu.Unlock()
-
-    allPeersStats := make(map[string]webrtc.StatsReport)
-    connectionStates := make(map[string]string)
-    turnIds := make(map[string][]int)
-    idsToDisable := p.disableRecBtnPeers
-    p.disableRecBtnPeers = nil
-
-    for id,peer := range(p.peers) {
-        if peer == nil || peer.connection == nil{
-            continue
-        }
-        // ...
-    }
+    p.peersMu.Lock()
+    snapshot := make(map[uint]*Peer, len(p.peers))
+    for id, peer := range p.peers {
+        snapshot[id] = peer
+    }
+    idsToDisable := p.disableRecBtnPeers
+    p.disableRecBtnPeers = nil
+    p.peersMu.Unlock()
+
+    allPeersStats := make(map[string]webrtc.StatsReport)
+    connectionStates := make(map[string]string)
+    turnIds := make(map[string][]int)
+
+    for id, peer := range snapshot {
+        if peer == nil || peer.connection == nil {
+            continue
+        }
+        // ...
+    }
    return allPeersStats, connectionStates, turnIds, idsToDisable
}

Not critical for the debug window, but improves robustness if stats are polled frequently.


371-387: Age-based reconnection guard looks reasonable but is brittle to system clock changes

The logic to skip reconnection for peers younger than 120 seconds avoids fighting the initial connect (and is clearly marked as a workaround). Two minor considerations:

  • creationTimeSeconds is presumably based on time.Now().Unix(). If the system clock jumps backwards, tSincePeerCreation could go negative. It’s unlikely but would cause the “skip reconnection” guard to fail open.
  • It might be cleaner to store a time.Time in the peer and use time.Since(peer.creationTime).

No blocking issues here, especially given the debug/workaround nature; just something to keep in mind if this flows into production.


738-761: HandleManualNoRelayReconnRequest depends on forceTurnRelay semantics—please confirm behavior

This path:

  • Sets peer.forceTurnRelay = false under peersMu.
  • Then calls handleReconnection(playerId, true).

Whether this actually enforces a “no relay” reconnection depends entirely on how Peer.ConnectOnce interprets peer.forceTurnRelay. From this file, all reconnection paths still pass a TURN server list into ConnectOnce, so it’s non-obvious whether forcing-direct actually happens.

Given the inline comment “Add: Doesn’t work.”, this is clearly experimental. I’d recommend either:

  • Verifying that Peer.ConnectOnce and/or the underlying ICE config actually switches to a non-relay policy when forceTurnRelay is false, or
  • Clearly marking this as a stub/unused feature so callers (e.g., debug UI) don’t rely on it yet.

792-806: scheduleManualReconnection assumes a 2s delay is sufficient

The logic is straightforward and functionally fine for a debug feature: sleep 2 seconds, then, if the peer still exists, call handleReconnection (which will use the manual TURN list prepared earlier).

Given this is intentionally heuristic (“assume that second peer received it and even if not, it doesn’t really matter”), no changes are strictly required. Just be aware that in high-latency conditions 2 seconds might be short; you may want this configurable if the feature is promoted to production.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6379b6d and f024bfa.

📒 Files selected for processing (2)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (1)
webrtc/peer_manager.go (1)

808-826: Remote manual reconnection handler has consistent locking; behavior looks reasonable

handleRemoteManualReconnRequest:

  • Guards peers[playerId] and peer.remoteManualRRequest under peersMu.
  • Ensures preparePeerForManualReconn is called only after unlocking to avoid deadlocks.
  • Protects disableRecBtnPeers updates under peersMu.

Given the broader manual recon locking concerns already raised, this specific function looks correct within that model.

Comment thread dbgwnd/dbgwnd.go
}

func refreshLogs() {
textYpos,_ := strconv.ParseFloat(strings.Split(logView.Yview(), " ")[1], 64)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ParseFloat error is silently ignored.

If logView.Yview() returns an unexpected format, the split and parse will fail, leaving textYpos at its zero value (0.0). This will cause the autoscroll check at line 337 to always fail.

Consider handling the error:

-	textYpos,_ := strconv.ParseFloat(strings.Split(logView.Yview(), " ")[1], 64)
+	textYpos, err := strconv.ParseFloat(strings.Split(logView.Yview(), " ")[1], 64)
+	if err != nil {
+		textYpos = 0.0 // or handle appropriately
+	}
🤖 Prompt for AI Agents
In dbgwnd/dbgwnd.go around line 288, the call textYpos,_ :=
strconv.ParseFloat(strings.Split(logView.Yview(), " ")[1], 64) ignores the
ParseFloat error and assumes the split result exists; validate the split length
and capture the ParseFloat error, and handle it (e.g., log the error and set a
safe default or abort the autoscroll check) so a malformed Yview() string
doesn't leave textYpos at 0.0 and break the autoscroll logic.

Comment thread dbgwnd/dbgwnd.go
Comment on lines +574 to +579
playerName := "-"
if name, ok := playerNames[id]; ok {
playerName = name
} else {
playerNames = adapter.GetNicknames()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Entire playerNames map is overwritten when a single name is missing.

At line 578, when a player name is not found in the cached playerNames map, the code fetches all nicknames and replaces the entire map. This is inefficient and loses any previously cached names.

Consider updating selectively:

 	playerName := "-"
 	if name, ok := playerNames[id]; ok {
 		playerName = name
 	} else {
-		playerNames = adapter.GetNicknames()
+		allNames := adapter.GetNicknames()
+		for k, v := range allNames {
+			playerNames[k] = v
+		}
+		if name, ok := playerNames[id]; ok {
+			playerName = name
+		}
 	}
🤖 Prompt for AI Agents
In dbgwnd/dbgwnd.go around lines 574-579, the code currently replaces the entire
playerNames map when a single id is missing; instead, fetch only the missing
nickname(s) or fetch the full nicknames and merge them into the existing
playerNames map rather than overwriting it. Change the logic so that when a name
is not found you call adapter.GetNicknames() (or adapter.GetNickname(id) if
available), iterate the returned entries to update/insert into playerNames, and
then set playerName from the now-updated map; this preserves previously cached
names and avoids losing existing entries.

Comment thread dbgwnd/dbgwnd.go
Comment on lines +617 to +622
if len(oldPeer) < len(sortedIds) {
for i,_ := range(sortedIds) {
if _, ok := oldPeer[sortedIds[i]]; !ok {
disabledBtnsTimers[sortedIds[i]] = disableRecButnOnNewConnect
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

New peer detection may miss peer replacements.

The condition if len(oldPeer) < len(sortedIds) only detects when the peer count increases. If a peer disconnects and another connects in the same refresh cycle (count unchanged), the new peer won't be detected or added to oldPeer, and won't get the connection timer.

Consider detecting all new peers regardless of count:

-	if len(oldPeer) < len(sortedIds) {
-		for i,_ := range(sortedIds) {
-			if _, ok := oldPeer[sortedIds[i]]; !ok {
-				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnNewConnect
-			}
+	for i,_ := range(sortedIds) {
+		if _, ok := oldPeer[sortedIds[i]]; !ok {
+			disabledBtnsTimers[sortedIds[i]] = disableRecButnOnNewConnect
+			oldPeer[sortedIds[i]] = true
 		}
 	}
🤖 Prompt for AI Agents
In dbgwnd/dbgwnd.go around lines 617-622, the current length check only catches
increases in peer count and misses replacements; remove the `if len(oldPeer) <
len(sortedIds)` guard and always iterate over `sortedIds`, and for each id that
is not present in `oldPeer` set `disabledBtnsTimers[id] =
disableRecButnOnNewConnect` (ensure `oldPeer` and `disabledBtnsTimers` maps are
initialized before use). This guarantees newly connected peers are detected even
when total count is unchanged.

Comment thread webrtc/peer_manager.go
Comment on lines +828 to +886
func (p *PeerManager) preparePeerForManualReconn(playerId uint) {
peerAsString := fmt.Sprintf("%d", playerId)
p.peersMu.Lock()
peer, ok := p.peers[playerId]

if ok {
peer.numOfManualReconns += 1
peer.peerSpecificTurn = nil

numOfTurns := len(p.turnServer)
totalCombinations := numOfTurns * numOfTurns
isLeader := p.localUserId < playerId
numOfReconns := peer.numOfManualReconns

if numOfReconns > totalCombinations {
numOfReconns = ((numOfReconns - 1) % totalCombinations) + 1
}

// Every manual reconnection attempt should give a different pair
// So players can find a better connection if some TURNs are lagging
// It tries pairs with same TURNs first, then mix them. 2 servers example:
// 1. Cloud<->Cloud 2. Xirsys<->Xirsys 3. Cloud<->Xirsys 4. Xirsys<->Cloud
if numOfReconns <= numOfTurns {
peer.peerSpecificTurn = append(peer.peerSpecificTurn, p.turnServer[numOfReconns - 1])
applog.Info("Specific TURN prepared. Leader: " + strconv.Itoa(numOfReconns) +
" secondary: " + strconv.Itoa(numOfReconns) + " Peer: " + peerAsString +
" Number of manual reconnects: "+ strconv.Itoa(peer.numOfManualReconns))
peer.specTurnIdLocal = numOfReconns
peer.specTurnIdRemote = numOfReconns
} else {
leaderServerNum :=
int(math.Ceil(float64((numOfReconns - numOfTurns)) / float64((totalCombinations - numOfTurns)/numOfTurns)))
secondaryServerNum := (numOfReconns - numOfTurns) / leaderServerNum

for i := 1; i <= numOfTurns; i++ {
if i == leaderServerNum {
secondaryServerNum += 1
break
} else if i == secondaryServerNum {
break
}
}

if isLeader {
peer.peerSpecificTurn = append(peer.peerSpecificTurn, p.turnServer[leaderServerNum - 1])
peer.specTurnIdLocal = leaderServerNum
peer.specTurnIdRemote = secondaryServerNum
} else {
peer.peerSpecificTurn = append(peer.peerSpecificTurn, p.turnServer[secondaryServerNum - 1])
peer.specTurnIdLocal = secondaryServerNum
peer.specTurnIdRemote = leaderServerNum
}

applog.Info("Specific TURN prepared. Leader:" + strconv.Itoa(leaderServerNum) +
" secondary: " + strconv.Itoa(secondaryServerNum) + " Peer: " + peerAsString)
}
}
p.peersMu.Unlock()
} No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

preparePeerForManualReconn can panic when no TURN servers are configured

The combination logic over TURN servers is clever and, with the 1-based modulo fix, avoids the earlier 0 index problem. However:

  • If len(p.turnServer) == 0, then:
    • totalCombinations := numOfTurns * numOfTurns → 0.
    • The normalization ((numOfReconns - 1) % totalCombinations) divides by zero and panics.
    • Even before that, all later indexing into p.turnServer[...] would be invalid.

If manual reconnect is only valid when at least one TURN server exists, it’s safer to guard explicitly:

func (p *PeerManager) preparePeerForManualReconn(playerId uint) {
    peerAsString := fmt.Sprintf("%d", playerId)
    p.peersMu.Lock()
    peer, ok := p.peers[playerId]

    if ok {
-       peer.numOfManualReconns += 1
-       peer.peerSpecificTurn = nil
-
-       numOfTurns := len(p.turnServer)
+       numOfTurns := len(p.turnServer)
+       if numOfTurns == 0 {
+           applog.Error("Manual reconnection requested but no TURN servers are configured", zap.String("peer", peerAsString))
+           p.peersMu.Unlock()
+           return
+       }
+
+       peer.numOfManualReconns++
+       peer.peerSpecificTurn = nil
+
        totalCombinations := numOfTurns * numOfTurns
        // ...

That makes the invariant explicit and prevents hard panics when configuration is incomplete.

🤖 Prompt for AI Agents
In webrtc/peer_manager.go around lines 828 to 886, add an explicit guard for the
case where len(p.turnServer) == 0 before any math or indexing that assumes at
least one TURN: after computing numOfTurns (and before totalCombinations or any
p.turnServer[...] access), if numOfTurns == 0 clear/set peer.peerSpecificTurn
appropriately, log or warn that manual reconnection is not possible without TURN
servers, unlock the mutex and return early; this prevents the divide-by-zero in
the ((... ) % totalCombinations) normalization and avoids invalid p.turnServer
indexing.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (6)
webrtc/peer_manager.go (6)

21-22: Global TURN URL caches are sticky across sessions and parsing is still .com‑specific

allTurnServersUrls and turnsNameById are package‑level and only ever appended to; saveTurnsURLs doesn’t clear them. If a new PeerManager is created in the same process (adapter restart, tests, multiple games), you’ll accumulate duplicates and stale TURN names, and GetTurnURLs will keep returning them. Additionally, the parsing logic assumes a .com suffix and may generate poor names for .org, .io, ccTLDs, etc.

Consider:

  • Resetting both structures at the start of saveTurnsURLs (e.g. allTurnServersUrls = nil; turnsNameById = make(map[int]string)), and
  • Switching to net/url + host parsing instead of .com substring matching as suggested in prior reviews.

Also, be mindful that GetTurnURLs returns the backing slice and map; callers should treat them as read‑only to avoid accidental mutation.

Also applies to: 108-133, 353-355


153-220: Manual reconnection flags use inconsistent locking, causing data races

manualReconnIsActive and remoteManualRRequest are read and written under different synchronization regimes:

  • Written under peersMu in HandleManualReconnectRequest, HandleManualNoRelayReconnRequest, and handleRemoteManualReconnRequest.
  • Written under reconnectMu in the manual branch of handleReconnection.
  • Read with no mutex in handleReconnection (conditions at lines 175–185) and onPeerStateChanged (line 464).

In Go, this mixed/unsynchronized access constitutes a data race and can lead to stale or inconsistent decisions about whether a peer is already in a manual reconnect flow, especially with concurrent UI/debug actions and WebRTC callbacks. It also makes it hard to reason about when flags are cleared vs. when reconnection scheduling happens.

Concrete suggestions:

  • Pick a single mutex to guard these fields across the codebase (likely peersMu, since you already use it in most handlers), or
  • Encapsulate their access behind Peer methods that consistently take a specific lock.

handleReconnection in particular should either:

  • Hold peersMu while inspecting these flags and/or snapshot them under lock into local variables, or
  • Use a dedicated per‑peer lock inside Peer for all reconnection‑state decisions.

I’d treat this as a correctness issue even for a debug‑only tool; it will be very hard to debug reconnection behaviour if the flags race.

Also applies to: 462-493, 742-790, 808-885


561-592: manualReconnectionMessage global template is still a source of stale IDs and races

manualReconnectionMessage is:

  • Initialized once in onPeerCandidatesGathered the first time GameID == 0, and
  • Later mutated in place in HandleManualReconnectRequest by setting BaseEvent.RecipientID before sending.

This has several problems (mirroring prior reviews):

  • The first candidate sender “wins” and all later manual reconnects reuse that one message as a template, including its Session and any subtle fields you may add in the future.
  • Mutating a global struct and then sending it concurrently for multiple peers is racy: two manual reconnects can overwrite RecipientID (and any other fields) between construction and SendEvent.
  • There’s no guarantee manualReconnectionMessage has been initialized before the first manual reconnect request; in that case you’ll send a message with default/zero GameID.

Safer approach:

  • Drop the global mutable template entirely.
  • When you need to send a manual reconnect signal, construct a fresh CandidatesMessage on the spot with:
    • Session: nil (or whatever you actually need),
    • Candidates: []webrtc.ICECandidate{} to preserve the “empty = manual reconnect” convention, and
    • A correctly set RecipientID for the specific playerId.

That keeps the global state out of the equation and removes the data race.

Also applies to: 763-777


462-493: onPeerStateChanged also reads manual reconnect flag without synchronization

In the Failed/Closed branch you guard reconnection scheduling with if !peer.manualReconnIsActive { ... } else { ... }, but this read is done without any mutex, while the flag is written under peersMu/reconnectMu elsewhere. This is the same race as in handleReconnection, but in a WebRTC callback.

Once you pick a single mutex or encapsulated accessor for manualReconnIsActive/remoteManualRRequest, please update this site as well to avoid inconsistent decisions about whether to auto‑schedule reconnection vs. rely on manual flows.


658-659: Global watcher state and initialConnectionWatcher fragility

watchers and watcherMutex remain package‑level. Combined with the now‑commented‑out call site, this has a few issues (as previously noted):

  • If a new PeerManager is constructed in the same process, stale entries in watchers may block creation of new watchers for the same player IDs.
  • initialConnectionWatcher dereferences peer.connection without checking for nil; while you protect access with peersMu, peer.connection itself may be nil early in startup or after closure, which would panic when calling ConnectionState().

Given this is already described as a workaround, I’d either:

  • Move watchers into PeerManager and reset it per game, and
  • Harden initialConnectionWatcher to snapshot the peer under lock and only call ConnectionState() if both peer and peer.connection are non‑nil, ideally comparing against the typed constant webrtc.PeerConnectionStateConnected instead of "connected".

If you intend to leave the watcher disabled long‑term, it may be worth clearly marking this as dead/debug code or removing it to avoid bit‑rot.

Also applies to: 668-736


828-885: preparePeerForManualReconn still assumes at least one TURN server; add an explicit guard

As in prior feedback, this function assumes len(p.turnServer) > 0:

  • numOfTurns := len(p.turnServer)
  • totalCombinations := numOfTurns * numOfTurns
  • Later math and indexing ((numOfReconns-1) % totalCombinations, p.turnServer[...]) will panic when numOfTurns == 0 due to divide‑by‑zero and invalid indices.

Even if your current configs always provide at least one TURN, this is a brittle hidden invariant. Please add an early guard, for example:

 func (p *PeerManager) preparePeerForManualReconn(playerId uint) {
@@
-    if ok {
-        peer.numOfManualReconns += 1
-        peer.peerSpecificTurn = nil
-
-        numOfTurns := len(p.turnServer)
-        totalCombinations := numOfTurns * numOfTurns
+    if ok {
+        numOfTurns := len(p.turnServer)
+        if numOfTurns == 0 {
+            applog.Error("Manual reconnection requested but no TURN servers are configured",
+                zap.String("peer", peerAsString))
+            p.peersMu.Unlock()
+            return
+        }
+
+        peer.numOfManualReconns++
+        peer.peerSpecificTurn = nil
+
+        totalCombinations := numOfTurns * numOfTurns

and then keep the rest of the combination logic unchanged. That makes the assumption explicit and prevents a hard crash if TURN is misconfigured.

🧹 Nitpick comments (7)
webrtc/peer.go (3)

49-57: New peer state fields and initialization look consistent

The added fields for creation time, manual reconnection state, per‑peer TURN IDs, and drop counters are initialized coherently and line up with how PeerManager uses them (creation age checks, manual reconnect paths, and UI stats). If you keep evolving the manual reconnect logic, consider adding brief comments on which mutex (if any) is expected to guard each of these fields, to avoid future locking inconsistencies between Peer and PeerManager.

Also applies to: 145-152


403-409: Double‑check demoting send failures to Debug level

Changing the log on gameDataChannel.Send failures from Error to Debug will significantly reduce visibility into persistent outbound send issues. If repeated send failures meaningfully affect gameplay, you may want to:

  • Keep this at Warn (or emit a periodic Warn similar to the receive‑side drop counter), while still suppressing per‑packet spam with aggregation, or
  • At least ensure the debug window or other tooling surfaces these Debug logs when diagnosing connectivity problems.

Please confirm that losing Error‑level signal here is acceptable for your planned debug workflows.


425-435: Packet drop counting logic is fine; consider structured logging

The throttled warning after every 100 dropped packets works and will prevent log spam. You might want to:

  • Log numOfDroppedPackets and peerId as structured fields instead of concatenated into the message, to keep logs grep‑ and UI‑friendly.
  • Slight nit: dropPacketsLastWarn is effectively a modulo‑100 counter; renaming it (or commenting) could make the intent clearer.

Functionally this is OK as‑is.

webrtc/peer_manager.go (4)

36-42: Longer ICE failure and reconnection intervals – confirm behavioural trade‑off

Bumping peerFailedTimeout to 120s and peerReconnectionInterval to 20s will make failures take longer to be detected and reconnection attempts less frequent. That can be fine for flaky or high‑latency links, but it also means users may sit on dead connections for up to ~2 minutes.

Given this PR is “debug window / not for merging”, please confirm whether these values are intended for production or just for experimentation; if the latter, they probably should be gated or reverted before merging.


51-65: Force‑enabling TURN relay and new disableRecBtnPeers state

Two behavioural changes worth calling out:

  • forceTurnRelay is now always true in NewPeerManager, which forces relay for all peers regardless of launcher/session settings. That has significant impact (no direct P2P even when possible), so if this is for debugging it should likely not ship unguarded into mainline.
  • disableRecBtnPeers is used as a one‑shot list of peer IDs for the UI to disable manual reconnect buttons. The pattern (append while holding peersMu, then read‑and‑clear under peersMu in GetAllPeersStats) looks sound.

I’d recommend at least documenting that forceTurnRelay is intentionally forced here and should be revisited before promoting this branch.

Also applies to: 67-92


251-263: Empty‑candidates hook for manual reconnection is fragile by design

Interpreting a CandidatesMessage with an empty Candidates slice as a manual reconnection signal works as a short‑term hack, but it conflates two distinct concepts into one event type. This can become hard to maintain once you or others add richer signalling.

Since you’ve already commented this as a “dirty hook” and plan a dedicated reconnection message, I’d just reinforce that this should be a TODO and not become a long‑term protocol contract.


321-343: GetAllPeersStats and disableRecBtnPeers usage looks reasonable

GetAllPeersStats:

  • Correctly holds peersMu while iterating the map.
  • Skips peers with nil or uninitialized connection, preventing panics.
  • Returns connection state strings and per‑peer TURN IDs for the debug UI.

The “one‑shot” pattern for disableRecBtnPeers (append under lock in the manual/remote manual handlers, then snapshot and clear under lock in GetAllPeersStats) is a clean way to signal the UI which buttons to disable.

Only minor thought: peer.connection.GetStats() is called while holding peersMu; if GetStats() is ever slow or blocking, it will stall other operations that need peersMu. If that becomes an issue, you could snapshot the peers into a slice under lock, then call GetStats() after releasing it.

Also applies to: 64-65, 770-771, 814-815

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f024bfa and 1a5c948.

📒 Files selected for processing (3)
  • adapter/adapter.go (5 hunks)
  • webrtc/peer.go (4 hunks)
  • webrtc/peer_manager.go (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • adapter/adapter.go
🧰 Additional context used
🧬 Code graph analysis (2)
webrtc/peer.go (2)
applog/context.go (1)
  • FromContext (10-12)
applog/applog.go (2)
  • Debug (110-123)
  • Warn (95-108)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • Debug (110-123)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (4)
webrtc/peer_manager.go (4)

164-220: Reconnection flow logic mostly OK; watch reuse with forceReconnect

The split between “normal” reconnects and manual reconnects in handleReconnection makes sense:

  • For non‑manual reconnects (!peer.manualReconnIsActive && !peer.remoteManualRRequest), you short‑circuit when the peer is already active/connecting and reset TURN IDs, then call ConnectOnce with the default turnServer list and schedule a retry on failure.
  • For manual reconnects, you call ConnectOnce with peer.peerSpecificTurn and clear the manual flags once done.

Given the flag races noted above, just be sure that:

  • forceReconnect is only used to bypass the “already active/connecting” bail‑out, and does not accidentally bypass the manual gating conditions.
  • peer.peerSpecificTurn is always populated by preparePeerForManualReconn before the manual branch is hit (your scheduleManualReconnection sleep helps here, but it’s inherently racy if the system is under heavy load).

From a logic perspective this flow looks reasonable once the locking is cleaned up.


365-386: Creation‑time gate for early reconnection is sensible

Using peer.creationTimeSeconds to avoid scheduling reconnection in the first 120 seconds after peer creation seems like a pragmatic workaround for slow initial connects (especially given your RU comment). The log message giving the peer age in seconds is also useful for diagnosis.

Just ensure that Peer instances are always created via CreatePeer (which populates creationTimeSeconds) so you don’t end up with zero timestamps and negative/huge tSincePeerCreation values.

Also applies to: 428-436


792-806: scheduleManualReconnection sequencing is fine but inherently racy

The “sleep 2 seconds, then call handleReconnection if the peer still exists” pattern works well enough for a debug‑oriented feature and matches your comments about assuming the remote side saw the signal.

Just be aware that:

  • There’s no guarantee preparePeerForManualReconn has completed when this fires, and
  • If the peer disappears in between the existence check and the reconnect call, you’ll still run handleReconnection on a stale pointer.

If this flow graduates beyond debugging, you may want a slightly more robust handshake (e.g., explicit ack or at least a snapshot of the peer and its peerSpecificTurn under lock).


742-761: The referenced function HandleManualNoRelayReconnRequest does not exist in the current codebase at the cited lines (742-761 in a 514-line file)

The review comment references code that is not present in the repository. However, the underlying concern about unsynchronized access to peer.forceTurnRelay is valid:

  • forceTurnRelay is read without synchronization in Peer.reconnect() (line 155) and Peer.reconnectWebRtcPeer() (lines 216, 222)
  • forceTurnRelay is written without synchronization in Peer.reconnectWebRtcPeer() (line 222) and PeerManager.onPeerStateChanged() (line 361)
  • A dedicated reconnectMu exists on the Peer struct but is not consistently used to guard forceTurnRelay writes
  • Lines 364-366 in peer_manager.go show commented-out reconnectMu locking, suggesting prior awareness of this issue

The data race concern is legitimate if this function or similar manual reconnection logic exists elsewhere, but the specific code snippet and line numbers do not match the current codebase.

@Strogoo Strogoo mentioned this pull request Dec 6, 2025

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (8)
adapter/adapter.go (2)

73-87: Backoff growth condition is dead and contradicts the comment

backoff is initialized to time.Second and never changed, so if backoff < 0*time.Second is always false and the multiplication never runs, while the comment says “don’t increase delay and just use fixed 1 sec value”.

Either remove the condition and multiplication entirely, or delete the unused backoff variable and directly use time.Second in time.After for clarity:

-        backoff := time.Second
+        backoff := time.Second
@@
-            case <-time.After(backoff):
-                // Better don't increase delay and just use fixed 1 sec value
-                // Might be critical in some situations when you want to restore conn as fast as possible
-                if backoff < 0*time.Second {
-                    backoff *= 2
-                }
+            case <-time.After(backoff):
+                // Fixed 1s delay for faster reconnection; no exponential backoff.

20-22: Global PeerManager remains racy between Start() and debug UI access

PeerManager is written once in Start() and read via GetPeerManager() with no synchronization. Given Start() runs in its own goroutine and the debug window can call GetPeerManager() concurrently, this is a data race under Go’s memory model (even though the pointer only changes from nil to non-nil).

Please either avoid the package-level global or guard it with a mutex/atomic.Value, e.g.:

+import "sync"
+
-var PeerManager     *webrtc.PeerManager
+var (
+    peerManagerMu sync.RWMutex
+    peerManager   *webrtc.PeerManager
+)
@@
-    peerManager := webrtc.NewPeerManager(
+    peerManager := webrtc.NewPeerManager(
@@
-    PeerManager = peerManager
+    peerManagerMu.Lock()
+    peerManager = peerManager
+    peerManagerMu.Unlock()
@@
 func GetPeerManager() *webrtc.PeerManager {
-    return PeerManager
+    peerManagerMu.RLock()
+    defer peerManagerMu.RUnlock()
+    return peerManager
 }

Also applies to: 244-246

webrtc/peer_manager.go (6)

21-23: Global TURN URL cache leaks across sessions and URL parsing is brittle

allTurnServersUrls and turnsNameById are package-level and only ever appended/assigned in saveTurnsURLs(). If a new PeerManager is created in the same process (e.g., multiple games or adapter restarts), old URLs and names linger and new ones are added, producing duplicates and stale data. The heuristic based on strings.Index(url, ".com") and substring slicing is also fragile for non-.com TLDs and short hosts.

Consider:

  • Resetting the globals at the start of saveTurnsURLs():
func (p *PeerManager) saveTurnsURLs() {
-	for i, _ := range(p.turnServer) {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
+
+	for i := range p.turnServer {
  • And longer term, replacing the .com substring logic with net/url parsing and proper host extraction (as suggested in earlier reviews) so the display names are robust across different TURN hostnames.

Also applies to: 108-133, 321-343, 353-355


164-217: Manual reconnection flags use inconsistent locking and are read without synchronization

peer.manualReconnIsActive and peer.remoteManualRRequest are:

  • Written under peersMu in HandleManualReconnectRequest and handleRemoteManualReconnRequest.
  • Written under peer.reconnectMu in the manual branch of handleReconnection.
  • Read without any lock in handleReconnection (both the “already active/connecting” gate and the manual/normal path split) and in onPeerStateChanged.

This violates Go’s race rules and can lead to:

  • Data races on these bools between reconnection goroutines and the WebRTC state-change callback.
  • Inconsistent behavior where some reconnection paths think manual recon is active and others do not.
  • Hard-to-reproduce bugs where automatic reconnection runs concurrently with a manual recon attempt.

A safer pattern is:

  • Pick a single mutex (ideally peersMu) as the owner for these two fields.
  • Only read/write manualReconnIsActive and remoteManualRRequest while holding that mutex.
  • Avoid using peer.reconnectMu for them; keep reconnectMu scoped to reconnectionScheduled / low-level reconnect sequencing.

For example, in handleReconnection’s manual branch:

-    } else {
-        applog.Info("Connecting to peer with specific TURN", zap.Uint("playerId", playerId))
-        if err := peer.ConnectOnce(peer.peerSpecificTurn); err != nil {
-            applog.Error("Peer connection with specific TURN failed", zap.Uint("peer", playerId), zap.Error(err))
-        }
-        peer.reconnectMu.Lock()
-        peer.manualReconnIsActive = false
-        peer.remoteManualRRequest = false
-        peer.reconnectMu.Unlock()
-    }
+    } else {
+        applog.Info("Connecting to peer with specific TURN", zap.Uint("playerId", playerId))
+        if err := peer.ConnectOnce(peer.peerSpecificTurn); err != nil {
+            applog.Error("Peer connection with specific TURN failed", zap.Uint("peer", playerId), zap.Error(err))
+        }
+
+        p.peersMu.Lock()
+        if peer2, ok := p.peers[playerId]; ok && peer2 == peer {
+            peer.manualReconnIsActive = false
+            peer.remoteManualRRequest = false
+        }
+        p.peersMu.Unlock()
+    }

And similarly, wrap the flag checks at the top of handleReconnection and in onPeerStateChanged with peersMu so all accesses are consistent.

Also applies to: 456-490, 743-761, 763-806, 808-826, 828-886


561-592: Global manualReconnectionMessage template is racy and can target the wrong peer

manualReconnectionMessage is:

  • Initialized once in onPeerCandidatesGathered the first time candidates are sent (using that first remotePeer).
  • Later mutated in-place in HandleManualReconnectRequest via manualReconnectionMessage.BaseEvent.RecipientID = &playerId and sent.

Issues:

  1. The template is shared across all peers; HandleManualReconnectRequest for peer B can still be using a Session/metadata captured when talking to peer A.
  2. RecipientID is mutated on the shared global; concurrent manual recon requests for different peers will race and can stomp each other’s recipient before SendEvent.
  3. There’s no synchronization around reads/writes of manualReconnectionMessage.

Since the receiving side only treats “Candidates with empty list” as a manual-reconnect signal, you don’t need this global at all. Build a fresh message per request:

 func (p *PeerManager) HandleManualReconnectRequest(playerId uint) {
@@
-        if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
+        if !peer.manualReconnIsActive && !peer.remoteManualRRequest {
             peer.manualReconnIsActive = true
             p.disableRecBtnPeers = append(p.disableRecBtnPeers, peerAsString)
-            
-            manualReconnectionMessage.BaseEvent.RecipientID = &playerId
-            err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+            
+            msg := icebreaker.CandidatesMessage{
+                BaseEvent: icebreaker.BaseEvent{
+                    EventType:   icebreaker.EventKindCandidates,
+                    GameID:      p.gameId,
+                    SenderID:    p.localUserId,
+                    RecipientID: &playerId,
+                },
+                Session:    nil,
+                Candidates: []webrtc.ICECandidate{},
+            }
+            err := p.icebreakerClient.SendEvent(msg)

You can then remove manualReconnectionMessage entirely unless it’s needed elsewhere.

Also applies to: 763-790


321-343: GetAllPeersStats correctly guards against nil connections

The new GetAllPeersStats implementation:

  • Holds peersMu for the duration.
  • Skips peers where peer == nil or peer.connection == nil.
  • Collects stats, connection state, and TURN IDs.
  • Resets disableRecBtnPeers after exporting it.

This addresses the earlier nil-dereference risk when some peers haven’t finished starting up.


658-736: Watcher globals and initialConnectionWatcher remain fragile (currently unused)

watchers and watcherMutex are package-level and never reset, and initialConnectionWatcher still dereferences peer.connection without checking for nil (even though it now uses peersMu). Right now the call site is commented out, so this is dead code, but if re-enabled:

  • You risk cross-session leakage in watchers (old entries persisting).
  • A panic if peer.connection is nil when the watcher runs.

Consider either removing this workaround entirely for the debug build, or:

  • Moving watchers into PeerManager and clearing it per game/session.
  • Adding a peer != nil && peer.connection != nil guard before calling ConnectionState().

828-886: preparePeerForManualReconn can panic when no TURN servers are configured

preparePeerForManualReconn assumes len(p.turnServer) > 0:

  • numOfTurns := len(p.turnServer)
  • totalCombinations := numOfTurns * numOfTurns
  • Normalization with modulo totalCombinations
  • Indexing p.turnServer[...]

If numOfTurns == 0 (e.g., misconfigured session or all URLs filtered out by the adapter’s TURN filtering), you’ll hit a divide-by-zero and/or index-out-of-range panic as soon as a manual recon is requested.

Guard this explicitly:

 func (p *PeerManager) preparePeerForManualReconn(playerId uint) {
@@
-    if ok {
-        peer.numOfManualReconns += 1
-        peer.peerSpecificTurn = nil
-
-        numOfTurns := len(p.turnServer)
-        totalCombinations := numOfTurns * numOfTurns
+    if ok {
+        numOfTurns := len(p.turnServer)
+        if numOfTurns == 0 {
+            applog.Error("Manual reconnection requested but no TURN servers are configured",
+                zap.String("peer", peerAsString))
+            p.peersMu.Unlock()
+            return
+        }
+
+        peer.numOfManualReconns++
+        peer.peerSpecificTurn = nil
+
+        totalCombinations := numOfTurns * numOfTurns

This makes the invariant explicit and avoids hard panics when TURN is unavailable.

🧹 Nitpick comments (5)
webrtc/peer.go (1)

418-435: Drop-counter logic works but log formatting could be simplified

The new drop counters avoid log spam by warning only every ~100 dropped packets, which is sensible. You might consider switching to structured logging fields (zap.Int("droppedTotal", ...), zap.Uint("peerId", ...)) instead of string concatenation to keep logs more queryable, but this is optional.

adapter/adapter.go (3)

188-211: Nickname caching now avoids map races; minor style tweak possible

The userNicknames map is now updated under mu and GetNicknames returns a snapshot copy while still holding the lock, which fixes the earlier concurrent-read/write-on-map issue. You might simplify GetNicknames slightly with defer mu.Unlock() for clearer control flow, but functionally this is solid.

Also applies to: 248-256


92-133: TURN filtering is reasonable but consider handling “all filtered out” explicitly

The new TURN construction correctly preserves credentials while:

  • Dropping the hardcoded IP 139.162.142.250.
  • Optionally stripping UDP URLs for region == "RU".
  • Excluding TCP 3478/5349 while keeping 80/443.

However, it’s possible to end up with turnServ.URLs empty for some servers, or even turnServer effectively empty after filtering. Given manual reconnection logic in webrtc.PeerManager assumes at least one TURN server, consider:

  • Logging a warning when a server ends up with no URLs, and
  • Logging (or failing) early if all URLs are filtered out so the caller can avoid manual recon flows that assume TURN availability.

24-27: Region constant and GetRegion helper are straightforward

Hard-coding region = "Global" and exposing it via GetRegion() is simple and self-contained. Just ensure this remains debug-only or is made configurable before any production use.

Also applies to: 258-260

webrtc/peer_manager.go (1)

67-89: forceTurnRelay is now always true—verify this is intended for debug-only builds

NewPeerManager now hard-codes forceTurnRelay: true, ignoring any launcherInfo.ForceTurnRelay setting. That means all peers will use TURN relay (or attempt to) even when a direct path would be preferable.

If this is only for the debug window experiment, that’s fine; otherwise, you likely want to respect the launcher’s configuration or make this behavior explicitly toggleable.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a5c948 and bbb8ef9.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (7)
  • adapter/adapter.go (5 hunks)
  • applog/applog.go (3 hunks)
  • cmd/faf-adapter/main.go (1 hunks)
  • dbgwnd/dbgwnd.go (1 hunks)
  • go.mod (2 hunks)
  • webrtc/peer.go (4 hunks)
  • webrtc/peer_manager.go (16 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • cmd/faf-adapter/main.go
  • applog/applog.go
  • dbgwnd/dbgwnd.go
🧰 Additional context used
🧬 Code graph analysis (3)
webrtc/peer.go (2)
applog/context.go (1)
  • FromContext (10-12)
applog/applog.go (2)
  • Debug (110-123)
  • Warn (95-108)
adapter/adapter.go (1)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
webrtc/peer_manager.go (3)
adapter/adapter.go (1)
  • PeerManager (20-20)
launcher/info.go (1)
  • Info (8-20)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (3)
go.mod (1)

50-78: This review comment references code that does not exist in the repository and should be retracted.

The go.mod file is 38 lines total and contains only WebRTC and compression dependencies (pion/webrtc/v4, klauspost/compress, golang.org/x/net, etc.). It contains zero references to modernc.org, tk9.0, or any of the ~40 indirect dependencies mentioned in this review.

The code snippet showing modernc.org/tk9.0 and related packages at lines 50-78 is not present in the actual file, which ends at line 38. A search of the entire codebase confirms no tk9.0 or modernc.org references exist anywhere.

All concerns raised about CGo wrappers, supply chain expansion, platform-specific compatibility, and CVEs in libexpat/libfontconfig/libc are not applicable to this changeset. The review appears to have been written against a different PR, repository state, or file.

Likely an incorrect or invalid review comment.

webrtc/peer.go (2)

49-57: New peer metadata fields and initialization look consistent

The added fields for creation time, manual reconnection, TURN IDs, and packet-drop tracking are coherently defined on Peer and correctly initialized in CreatePeer. They integrate cleanly with existing state and do not introduce obvious correctness issues in this file.

Also applies to: 133-153


391-408: Send error logging downgrade to Debug is reasonable

Changing failed data-channel send logging from Error to Debug reduces noise for transient issues while preserving visibility when needed. The surrounding control flow is unchanged and still exits cleanly on channel close/disable.

Comment thread go.mod
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove or replace golang.org/x/exp dependency.

The golang.org/x/exp package is explicitly marked as unstable and unsuitable for production use. While this PR is currently draft/exploratory, remove this dependency before merging to production.

If the code depends on features from x/exp, migrate to stable equivalents:

  • If using exp/slices: switch to the standard slices package (Go 1.22+, and go.mod specifies 1.25.5).
  • If using other exp features: evaluate stable alternatives or inline minimal implementations.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
webrtc/peer_manager.go (2)

345-351: GetAllPeerIds reads p.peers without holding peersMu.

This method iterates over p.peers without acquiring the mutex, while other methods modify the map under peersMu. This can cause a concurrent map read/write panic.

🔎 Proposed fix
 func (p *PeerManager) GetAllPeerIds() []uint {
+	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
 	ids := make([]uint, 0, len(p.peers))
 	for id := range p.peers {
 		ids = append(ids, id)
 	}
 	return ids
 }

357-363: removePeer accesses p.peers without holding peersMu.

This method reads and deletes from p.peers without mutex protection, risking concurrent map access panics.

🔎 Proposed fix
 func (p *PeerManager) removePeer(playerId uint) {
+	p.peersMu.Lock()
+	defer p.peersMu.Unlock()
 	peer := p.peers[playerId]
 	if peer != nil {
 		_ = peer.Close()
 		delete(p.peers, playerId)
 	}
 }
♻️ Duplicate comments (15)
dbgwnd/dbgwnd.go (9)

109-116: Closure captures loop variable by reference — all buttons will trigger with i=15.

The closure func() { reconnectBtnClicked(i) } captures i by reference. All 15 buttons will call reconnectBtnClicked(15) after the loop completes.

🔎 Proposed fix
 	for i := 0; i < 15; i++ {
-		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(i) }))
+		idx := i // capture current value
+		but := Button(Txt("RECONNECT"), Font("Helvetica", 6, "bold"), Command(func() { reconnectBtnClicked(idx) }))
 		reconnectButtons = append(reconnectButtons, but)

205-221: Checkbutton comparisons use wrong string value.

Tcl/Tk stores canonical boolean values as "1" and "0", not "true" and "false". With Onvalue(true), Offvalue(false), the comparisons at lines 207 and 216 will never match.

🔎 Proposed fix
 func rawLogsChkButtonPressed(){
 	clearLogWindow = true
-	if rawLogsChkButton.Variable() == "true" {
+	if rawLogsChkButton.Variable() == "1" {
 		showRawLogs = true
 	} else {
 		showRawLogs = false
 	}
 }

 func hideDebugChkButtonPressed(){
 	clearLogWindow = true
-	if hideDebugChkButton.Variable() == "true" {
+	if hideDebugChkButton.Variable() == "1" {
 		hideDebug = true
 	} else {
 		hideDebug = false
 	}
 }

235-236: strconv.Atoi errors silently ignored.

If parsing fails, playerID will be 0, which may trigger unintended behavior when calling HandleManualNoRelayReconnRequest(0) or HandleManualReconnectRequest(0).

🔎 Proposed fix for both functions
 // In reconnNoRelaytBtnClicked:
-			playerID, _ := strconv.Atoi(sortedIds[buttonID])
+			playerID, err := strconv.Atoi(sortedIds[buttonID])
+			if err != nil {
+				return
+			}
 			pm.HandleManualNoRelayReconnRequest(uint(playerID))

 // In reconnectBtnClicked:
-		playerID, _ := strconv.Atoi(sortedIds[buttonID])
+		playerID, err := strconv.Atoi(sortedIds[buttonID])
+		if err != nil {
+			applog.Error("UI: failed to parse player ID", zap.Error(err))
+			return
+		}
 		pm.HandleManualReconnectRequest(uint(playerID))

Also applies to: 246-247


259-264: Listbox selection uses only the first byte of the ID string.

selectedId[0] indexes a single byte (e.g. "12"49 which is the byte for '1'). The map lookup and title will use the wrong value.

🔎 Proposed fix
 func infoListIdSelected(){
 	selected := connIdListBox.Curselection()

 	if len(selected) > 0 {
 		selectedId := connIdListBox.Get( strconv.Itoa(selected[0]))
 		if len(selectedId) > 0 {
-			stat, ok := allPeersStats[selectedId[0]]
+			stat, ok := allPeersStats[selectedId]

 			if ok {
 				connInfoView.Clear()
-				title := "Connection ID: " + selectedId[0] + " Time: " + time.Now().Format("15:04:05")
+				title := "Connection ID: " + selectedId + " Time: " + time.Now().Format("15:04:05")

 				connInfoView.Insert(END,  title, "", "\n")

291-291: ParseFloat error and split length not validated.

If logView.Yview() returns an unexpected format, the split may have fewer than 2 elements causing a panic, or ParseFloat may fail leaving textYpos at 0.0, breaking autoscroll logic.

🔎 Proposed fix
-	textYpos,_ := strconv.ParseFloat(strings.Split(logView.Yview(), " ")[1], 64)
+	textYpos := 1.0 // default to bottom
+	parts := strings.Split(logView.Yview(), " ")
+	if len(parts) >= 2 {
+		if parsed, err := strconv.ParseFloat(parts[1], 64); err == nil {
+			textYpos = parsed
+		}
+	}

354-356: Log parsing bails out on blank lines instead of continuing.

Every log line ends with a newline, so strings.Split yields empty strings between entries. Because the loop breaks when len(st) < 5, the first blank entry stops processing forever—new log lines never reach the UI.

🔎 Proposed fix
 		// there should be no empty lines and if there is one
 		// then something went wrong and we skip this try
 		if len(st) < 5 {
-			break
+			continue
 		}
 		rawLogCount += 1

 		// ... and in the formatted branch:
 		if len(st) < 5 {
-			break
+			continue
 		}

Also applies to: 367-369


577-582: Entire playerNames map is overwritten when a single name is missing.

At line 581, when a player name is not found, the entire map is replaced with a fresh copy from adapter.GetNicknames(), losing any previously cached names that might not be returned again.

🔎 Proposed fix
 	playerName := "-"
 	if name, ok := playerNames[id]; ok {
 		playerName = name
 	} else {
-		playerNames = adapter.GetNicknames()
+		allNames := adapter.GetNicknames()
+		for k, v := range allNames {
+			playerNames[k] = v
+		}
+		if name, ok := playerNames[id]; ok {
+			playerName = name
+		}
 	}

599-608: reconnectButtonsEnabled is incremented incorrectly and never set to the intended value.

The variable is incremented on every iteration (reconnectButtonsEnabled += 1), resulting in reconnectButtonsEnabled growing unboundedly across refreshes instead of reflecting numOfIds.

🔎 Proposed fix
 	if reconnectButtonsEnabled != numOfIds {
 		// normal buttons
 		for i,b := range(reconnectButtons){
-			reconnectButtonsEnabled += 1
 			if i < numOfIds {
 				b.Configure((State("normal")))
 			} else {
 				b.Configure((State("disabled")))
 			}
 		}
+		reconnectButtonsEnabled = numOfIds

620-626: New peer detection may miss peer replacements.

The condition if len(oldPeer) < len(sortedIds) only detects when the peer count increases. If a peer disconnects and another connects in the same refresh cycle, the new peer won't be detected.

🔎 Proposed fix
-	if len(oldPeer) < len(sortedIds) {
-		for i,_ := range(sortedIds) {
-			if _, ok := oldPeer[sortedIds[i]]; !ok {
-				disabledBtnsTimers[sortedIds[i]] = disableRecButnOnNewConnect
-			}
+	for i,_ := range(sortedIds) {
+		if _, ok := oldPeer[sortedIds[i]]; !ok {
+			disabledBtnsTimers[sortedIds[i]] = disableRecButnOnNewConnect
+			oldPeer[sortedIds[i]] = true
 		}
 	}
webrtc/peer_manager.go (6)

21-22: Global TURN state will persist across adapter restarts.

allTurnServersUrls and turnsNameById are package-level variables that accumulate data. If the adapter restarts within the same process, stale entries persist and new URLs are appended, causing duplicates.

🔎 Proposed fix
 func (p *PeerManager) saveTurnsURLs() {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
 	for i, _ := range(p.turnServer) {

108-133: TURN URL parsing is fragile with hardcoded .com assumption.

The code at line 114 assumes all TURN URLs contain .com and will produce incorrect or empty names for other TLDs (.org, .io, .net, country codes, etc.).

🔎 Proposed fix using net/url
+import "net/url"

 func (p *PeerManager) saveTurnsURLs() {
+	allTurnServersUrls = nil
+	turnsNameById = make(map[int]string)
 	for i, _ := range(p.turnServer) {
 		for _,urlStr := range(p.turnServer[i].URLs) {
 			_, ok := turnsNameById[i+1]
 			if !ok {
-				preparedName := " "
-				index := strings.Index(url, ".com")
-				// ... existing fragile logic ...
+				preparedName := "unknown"
+				if u, err := url.Parse(urlStr); err == nil && u.Host != "" {
+					host := strings.Split(u.Host, ":")[0]
+					parts := strings.Split(host, ".")
+					if len(parts) >= 2 {
+						preparedName = parts[len(parts)-2]
+					} else if len(parts) == 1 {
+						preparedName = parts[0]
+					}
+					if len(preparedName) > 6 {
+						preparedName = preparedName[:6]
+					}
+				}
 				turnsNameById[i+1] = preparedName
 			}

563-563: Global manualReconnectionMessage is mutated in place, causing race conditions.

The global manualReconnectionMessage has its RecipientID modified at line 774, then sent at line 775. If two manual reconnections happen concurrently for different peers, the RecipientID can be overwritten before SendEvent is called.

🔎 Proposed fix — create a local copy
-			manualReconnectionMessage.BaseEvent.RecipientID = &playerId
-			err := p.icebreakerClient.SendEvent(manualReconnectionMessage)
+			msg := manualReconnectionMessage // shallow copy
+			msg.BaseEvent.RecipientID = &playerId
+			err := p.icebreakerClient.SendEvent(msg)

Also applies to: 774-775


660-661: Global watchers map risks cross-session contamination.

The watchers map and watcherMutex are package-level. If the adapter is restarted or multiple games run, stale entries persist and could prevent new watchers from being created for the same player IDs.

Consider making watchers an instance field of PeerManager or clearing it when a new PeerManager is created.


696-702: initialConnectionWatcher can panic on nil peer.connection.

Inside the loop at line 698, peer.connection.ConnectionState() is called without checking if peer.connection is nil. During early setup or after closure, this will panic.

🔎 Proposed fix
 	p.peersMu.Lock()
 	if peer, ok := p.peers[playerId]; ok {
-		if peer.connection.ConnectionState().String() == "connected"{
+		if peer.connection != nil && peer.connection.ConnectionState() == webrtc.PeerConnectionStateConnected {
 			peerConnnectedSecondsTotal += 1
 		}
 	}
 	p.peersMu.Unlock()

839-845: preparePeerForManualReconn can panic when no TURN servers are configured.

If len(p.turnServer) == 0, then totalCombinations is 0, and the modulo operation at line 845 will cause a division-by-zero panic.

🔎 Proposed fix
 	if ok {
+		numOfTurns := len(p.turnServer)
+		if numOfTurns == 0 {
+			applog.Error("Manual reconnection requested but no TURN servers are configured", zap.String("peer", peerAsString))
+			p.peersMu.Unlock()
+			return
+		}
+
 		peer.numOfManualReconns += 1
 		peer.peerSpecificTurn = nil
-
-		numOfTurns := len(p.turnServer)
 		totalCombinations := numOfTurns * numOfTurns
🧹 Nitpick comments (1)
dbgwnd/dbgwnd.go (1)

25-51: Excessive package-level mutable state creates testing and maintenance challenges.

The file uses ~25 package-level variables for UI state, widget references, and data caches. This makes the code difficult to test, reason about, and could cause issues if the window were ever recreated or if tests need isolation.

Consider encapsulating this state within a struct (e.g., DebugWindow) in future iterations.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbb8ef9 and c68e5cb.

📒 Files selected for processing (2)
  • dbgwnd/dbgwnd.go (1 hunks)
  • webrtc/peer_manager.go (16 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
dbgwnd/dbgwnd.go (3)
webrtc/peer_manager.go (1)
  • PeerManager (51-65)
adapter/adapter.go (4)
  • PeerManager (20-20)
  • GetPeerManager (244-246)
  • GetRegion (258-260)
  • GetNicknames (248-256)
applog/applog.go (3)
  • Info (79-93)
  • Error (125-138)
  • GetLogFilePath (375-377)
webrtc/peer_manager.go (1)
icebreaker/events.go (3)
  • CandidatesMessage (50-54)
  • BaseEvent (23-28)
  • EventKindCandidates (14-14)
🔇 Additional comments (1)
webrtc/peer_manager.go (1)

86-86: forceTurnRelay is hardcoded to true.

The constructor now unconditionally sets forceTurnRelay: true, ignoring any session configuration. This may be intentional for debugging, but verify this is the desired behavior for production.

Comment thread webrtc/peer_manager.go
Comment on lines +581 to +593
if manualReconnectionMessage.GameID == 0 {
manualReconnectionMessage = icebreaker.CandidatesMessage{
BaseEvent: icebreaker.BaseEvent{
EventType: icebreaker.EventKindCandidates,
GameID: p.gameId,
SenderID: p.localUserId,
RecipientID: &remotePeer,
},
Session: description,
Candidates: candidates,
}
manualReconnectionMessage.Candidates = make([]webrtc.ICECandidate, 0)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

manualReconnectionMessage template populated with first peer's data only.

The global template is populated once (when manualReconnectionMessage.GameID == 0) with the first peer's remotePeer and candidates. The Session and original Candidates fields remain from that first peer. While Candidates is cleared to empty, Session may be stale or incorrect for subsequent manual reconnections.

Consider constructing a fresh message each time in HandleManualReconnectRequest rather than using a global template.

🤖 Prompt for AI Agents
In webrtc/peer_manager.go around lines 581-593, the code reuses a single global
manualReconnectionMessage template initialized only when GameID==0, causing
stale Session (and potentially other fields) to be sent to subsequent peers;
instead, create a new icebreaker.CandidatesMessage inside
HandleManualReconnectRequest for each remotePeer: populate BaseEvent with the
current p.gameId, p.localUserId, and the current remotePeer, set Session to the
current description and Candidates to the current candidates (or an empty slice
if needed), and stop mutating/depending on the shared manualReconnectionMessage
variable so each manual reconnect builds and sends a fresh, per-peer message.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants