ascii-chat 0.6.0
Real-time terminal-based video chat with ASCII art conversion
Loading...
Searching...
No Matches
Server Main Entry Point

🚀 Server initialization, signal handling, and connection management More...

Files

file  main.c
 🖥️ Server main entry point: multi-client connection manager with per-client rendering threads (60fps video + 172fps audio)
 
file  main.h
 ascii-chat Server Mode Entry Point Header
 

Data Structures

struct  server_context_t
 Server context - encapsulates all server state. More...
 

Typedefs

typedef struct server_context_t server_context_t
 Server context - encapsulates all server state.
 

Variables

bool g_server_encryption_enabled = false
 Global flag indicating if server encryption is enabled.
 
private_key_t g_server_private_key = {0}
 Global server private key.
 
public_key_t g_client_whitelist [MAX_CLIENTS] = {0}
 Global client public key whitelist.
 
size_t g_num_whitelisted_clients = 0
 Number of whitelisted clients.
 

Detailed Description

🚀 Server initialization, signal handling, and connection management

Server Main Entry Point

Overview

The server main entry point orchestrates the complete server lifecycle, from initialization and socket binding through the multi-client connection accept loop to graceful shutdown. It serves as the conductor of the ascii-chat server's modular architecture, coordinating all subsystems while maintaining real-time performance and thread safety guarantees.

Implementation: src/server/main.c, src/server/main.h

Key Responsibilities:

  • Platform initialization (Windows/POSIX compatibility)
  • Logging and configuration setup
  • Network socket creation and binding (IPv4 and IPv6)
  • Global resource initialization (audio mixer, buffer pools, crypto)
  • Background thread management (statistics logging)
  • Main connection accept loop with client lifecycle management
  • Signal handling for graceful shutdown (SIGINT, SIGTERM)
  • Resource cleanup and thread coordination

Architecture

Initialization Sequence

The server initialization follows a strict dependency order:

// 1. Parse command-line options
// 2. Initialize platform (Windows: Winsock, timer resolution)
// 3. Initialize logging system
asciichat_shared_init("server.log");
// 4. Initialize crypto system (keys, whitelist)
init_server_crypto();
// 5. Initialize lock debugging system
lock_debug_init();
// 6. Initialize statistics system
// 7. Setup signal handlers (SIGINT, SIGTERM, SIGUSR1)
platform_signal(SIGINT, sigint_handler);
platform_signal(SIGTERM, sigterm_handler);
// 8. Initialize synchronization primitives
// 9. Initialize client hash table for O(1) lookups
g_client_manager.clients_by_id = NULL; // uthash head pointer
// 10. Initialize audio mixer for multi-client audio
// 11. Bind listening sockets (IPv4 and/or IPv6)
listenfd = bind_and_listen(ipv4_address, AF_INET, port);
listenfd6 = bind_and_listen(ipv6_address, AF_INET6, port);
// 12. Start statistics logger thread
asciichat_thread_create(&g_stats_logger_thread, stats_logger_thread, NULL);
#define AUDIO_SAMPLE_RATE
Audio sample rate (48kHz professional quality, Opus-compatible)
mixer_t * mixer_create(int max_sources, int sample_rate)
Create a new audio mixer.
Definition mixer.c:218
asciichat_error_t asciichat_shared_init(const char *default_log_filename, bool is_client)
Initialize common subsystems shared by client and server.
Definition common.c:64
#define MAX_CLIENTS
Maximum possible clients (static array size) - actual runtime limit set by –max-clients (1-32)
Definition limits.h:23
asciichat_error_t options_init(int argc, char **argv)
Initialize options by parsing command-line arguments.
Definition options.c:144
@ MODE_SERVER
Server mode - network server options.
Definition options.h:427
signal_handler_t platform_signal(int sig, signal_handler_t handler)
Set a signal handler.
int mutex_init(mutex_t *mutex)
Initialize a mutex.
asciichat_error_t platform_init(void)
Initialize platform-specific subsystems.
int rwlock_init(rwlock_t *lock)
Initialize a read-write lock.
int asciichat_thread_create(asciichat_thread_t *thread, void *(*func)(void *), void *arg)
Create a new thread.
mixer_t * g_audio_mixer
Global audio mixer from main.c.
rwlock_t g_client_manager_rwlock
Reader-writer lock protecting the global client manager.
client_manager_t g_client_manager
Global client manager singleton - central coordination point.
void * stats_logger_thread(void *arg)
Main statistics collection and reporting thread function.
Definition stats.c:332
int stats_init(void)
Initialize the stats mutex.
Definition stats.c:184
client_info_t * clients_by_id
uthash head pointer for O(1) client_id -> client_info_t* lookups
mutex_t mutex
Legacy mutex (mostly replaced by rwlock)

