462 lines
9.2 KiB
Go
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
|
|
}
|