rs2322tcp/internal/server/session.go
2026-08-10 10:38:14 +02:00

462 lines
9.2 KiB
Go

/*
* ============================================================================
* 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
}