Main Connection Loop

The main connection loop orchestrates the complete multi-client lifecycle:

while (!atomic_load(&g_server_should_exit)) {
// PHASE 1: Cleanup disconnected clients (free slots)
// Collect cleanup tasks under read lock
for (int i = 0; i < MAX_CLIENTS; i++) {
if (!client->active && client->receive_thread_initialized) {
cleanup_tasks[cleanup_count++] = client->client_id;
}
}
// Process cleanup without holding lock
for (int i = 0; i < cleanup_count; i++) {
asciichat_thread_join(&cleanup_tasks[i].receive_thread, NULL);
remove_client(cleanup_tasks[i].client_id);
}
// PHASE 2: Accept new connection (with timeout)
fd_set read_fds;
socket_select(max_fd + 1, &read_fds, NULL, NULL, &timeout);
socket_t client_sock = accept_with_timeout(accept_listenfd,
&client_addr, &client_len, 0);
// PHASE 3: Add client (spawns all per-client threads)
int client_id = add_client(client_sock, client_ip, client_port);
if (client_id < 0) {
socket_close(client_sock); // Reject connection
continue;
}
// Connection accepted - client threads now running
log_info("Client %d connected, total clients: %d", client_id,
}
#define log_info(...)
Log an INFO message.
int accept_with_timeout(socket_t listenfd, struct sockaddr *addr, socklen_t *addrlen, int timeout_seconds)
Accept connection with timeout.
Definition network.c:346
int socket_t
Socket handle type (POSIX: int)
Definition socket.h:50
#define rwlock_rdlock(lock)
Acquire a read lock (with debug tracking in debug builds)
Definition rwlock.h:194
int socket_close(socket_t sock)
Close a socket.
int socket_select(socket_t max_fd, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
Select sockets for I/O readiness.
int asciichat_thread_join(asciichat_thread_t *thread, void **retval)
Wait for a thread to complete (blocking)
#define rwlock_rdunlock(lock)
Release a read lock (with debug tracking in debug builds)
Definition rwlock.h:231
atomic_bool g_server_should_exit
Global shutdown flag from main.c.
int add_client(server_context_t *server_ctx, socket_t socket, const char *client_ip, int port)
int remove_client(server_context_t *server_ctx, uint32_t client_id)
int client_count
Current number of active clients.

Critical Loop Design:

  • Cleanup MUST happen before accept() to free connection slots
  • Timeout on accept() allows checking shutdown flag
  • Dual-stack support (IPv4 and IPv6 sockets checked simultaneously)
  • Graceful degradation if add_client() fails

Signal Handling

SIGINT Handler (Ctrl+C)

The SIGINT handler implements signal-safe shutdown:

static void sigint_handler(int sigint) {
(void)sigint;
static int sigint_count = 0;
sigint_count++;
// Second Ctrl+C forces immediate exit
if (sigint_count > 1) {
_exit(1);
}
// First Ctrl+C: Graceful shutdown
atomic_store(&g_server_should_exit, true);
printf("SIGINT received - shutting down server...\n");
// Close listening sockets to interrupt accept()
socket_close(atomic_load(&listenfd));
socket_close(atomic_load(&listenfd6));
// NOTE: Do NOT access client data structures in signal handler
// Signal handlers cannot safely use mutexes or complex data structures
// Main thread will handle client cleanup when it detects g_server_should_exit
}

Signal Safety Strategy:

  • Only async-signal-safe functions (atomic operations, write())
  • No mutex operations (can deadlock if main thread holds mutex)
  • No malloc/free (heap corruption risk if interrupted)
  • Minimal work - set flag and close sockets to interrupt blocking I/O

Why Socket Closure: Without closing sockets, threads remain blocked in:

  • accept() in main loop (waiting for new connections)
  • recv() in client receive threads (waiting for packets)
  • send() in client send threads (if network is slow)

Closing sockets causes these functions to return with errors, allowing threads to check g_server_should_exit and exit gracefully.

SIGTERM Handler

SIGTERM is sent by process managers (systemd, Docker) for graceful termination:

static void sigterm_handler(int sigterm) {
(void)sigterm;
atomic_store(&g_server_should_exit, true);
printf("SIGTERM received - shutting down server...\n");
// Return immediately - main thread handles cleanup
}

Conservative Approach:

  • More conservative than SIGINT handler
  • Relies on main thread for complete cleanup
  • Focuses on minimal flag setting
  • Prevents partial states during automated shutdown

SIGUSR1 Handler (Lock Debugging)

SIGUSR1 triggers lock debugging output for troubleshooting deadlocks:

static void sigusr1_handler(int sigusr1) {
(void)sigusr1;
lock_debug_trigger_print(); // Signal-safe lock state dump
}

This allows external triggering of lock debugging without modifying running server.

Shutdown Sequence

The shutdown sequence ensures complete resource cleanup:

// 1. Signal all threads to exit
atomic_store(&g_server_should_exit, true);
// 2. Wake up blocked threads
static_cond_broadcast(&g_shutdown_cond);
// 3. Close all client sockets (unblock receive threads)
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client->socket != INVALID_SOCKET_VALUE) {
socket_close(client->socket); // Interrupts blocking recv()
}
}
// 4. Wait for stats logger thread
asciichat_thread_join(&g_stats_logger_thread, NULL);
// 5. Remove all clients (joins all worker threads)
for (int i = 0; i < client_count; i++) {
remove_client(clients_to_remove[i]);
}
// 6. Cleanup resources in reverse dependency order
// Clean up uthash table (iterate and delete all entries)
HASH_ITER(hh, g_client_manager.clients_by_id, ...);
lock_debug_cleanup();
socket_close(listenfd);
socket_close(listenfd6);
data_buffer_pool_cleanup_global();
void mixer_destroy(mixer_t *mixer)
Destroy a mixer and free all resources.
Definition mixer.c:346
void log_destroy(void)
Destroy the logging system and close log file.
int rwlock_destroy(rwlock_t *lock)
Destroy a read-write lock.
#define INVALID_SOCKET_VALUE
Invalid socket value (POSIX: -1)
Definition socket.h:52
int mutex_destroy(mutex_t *mutex)
Destroy a mutex.
void simd_caches_destroy_all(void)
Destroy all SIMD caches.
static_cond_t g_shutdown_cond
Global shutdown condition variable for waking blocked threads.
void stats_cleanup(void)
Cleanup the stats mutex.
Definition stats.c:195

