mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-09 15:09:08 +02:00
[client] Keep the iOS ConnectionListener source-compatible
Adding OnStateChanged to the gomobile interface forces every Swift implementation to grow the method before the app builds again. Drop it from the iOS binding for now — the adapter satisfies the internal listener with a no-op and the legacy per-state callbacks keep firing — so the app upgrades on its own schedule. The state constants stay exported for that follow-up.
This commit is contained in:
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/netbirdio/netbird/client/internal/peer"
|
"github.com/netbirdio/netbird/client/internal/peer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client state values delivered via ConnectionListener.OnStateChanged,
|
// Client state values, re-exported as basic constants so gomobile emits them
|
||||||
// re-exported as basic constants so gomobile emits them into the generated
|
// into the generated bindings. They mirror peer.ClientState*: append-only,
|
||||||
// bindings. They mirror peer.ClientState*: append-only, never reorder.
|
// never reorder.
|
||||||
const (
|
const (
|
||||||
ClientStateDisconnected = int(peer.ClientStateDisconnected)
|
ClientStateDisconnected = int(peer.ClientStateDisconnected)
|
||||||
ClientStateConnected = int(peer.ClientStateConnected)
|
ClientStateConnected = int(peer.ClientStateConnected)
|
||||||
@@ -17,11 +17,13 @@ const (
|
|||||||
ClientStateNoNetwork = int(peer.ClientStateNoNetwork)
|
ClientStateNoNetwork = int(peer.ClientStateNoNetwork)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConnectionListener export internal Listener for mobile. It mirrors
|
// ConnectionListener export internal Listener for mobile.
|
||||||
// peer.Listener with OnStateChanged taking a plain int (one of the
|
//
|
||||||
// ClientState* constants), because gomobile cannot bind named types.
|
// It intentionally lacks OnStateChanged for now: adding a method to a gomobile
|
||||||
|
// interface breaks every Swift implementation, so the iOS app keeps building
|
||||||
|
// against the legacy per-state callbacks. A follow-up will extend it together
|
||||||
|
// with the app.
|
||||||
type ConnectionListener interface {
|
type ConnectionListener interface {
|
||||||
OnStateChanged(state int)
|
|
||||||
OnConnected()
|
OnConnected()
|
||||||
OnDisconnected()
|
OnDisconnected()
|
||||||
OnConnecting()
|
OnConnecting()
|
||||||
@@ -31,11 +33,11 @@ type ConnectionListener interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// connectionListenerAdapter adapts the gomobile-facing ConnectionListener to
|
// connectionListenerAdapter adapts the gomobile-facing ConnectionListener to
|
||||||
// peer.Listener, converting the typed state to the int the binding carries.
|
// peer.Listener.
|
||||||
type connectionListenerAdapter struct {
|
type connectionListenerAdapter struct {
|
||||||
ConnectionListener
|
ConnectionListener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a connectionListenerAdapter) OnStateChanged(state peer.ClientState) {
|
// OnStateChanged is dropped on iOS until the app adopts the state callback;
|
||||||
a.ConnectionListener.OnStateChanged(int(state))
|
// the legacy per-state callbacks continue to fire.
|
||||||
}
|
func (a connectionListenerAdapter) OnStateChanged(peer.ClientState) {}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
// Package netsweep cuts network-bound activity when the OS switches networks:
|
||||||
|
// a sweep closes the registered connections and aborts the in-flight dials, so
|
||||||
|
// their owners redial immediately instead of waiting for the old sockets to
|
||||||
|
// time out.
|
||||||
|
//
|
||||||
|
// A nil *Sweeper disables everything: all methods are nil-safe no-ops.
|
||||||
|
package netsweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// sweptConn deregisters itself from the sweeper when closed.
|
||||||
|
type sweptConn struct {
|
||||||
|
net.Conn
|
||||||
|
sweeper *Sweeper
|
||||||
|
id uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *sweptConn) Close() error {
|
||||||
|
c.sweeper.deregister(c.id)
|
||||||
|
return c.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweeper registers live connections and in-flight dials so Sweep can cut
|
||||||
|
// everything that started before the network changed.
|
||||||
|
type Sweeper struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
conns map[uint64]net.Conn
|
||||||
|
dials map[uint64]context.CancelFunc
|
||||||
|
nextID uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates an empty sweeper.
|
||||||
|
func New() *Sweeper {
|
||||||
|
return &Sweeper{
|
||||||
|
conns: make(map[uint64]net.Conn),
|
||||||
|
dials: make(map[uint64]context.CancelFunc),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapConn registers conn and returns a wrapper that deregisters it on Close.
|
||||||
|
func (s *Sweeper) WrapConn(conn net.Conn) net.Conn {
|
||||||
|
if s == nil {
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
id := s.nextID
|
||||||
|
s.nextID++
|
||||||
|
s.conns[id] = conn
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
return &sweptConn{Conn: conn, sweeper: s, id: id}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapDialContext derives a context that Sweep cancels. The returned release
|
||||||
|
// must be called when the dial finishes, typically deferred.
|
||||||
|
func (s *Sweeper) WrapDialContext(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
if s == nil {
|
||||||
|
return ctx, func() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
id := s.nextID
|
||||||
|
s.nextID++
|
||||||
|
s.dials[id] = cancel
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
release := func() {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.dials, id)
|
||||||
|
s.mu.Unlock()
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
return ctx, release
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sweep closes every registered connection, aborts every in-flight dial, and
|
||||||
|
// returns how many connections it closed.
|
||||||
|
func (s *Sweeper) Sweep() int {
|
||||||
|
if s == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
conns := s.conns
|
||||||
|
dials := s.dials
|
||||||
|
s.conns = make(map[uint64]net.Conn)
|
||||||
|
s.dials = make(map[uint64]context.CancelFunc)
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if len(dials) > 0 {
|
||||||
|
log.Debugf("aborting %d in-flight dials", len(dials))
|
||||||
|
for _, cancel := range dials {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, conn := range conns {
|
||||||
|
log.Debugf("sweeping connection %s -> %s", conn.LocalAddr(), conn.RemoteAddr())
|
||||||
|
if err := conn.Close(); err != nil {
|
||||||
|
log.Debugf("swept connection close error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(conns)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Sweeper) deregister(id uint64) {
|
||||||
|
s.mu.Lock()
|
||||||
|
delete(s.conns, id)
|
||||||
|
s.mu.Unlock()
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
package netsweep
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSweepClosesRegisteredConns(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
c1 := sweeper.WrapConn(connPair(t))
|
||||||
|
c2 := sweeper.WrapConn(connPair(t))
|
||||||
|
|
||||||
|
assert.Equal(t, 2, sweeper.Sweep(), "both live connections should be closed")
|
||||||
|
|
||||||
|
// The wrappers must report closed now.
|
||||||
|
buf := make([]byte, 1)
|
||||||
|
_, err := c1.Read(buf)
|
||||||
|
assert.Error(t, err, "first connection should be unusable after the sweep")
|
||||||
|
_, err = c2.Read(buf)
|
||||||
|
assert.Error(t, err, "second connection should be unusable after the sweep")
|
||||||
|
|
||||||
|
assert.Equal(t, 0, sweeper.Sweep(), "second sweep should find nothing")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseDeregisters(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
conn := sweeper.WrapConn(connPair(t))
|
||||||
|
require.NoError(t, conn.Close())
|
||||||
|
|
||||||
|
assert.Equal(t, 0, sweeper.Sweep(), "closed connection must leave the registry")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCloseIsIdempotent(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
conn := sweeper.WrapConn(connPair(t))
|
||||||
|
require.NoError(t, conn.Close())
|
||||||
|
assert.Error(t, conn.Close(), "double close surfaces the underlying error but must not panic")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSweepOnlyAffectsOlderConns(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
_ = sweeper.WrapConn(connPair(t))
|
||||||
|
assert.Equal(t, 1, sweeper.Sweep())
|
||||||
|
|
||||||
|
// A connection dialed after the sweep must survive until the next one.
|
||||||
|
_ = sweeper.WrapConn(connPair(t))
|
||||||
|
assert.Equal(t, 1, sweeper.Sweep(), "post-sweep connection belongs to the next sweep")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSweepAbortsInFlightDials(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
dialCtx, release := sweeper.WrapDialContext(context.Background())
|
||||||
|
defer release()
|
||||||
|
|
||||||
|
sweeper.Sweep()
|
||||||
|
|
||||||
|
assert.ErrorIs(t, dialCtx.Err(), context.Canceled, "sweep must cancel the in-flight dial context")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleasedDialIsNotAborted(t *testing.T) {
|
||||||
|
sweeper := New()
|
||||||
|
|
||||||
|
// Simulate a dial that finished before the sweep.
|
||||||
|
_, release := sweeper.WrapDialContext(context.Background())
|
||||||
|
release()
|
||||||
|
|
||||||
|
// A dial still in flight during the sweep.
|
||||||
|
pendingCtx, pendingRelease := sweeper.WrapDialContext(context.Background())
|
||||||
|
defer pendingRelease()
|
||||||
|
|
||||||
|
sweeper.Sweep()
|
||||||
|
assert.ErrorIs(t, pendingCtx.Err(), context.Canceled, "pending dial must be aborted")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilSweeperIsNoop(t *testing.T) {
|
||||||
|
var sweeper *Sweeper
|
||||||
|
|
||||||
|
conn := connPair(t)
|
||||||
|
assert.Equal(t, conn, sweeper.WrapConn(conn), "nil sweeper must return the conn unchanged")
|
||||||
|
assert.Equal(t, 0, sweeper.Sweep(), "nil sweeper closes nothing")
|
||||||
|
|
||||||
|
ctx, release := sweeper.WrapDialContext(context.Background())
|
||||||
|
release()
|
||||||
|
assert.NoError(t, ctx.Err(), "nil sweeper must not cancel the dial context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connPair dials a loopback TCP connection against a throwaway listener.
|
||||||
|
func connPair(t *testing.T) net.Conn {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := l.Close(); err != nil {
|
||||||
|
t.Logf("listener close error: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := net.Dial("tcp", l.Addr().String())
|
||||||
|
require.NoError(t, err)
|
||||||
|
return conn
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user