/* * ============================================================================ * Projekt.....: rs2322tcp * Datei.......: session.go * Copyright (C) 2026 Dieter Lang * * SPDX-License-Identifier: GPL-3.0-or-later * * Beschreibung: * Verwaltung des Lebenszyklus einer Client-Session einschließlich * der zugehörigen Netzwerkressourcen, Data-Listener und aktiven * Datenverbindungen. * ============================================================================ */ package server import ( "fmt" "io" "net" "sync" ) /////////////////////////////////////////////////////////////////////////////// // Session /////////////////////////////////////////////////////////////////////////////// // Session represents one active client connection. // // All resources belonging to one client connection are associated with // the session. This includes the control connection, dynamic data // listeners and active data connections belonging to the configured // remote devices. type Session struct { id uint64 conn net.Conn mu sync.Mutex closed bool resources []io.Closer dataListeners map[string]*DataListener dataConnections map[string]*DataConnection } /////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////// // NewSession creates a new client session. func NewSession(id uint64, conn net.Conn) (*Session, error) { if conn == nil { return nil, fmt.Errorf("connection is nil") } if id == 0 { return nil, fmt.Errorf("session ID must not be zero") } return &Session{ id: id, conn: conn, resources: make([]io.Closer, 0), dataListeners: make(map[string]*DataListener), dataConnections: make(map[string]*DataConnection), }, nil } /////////////////////////////////////////////////////////////////////////////// // Properties /////////////////////////////////////////////////////////////////////////////// // ID returns the unique session ID. func (s *Session) ID() uint64 { if s == nil { return 0 } return s.id } // Conn returns the control connection belonging to the session. func (s *Session) Conn() net.Conn { if s == nil { return nil } s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil } return s.conn } // IsClosed reports whether the session has already been closed. func (s *Session) IsClosed() bool { if s == nil { return true } s.mu.Lock() defer s.mu.Unlock() return s.closed } /////////////////////////////////////////////////////////////////////////////// // Resources /////////////////////////////////////////////////////////////////////////////// // AddResource adds a resource to the session. // // The resource must implement io.Closer. It will automatically be closed // when the session is closed. // // If the session is already closed, the resource is closed immediately // and an error is returned. func (s *Session) AddResource(resource io.Closer) error { if s == nil { if resource != nil { _ = resource.Close() } return fmt.Errorf("session is nil") } if resource == nil { return fmt.Errorf("resource is nil") } s.mu.Lock() if s.closed { s.mu.Unlock() _ = resource.Close() return fmt.Errorf("session is already closed") } s.resources = append(s.resources, resource) s.mu.Unlock() return nil } // ResourceCount returns the number of resources currently owned by the // session. // // This method is primarily useful for diagnostics and tests. func (s *Session) ResourceCount() int { if s == nil { return 0 } s.mu.Lock() defer s.mu.Unlock() return len(s.resources) } /////////////////////////////////////////////////////////////////////////////// // Data listeners /////////////////////////////////////////////////////////////////////////////// // AddDataListener associates a dynamic data listener with a remote device. // // The listener becomes a session resource and is therefore automatically // closed when the session is closed. func (s *Session) AddDataListener( deviceID string, listener *DataListener, ) error { if s == nil { if listener != nil { _ = listener.Close() } return fmt.Errorf("session is nil") } if deviceID == "" { if listener != nil { _ = listener.Close() } return fmt.Errorf("device ID is empty") } if listener == nil { return fmt.Errorf("data listener is nil") } s.mu.Lock() if s.closed { s.mu.Unlock() _ = listener.Close() return fmt.Errorf("session is already closed") } if _, exists := s.dataListeners[deviceID]; exists { s.mu.Unlock() _ = listener.Close() return fmt.Errorf( "data listener already exists for device %q", deviceID, ) } s.resources = append(s.resources, listener) s.dataListeners[deviceID] = listener s.mu.Unlock() return nil } // DataListener returns the data listener associated with a device. // // The second return value reports whether a listener exists for the // specified device. func (s *Session) DataListener( deviceID string, ) (*DataListener, bool) { if s == nil { return nil, false } s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil, false } listener, ok := s.dataListeners[deviceID] return listener, ok } // DataPort returns the dynamic TCP data port associated with a device. // // It returns 0 if the device has no listener or the session is closed. func (s *Session) DataPort(deviceID string) int { listener, ok := s.DataListener(deviceID) if !ok { return 0 } return listener.Port() } // DataListenerCount returns the number of data listeners currently // associated with the session. func (s *Session) DataListenerCount() int { if s == nil { return 0 } s.mu.Lock() defer s.mu.Unlock() return len(s.dataListeners) } /////////////////////////////////////////////////////////////////////////////// // Active data connections /////////////////////////////////////////////////////////////////////////////// // AddDataConnection associates an active data connection with a remote // device. // // Only one active data connection is allowed per device. The data // connection becomes a session resource and is therefore automatically // closed when the session is closed. func (s *Session) AddDataConnection( deviceID string, connection *DataConnection, ) error { if s == nil { if connection != nil { _ = connection.Close() } return fmt.Errorf("session is nil") } if deviceID == "" { if connection != nil { _ = connection.Close() } return fmt.Errorf("device ID is empty") } if connection == nil { return fmt.Errorf("data connection is nil") } s.mu.Lock() if s.closed { s.mu.Unlock() _ = connection.Close() return fmt.Errorf("session is already closed") } if _, exists := s.dataConnections[deviceID]; exists { s.mu.Unlock() _ = connection.Close() return fmt.Errorf( "data connection already exists for device %q", deviceID, ) } s.resources = append(s.resources, connection) s.dataConnections[deviceID] = connection s.mu.Unlock() return nil } // DataConnection returns the active data connection associated with a // device. // // The second return value reports whether an active connection exists. func (s *Session) DataConnection( deviceID string, ) (*DataConnection, bool) { if s == nil { return nil, false } s.mu.Lock() defer s.mu.Unlock() if s.closed { return nil, false } connection, ok := s.dataConnections[deviceID] return connection, ok } // RemoveDataConnection removes an active data connection from the // session. // // The connection is closed before it is removed from the session. // Removing a connection that is not registered is harmless. func (s *Session) RemoveDataConnection(deviceID string) error { if s == nil { return nil } s.mu.Lock() connection, exists := s.dataConnections[deviceID] if !exists { s.mu.Unlock() return nil } delete(s.dataConnections, deviceID) for i, resource := range s.resources { if resource == connection { s.resources = append( s.resources[:i], s.resources[i+1:]..., ) break } } s.mu.Unlock() return connection.Close() } // DataConnectionCount returns the number of currently active data // connections. func (s *Session) DataConnectionCount() int { if s == nil { return 0 } s.mu.Lock() defer s.mu.Unlock() return len(s.dataConnections) } /////////////////////////////////////////////////////////////////////////////// // Lifecycle /////////////////////////////////////////////////////////////////////////////// // Close terminates the session and releases all resources belonging // to the session. // // Close is safe to call multiple times. // // Data listeners and other session resources are closed before the // control connection is closed. func (s *Session) Close() error { if s == nil { return nil } s.mu.Lock() if s.closed { s.mu.Unlock() return nil } s.closed = true conn := s.conn s.conn = nil resources := s.resources s.resources = nil s.dataListeners = nil s.dataConnections = nil s.mu.Unlock() var firstErr error for _, resource := range resources { if resource == nil { continue } if err := resource.Close(); err != nil && firstErr == nil { firstErr = err } } if conn != nil { if err := conn.Close(); err != nil && firstErr == nil { firstErr = err } } return firstErr }