Cleanup Guarantees:

  • All threads joined before resource cleanup
  • Resources cleaned in reverse dependency order
  • No memory leaks or hanging processes
  • Deterministic shutdown regardless of thread state

IPv4 and IPv6 Dual-Stack Support

The server supports simultaneous binding to both IPv4 and IPv6:

Binding Logic:

  • Default (0 arguments): Bind to both 127.0.0.1 (IPv4) and ::1 (IPv6) for localhost dual-stack
  • If one IPv4 address provided: Bind only to that IPv4 address
  • If one IPv6 address provided: Bind only to that IPv6 address
  • If two addresses provided: Bind to both addresses (full dual-stack, order-independent)

Connection Acceptance:

  • Uses select() to check both sockets simultaneously
  • Accepts connections from either socket as they arrive
  • Handles IPv4-mapped IPv6 addresses transparently
  • Logs connection source (IPv4, IPv6, or IPv4-mapped)

Integration Points

With Client Management (client.c):

With Protocol Handler (protocol.c):

  • Not directly called (used by client receive threads)
  • Provides global state (g_server_should_exit)

With Stream Generation (stream.c):

  • Not directly called (used by render threads)
  • Render threads call stream functions independently

With Render Threads (render.c):

With Statistics (stats.c):

  • Starts stats_logger_thread_func() in background
  • Waits for stats thread during shutdown

With Cryptography (crypto.c):

  • Calls init_server_crypto() during initialization
  • Manages global server crypto state (g_server_private_key, g_client_whitelist)

Modular Design Philosophy

The original server.c became unmaintainable at 2408+ lines, making it:

  • Too large for LLM context windows (limited AI-assisted development)
  • Difficult for humans to navigate and understand
  • Slow to compile and modify
  • Hard to isolate bugs and add new features

The modular design enables:

  • Faster development cycles: Smaller compilation units compile faster
  • Better IDE support: Jump-to-definition, IntelliSense work reliably
  • Easier testing: Isolated components can be unit tested
  • Future extensibility: New protocols, renderers, optimizations can be added cleanly

Module Boundaries:

  • main.c: Server lifecycle, signal handling, connection acceptance
  • client.c: Per-client lifecycle, threading, state management
  • protocol.c: Packet processing, protocol state, media storage
  • stream.c: Video mixing, ASCII conversion, frame generation
  • render.c: Render thread management, rate limiting, timing
  • stats.c: Performance monitoring, resource tracking, reporting
  • crypto.c: Cryptographic handshake, key exchange, session encryption

Error Handling

Initialization Errors:

  • FATAL macros used for unrecoverable errors (crypto, network, platform)
  • Detailed error context via asciichat_errno system
  • Clean resource cleanup before exit

Connection Errors:

  • Graceful degradation: Failed client addition doesn't crash server
  • Socket closure on failure: Prevents resource leaks
  • Error logging: Detailed diagnostics for troubleshooting

Runtime Errors:

  • Connection loss handled by client management (not fatal)
  • Thread failures logged but don't crash server
  • Resource allocation failures handled gracefully

Concurrency Model

Main Thread Responsibilities:

  • Accept new connections (blocking with timeout)
  • Manage client lifecycle (add/remove)
  • Handle disconnection cleanup
  • Coordinate graceful shutdown

Background Thread Responsibilities:

  • Per-client receive: Handle incoming packets (client.c)
  • Per-client send: Manage outgoing packets (client.c)
  • Per-client video render: Generate ASCII frames (render.c)
  • Per-client audio render: Mix audio streams (render.c)
  • Stats logger: Monitor server performance (stats.c)

Thread Safety:

  • Global RWLock (g_client_manager_rwlock) protects client array
  • Atomic variables (g_server_should_exit) for shutdown coordination
  • Snapshot pattern for client state access
  • Lock ordering protocol prevents deadlocks

See Concurrency Documentation for complete details.

Best Practices

DO:

  • Always check g_server_should_exit in loops
  • Use proper lock ordering (RWLock → per-client mutex)
  • Use snapshot pattern for client state access
  • Close sockets in signal handlers to interrupt blocking I/O
  • Wait for all threads during shutdown

DON'T:

  • Don't access client data structures in signal handlers
  • Don't acquire locks in signal handlers (can deadlock)
  • Don't call malloc/free in signal handlers (heap corruption risk)
  • Don't skip client cleanup (memory leaks)
  • Don't ignore lock ordering protocol (deadlock risk)
See also
src/server/main.c
src/server/main.h
Server Overview
Client Management
Concurrency Documentation

Typedef Documentation

◆ server_context_t

#include <main.h>

Server context - encapsulates all server state.

This structure holds all server-wide state that was previously stored in global variables. It's passed to client handlers via tcp_server user_data, reducing global state and improving modularity.

DESIGN RATIONALE:

  • Reduces global state: All server state in one place
  • Improves testability: Can create multiple independent server instances
  • Better encapsulation: Clear ownership of resources
  • Thread-safe: Context is read-only after initialization

LIFETIME:

Variable Documentation

◆ g_client_whitelist

public_key_t g_client_whitelist[MAX_CLIENTS] = {0}

#include <main.c>

Global client public key whitelist.

Array of public keys for clients that are authorized to connect to the server. Used for client authentication when whitelist mode is enabled. Sized to hold up to MAX_CLIENTS entries.

Note
Only used when client authentication is enabled
Accessed from crypto.c for client authentication

Definition at line 355 of file server/main.c.

355{0};

Referenced by server_crypto_handshake(), and server_main().

◆ g_num_whitelisted_clients

size_t g_num_whitelisted_clients = 0

#include <main.c>

Number of whitelisted clients.

Tracks the current number of entries in g_client_whitelist that are valid and active. Used to iterate the whitelist and check authorization.

Definition at line 365 of file server/main.c.

Referenced by server_crypto_handshake(), and server_main().

◆ g_server_encryption_enabled

bool g_server_encryption_enabled = false

#include <main.c>

Global flag indicating if server encryption is enabled.

Set to true when the server is configured to use encryption and has successfully loaded a private key. Controls whether the server performs cryptographic handshakes with clients.

Note
Accessed from crypto.c for server-side crypto operations

Definition at line 330 of file server/main.c.

Referenced by server_crypto_handshake(), and server_main().

◆ g_server_private_key

private_key_t g_server_private_key = {0}

#include <main.c>

Global server private key.

Stores the server's private key loaded from the key file. Used for cryptographic handshakes and packet encryption/decryption. Initialized during server startup from the configured key file path.

Note
Accessed from crypto.c for server-side crypto operations

Definition at line 342 of file server/main.c.

342{0};

Referenced by server_crypto_handshake(), and server_main().