feat(runtime): add integration hardening and diagnostics

This commit is contained in:
2026-09-05 21:56:09 +03:00
parent ffdaa299bc
commit c730cb818d
19 changed files with 956 additions and 160 deletions
@@ -68,6 +68,7 @@ maze32_lobby_result_t maze32_lobby_restore(maze32_lobby_t* lobby, int connection
void maze32_lobby_disconnect(maze32_lobby_t* lobby, int connection_id);
maze32_lobby_result_t maze32_lobby_start(maze32_lobby_t* lobby, int connection_id, uint64_t now_ms);
int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms);
void maze32_lobby_reset_match(maze32_lobby_t* lobby);
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
int connection_id);
unsigned int maze32_lobby_connected_observers(const maze32_lobby_t* lobby);
@@ -21,6 +21,7 @@ typedef enum {
MAZE32_MESSAGE_START,
MAZE32_MESSAGE_MOVE,
MAZE32_MESSAGE_DROP_KEY,
MAZE32_MESSAGE_RESTART,
MAZE32_MESSAGE_HEARTBEAT,
MAZE32_MESSAGE_SUGGEST_NAME,
} maze32_message_type_t;
+10
View File
@@ -195,6 +195,16 @@ int maze32_lobby_activate_if_due(maze32_lobby_t* lobby, uint64_t now_ms) {
return 1;
}
void maze32_lobby_reset_match(maze32_lobby_t* lobby) {
if (lobby == NULL) {
return;
}
lobby->phase = MAZE32_LOBBY_WAITING;
lobby->countdown_started_ms = 0U;
lobby->activation_ms = 0U;
lobby->revision++;
}
const maze32_session_t* maze32_lobby_session_for_connection(const maze32_lobby_t* lobby,
int connection_id) {
if (lobby == NULL) {
@@ -190,6 +190,9 @@ static maze32_message_type_t message_type(const char* value) {
if (strcmp(value, "drop_key") == 0) {
return MAZE32_MESSAGE_DROP_KEY;
}
if (strcmp(value, "restart") == 0) {
return MAZE32_MESSAGE_RESTART;
}
if (strcmp(value, "heartbeat") == 0) {
return MAZE32_MESSAGE_HEARTBEAT;
}
@@ -35,36 +35,33 @@ typedef struct {
typedef struct {
EventGroupHandle_t events;
SemaphoreHandle_t operation_mutex;
esp_netif_t *station_netif;
esp_netif_t *access_point_netif;
esp_netif_t* station_netif;
esp_netif_t* access_point_netif;
volatile maze32_network_state_t state;
volatile int station_wait_active;
unsigned int retry_count;
char access_point_ssid[24];
maze32_reconfiguration_guard_fn reconfiguration_guard;
void *guard_context;
void* guard_context;
httpd_handle_t server;
} maze32_network_t;
esp_err_t maze32_network_init(maze32_network_t *network,
maze32_reconfiguration_guard_fn guard,
void *guard_context);
esp_err_t maze32_network_start(maze32_network_t *network);
esp_err_t maze32_network_apply_credentials(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials);
esp_err_t maze32_network_reset_credentials(maze32_network_t *network);
void maze32_network_retry_saved_credentials(void *context);
void maze32_network_return_to_provisioning(void *context);
esp_err_t maze32_network_scan(maze32_network_t *network,
maze32_wifi_scan_result_t *results,
size_t capacity,
size_t *count);
maze32_network_state_t maze32_network_state(const maze32_network_t *network);
const char *maze32_network_state_name(maze32_network_state_t state);
const char *maze32_network_access_point_ssid(const maze32_network_t *network);
esp_err_t maze32_network_ip_address(const maze32_network_t *network,
char *output,
esp_err_t maze32_network_init(maze32_network_t* network, maze32_reconfiguration_guard_fn guard,
void* guard_context);
esp_err_t maze32_network_start(maze32_network_t* network);
esp_err_t maze32_network_apply_credentials(maze32_network_t* network,
const maze32_wifi_credentials_t* credentials);
esp_err_t maze32_network_reset_credentials(maze32_network_t* network);
void maze32_network_retry_saved_credentials(void* context);
void maze32_network_return_to_provisioning(void* context);
esp_err_t maze32_network_scan(maze32_network_t* network, maze32_wifi_scan_result_t* results,
size_t capacity, size_t* count);
maze32_network_state_t maze32_network_state(const maze32_network_t* network);
const char* maze32_network_state_name(maze32_network_state_t state);
const char* maze32_network_access_point_ssid(const maze32_network_t* network);
esp_err_t maze32_network_ip_address(const maze32_network_t* network, char* output,
size_t output_size);
esp_err_t maze32_provisioning_register(httpd_handle_t server, maze32_network_t *network);
esp_err_t maze32_provisioning_register(httpd_handle_t server, maze32_network_t* network);
#ifdef __cplusplus
}
+82 -80
View File
@@ -13,42 +13,61 @@
#define MAZE32_STATION_RETRY_LIMIT 3U
#define MAZE32_STATION_TIMEOUT_MS 12000U
static const char *TAG = "maze32_network";
static maze32_network_t *active_network;
static const char* TAG = "maze32_network";
static maze32_network_t* active_network;
static void set_state(maze32_network_t *network, maze32_network_state_t state)
{
static void set_state(maze32_network_t* network, maze32_network_state_t state) {
network->state = state;
ESP_LOGI(TAG, "Network state: %s", maze32_network_state_name(state));
}
static void network_event(void *argument,
esp_event_base_t event_base,
int32_t event_id,
void *event_data)
{
static void network_event(void* argument, esp_event_base_t event_base, int32_t event_id,
void* event_data) {
(void)event_data;
maze32_network_t *network = argument;
maze32_network_t* network = argument;
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START &&
network->state == MAZE32_NETWORK_STATION_CONNECTING) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED &&
network->state == MAZE32_NETWORK_STATION_CONNECTING) {
(network->state == MAZE32_NETWORK_STATION_CONNECTING ||
network->state == MAZE32_NETWORK_STATION_CONNECTED)) {
if (network->state == MAZE32_NETWORK_STATION_CONNECTED) {
set_state(network, MAZE32_NETWORK_STATION_CONNECTING);
network->retry_count = 0U;
}
if (network->retry_count < MAZE32_STATION_RETRY_LIMIT) {
++network->retry_count;
ESP_LOGW(TAG, "Station connection failed; retry %u/%u",
network->retry_count, MAZE32_STATION_RETRY_LIMIT);
ESP_LOGW(TAG, "Station connection failed; retry %u/%u", network->retry_count,
MAZE32_STATION_RETRY_LIMIT);
esp_wifi_connect();
} else {
} else if (network->station_wait_active) {
xEventGroupSetBits(network->events, MAZE32_FAILED_BIT);
} else if (network->server != NULL) {
set_state(network, MAZE32_NETWORK_PROVISIONING);
esp_err_t result =
httpd_queue_work(network->server, maze32_network_return_to_provisioning, network);
if (result != ESP_OK) {
ESP_LOGE(TAG, "Could not schedule runtime fallback: %s", esp_err_to_name(result));
set_state(network, MAZE32_NETWORK_ERROR);
}
} else {
set_state(network, MAZE32_NETWORK_ERROR);
}
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
xEventGroupSetBits(network->events, MAZE32_CONNECTED_BIT);
if (network->station_wait_active) {
xEventGroupSetBits(network->events, MAZE32_CONNECTED_BIT);
} else if (network->state == MAZE32_NETWORK_STATION_CONNECTING) {
network->retry_count = 0U;
set_state(network, MAZE32_NETWORK_STATION_CONNECTED);
char address[16];
if (maze32_network_ip_address(network, address, sizeof(address)) == ESP_OK) {
ESP_LOGI(TAG, "Runtime station reconnect succeeded: IP=%s", address);
}
}
}
}
static esp_err_t stop_wifi(void)
{
static esp_err_t stop_wifi(void) {
esp_err_t result = esp_wifi_stop();
if (result == ESP_ERR_WIFI_NOT_STARTED || result == ESP_ERR_WIFI_NOT_INIT) {
return ESP_OK;
@@ -56,8 +75,7 @@ static esp_err_t stop_wifi(void)
return result;
}
static esp_err_t start_provisioning(maze32_network_t *network)
{
static esp_err_t start_provisioning(maze32_network_t* network) {
set_state(network, MAZE32_NETWORK_PROVISIONING);
esp_err_t result = stop_wifi();
if (result != ESP_OK) {
@@ -89,9 +107,8 @@ static esp_err_t start_provisioning(maze32_network_t *network)
return result;
}
static esp_err_t connect_station(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials)
{
static esp_err_t connect_station(maze32_network_t* network,
const maze32_wifi_credentials_t* credentials) {
esp_err_t result = stop_wifi();
if (result != ESP_OK) {
return result;
@@ -113,19 +130,19 @@ static esp_err_t connect_station(maze32_network_t *network,
result = esp_wifi_set_config(WIFI_IF_STA, &station);
}
if (result == ESP_OK) {
network->station_wait_active = 1;
result = esp_wifi_start();
}
if (result != ESP_OK) {
network->station_wait_active = 0;
ESP_LOGE(TAG, "Station startup failed: %s", esp_err_to_name(result));
return start_provisioning(network);
}
EventBits_t bits = xEventGroupWaitBits(
network->events,
MAZE32_CONNECTED_BIT | MAZE32_FAILED_BIT,
pdTRUE,
pdFALSE,
pdMS_TO_TICKS(MAZE32_STATION_TIMEOUT_MS));
EventBits_t bits =
xEventGroupWaitBits(network->events, MAZE32_CONNECTED_BIT | MAZE32_FAILED_BIT, pdTRUE,
pdFALSE, pdMS_TO_TICKS(MAZE32_STATION_TIMEOUT_MS));
network->station_wait_active = 0;
if ((bits & MAZE32_CONNECTED_BIT) != 0U) {
set_state(network, MAZE32_NETWORK_STATION_CONNECTED);
char address[16];
@@ -139,22 +156,18 @@ static esp_err_t connect_station(maze32_network_t *network,
return start_provisioning(network);
}
static int save_credentials(const maze32_wifi_credentials_t *credentials, void *context)
{
static int save_credentials(const maze32_wifi_credentials_t* credentials, void* context) {
(void)context;
return maze32_config_store_save(credentials) == ESP_OK ? 0 : -1;
}
static int clear_credentials(void *context)
{
static int clear_credentials(void* context) {
(void)context;
return maze32_config_store_clear() == ESP_OK ? 0 : -1;
}
esp_err_t maze32_network_init(maze32_network_t *network,
maze32_reconfiguration_guard_fn guard,
void *guard_context)
{
esp_err_t maze32_network_init(maze32_network_t* network, maze32_reconfiguration_guard_fn guard,
void* guard_context) {
if (network == NULL || guard == NULL || active_network != NULL) {
return ESP_ERR_INVALID_ARG;
}
@@ -173,8 +186,8 @@ esp_err_t maze32_network_init(maze32_network_t *network,
if (result != ESP_OK) {
return result;
}
snprintf(network->access_point_ssid, sizeof(network->access_point_ssid),
"maze32-%02X%02X", mac[4], mac[5]);
snprintf(network->access_point_ssid, sizeof(network->access_point_ssid), "maze32-%02X%02X",
mac[4], mac[5]);
wifi_init_config_t wifi_configuration = WIFI_INIT_CONFIG_DEFAULT();
result = esp_wifi_init(&wifi_configuration);
@@ -196,8 +209,7 @@ esp_err_t maze32_network_init(maze32_network_t *network,
return result;
}
esp_err_t maze32_network_start(maze32_network_t *network)
{
esp_err_t maze32_network_start(maze32_network_t* network) {
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return ESP_ERR_INVALID_ARG;
}
@@ -214,16 +226,13 @@ esp_err_t maze32_network_start(maze32_network_t *network)
return result;
}
esp_err_t maze32_network_apply_credentials(maze32_network_t *network,
const maze32_wifi_credentials_t *credentials)
{
esp_err_t maze32_network_apply_credentials(maze32_network_t* network,
const maze32_wifi_credentials_t* credentials) {
if (network == NULL) {
return ESP_ERR_INVALID_ARG;
}
int result = maze32_wifi_config_apply(credentials,
network->reconfiguration_guard,
save_credentials,
network->guard_context);
int result = maze32_wifi_config_apply(credentials, network->reconfiguration_guard,
save_credentials, network->guard_context);
if (result == -1) {
return ESP_ERR_INVALID_ARG;
}
@@ -233,13 +242,11 @@ esp_err_t maze32_network_apply_credentials(maze32_network_t *network,
return result == 0 ? ESP_OK : ESP_FAIL;
}
esp_err_t maze32_network_reset_credentials(maze32_network_t *network)
{
esp_err_t maze32_network_reset_credentials(maze32_network_t* network) {
if (network == NULL) {
return ESP_ERR_INVALID_ARG;
}
int result = maze32_wifi_config_reset(network->reconfiguration_guard,
clear_credentials,
int result = maze32_wifi_config_reset(network->reconfiguration_guard, clear_credentials,
network->guard_context);
if (result == -2) {
return ESP_ERR_INVALID_STATE;
@@ -247,9 +254,8 @@ esp_err_t maze32_network_reset_credentials(maze32_network_t *network)
return result == 0 ? ESP_OK : ESP_FAIL;
}
void maze32_network_retry_saved_credentials(void *context)
{
maze32_network_t *network = context;
void maze32_network_retry_saved_credentials(void* context) {
maze32_network_t* network = context;
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return;
}
@@ -265,9 +271,8 @@ void maze32_network_retry_saved_credentials(void *context)
xSemaphoreGive(network->operation_mutex);
}
void maze32_network_return_to_provisioning(void *context)
{
maze32_network_t *network = context;
void maze32_network_return_to_provisioning(void* context) {
maze32_network_t* network = context;
if (network == NULL || xSemaphoreTake(network->operation_mutex, portMAX_DELAY) != pdTRUE) {
return;
}
@@ -278,11 +283,8 @@ void maze32_network_return_to_provisioning(void *context)
xSemaphoreGive(network->operation_mutex);
}
esp_err_t maze32_network_scan(maze32_network_t *network,
maze32_wifi_scan_result_t *results,
size_t capacity,
size_t *count)
{
esp_err_t maze32_network_scan(maze32_network_t* network, maze32_wifi_scan_result_t* results,
size_t capacity, size_t* count) {
if (network == NULL || results == NULL || count == NULL || capacity == 0U ||
capacity > MAZE32_WIFI_SCAN_LIMIT ||
xSemaphoreTake(network->operation_mutex, pdMS_TO_TICKS(1000U)) != pdTRUE) {
@@ -313,34 +315,34 @@ esp_err_t maze32_network_scan(maze32_network_t *network,
return result;
}
maze32_network_state_t maze32_network_state(const maze32_network_t *network)
{
maze32_network_state_t maze32_network_state(const maze32_network_t* network) {
return network == NULL ? MAZE32_NETWORK_ERROR : network->state;
}
const char *maze32_network_state_name(maze32_network_state_t state)
{
const char* maze32_network_state_name(maze32_network_state_t state) {
switch (state) {
case MAZE32_NETWORK_BOOTING: return "booting";
case MAZE32_NETWORK_STATION_CONNECTING: return "station_connecting";
case MAZE32_NETWORK_STATION_CONNECTED: return "station_connected";
case MAZE32_NETWORK_PROVISIONING: return "provisioning";
case MAZE32_NETWORK_ERROR: return "error";
default: return "unknown";
case MAZE32_NETWORK_BOOTING:
return "booting";
case MAZE32_NETWORK_STATION_CONNECTING:
return "station_connecting";
case MAZE32_NETWORK_STATION_CONNECTED:
return "station_connected";
case MAZE32_NETWORK_PROVISIONING:
return "provisioning";
case MAZE32_NETWORK_ERROR:
return "error";
default:
return "unknown";
}
}
const char *maze32_network_access_point_ssid(const maze32_network_t *network)
{
const char* maze32_network_access_point_ssid(const maze32_network_t* network) {
return network == NULL ? "" : network->access_point_ssid;
}
esp_err_t maze32_network_ip_address(const maze32_network_t *network,
char *output,
size_t output_size)
{
if (network == NULL || output == NULL || output_size < 16U ||
network->station_netif == NULL) {
esp_err_t maze32_network_ip_address(const maze32_network_t* network, char* output,
size_t output_size) {
if (network == NULL || output == NULL || output_size < 16U || network->station_netif == NULL) {
return ESP_ERR_INVALID_ARG;
}
esp_netif_ip_info_t information;
@@ -19,6 +19,8 @@ extern "C" {
#define MAZE32_GATEWAY_QUEUE_LIMIT 12U
#define MAZE32_GATEWAY_OUTGOING_LIMIT 768U
#define MAZE32_GATEWAY_RATE_LIMIT_PER_SECOND 20U
#define MAZE32_GATEWAY_RATE_LIMIT_CLOSE_THRESHOLD 40U
#define MAZE32_GATEWAY_PLAYER_LATENCY_BUDGET_MS 250U
typedef struct {
int active;
@@ -26,14 +28,36 @@ typedef struct {
uint32_t last_client_sequence;
uint64_t rate_window_started_ms;
unsigned int messages_in_window;
int closing;
} maze32_gateway_connection_t;
typedef struct {
int socket_fd;
size_t length;
int drop_if_busy;
char payload[MAZE32_GATEWAY_OUTGOING_LIMIT];
} maze32_gateway_outgoing_t;
typedef struct {
uint32_t connection_high_water;
uint32_t queue_high_water;
uint32_t messages_received;
uint32_t messages_sent;
uint32_t messages_dropped;
uint32_t send_failures;
uint32_t malformed_messages;
uint32_t oversized_messages;
uint32_t rate_limited_messages;
uint32_t accepted_game_commands;
uint32_t matches_started;
uint32_t matches_completed;
uint32_t matches_reset;
uint64_t maximum_handler_us;
uint64_t maximum_send_us;
size_t minimum_internal_free_bytes;
size_t minimum_internal_largest_block_bytes;
} maze32_gateway_metrics_t;
typedef struct {
httpd_handle_t server;
maze32_lobby_t lobby;
@@ -45,6 +69,8 @@ typedef struct {
unsigned int suggestion_index;
esp_timer_handle_t countdown_timer;
esp_timer_handle_t reconnect_timer;
esp_timer_handle_t finish_timer;
maze32_gateway_metrics_t metrics;
maze32_maze_t maze;
maze32_maze_workspace_t maze_workspace;
maze32_game_t game;
@@ -56,6 +82,8 @@ void maze32_gateway_set_server(maze32_gateway_t* gateway, httpd_handle_t server)
esp_err_t maze32_gateway_register(httpd_handle_t server, maze32_gateway_t* gateway);
void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd);
int maze32_gateway_can_reconfigure(const maze32_gateway_t* gateway);
size_t maze32_gateway_format_diagnostics(maze32_gateway_t* gateway, char* output,
size_t output_size);
#ifdef __cplusplus
}
+346 -33
View File
@@ -3,6 +3,7 @@
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <unistd.h>
@@ -20,6 +21,46 @@ static uint64_t now_ms(void) {
return (uint64_t)(esp_timer_get_time() / 1000);
}
static size_t internal_free_bytes(void) {
return heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
}
static size_t internal_largest_block_bytes(void) {
return heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
}
static unsigned int active_connection_count(const maze32_gateway_t* gateway) {
unsigned int count = 0U;
for (size_t index = 0U; index < MAZE32_GATEWAY_CONNECTION_LIMIT; ++index) {
if (gateway->connections[index].active) {
count++;
}
}
return count;
}
static void sample_resources(maze32_gateway_t* gateway, const char* label) {
size_t free_bytes = internal_free_bytes();
size_t largest_block = internal_largest_block_bytes();
if (gateway->metrics.minimum_internal_free_bytes == 0U ||
free_bytes < gateway->metrics.minimum_internal_free_bytes) {
gateway->metrics.minimum_internal_free_bytes = free_bytes;
}
if (gateway->metrics.minimum_internal_largest_block_bytes == 0U ||
largest_block < gateway->metrics.minimum_internal_largest_block_bytes) {
gateway->metrics.minimum_internal_largest_block_bytes = largest_block;
}
if (label != NULL) {
ESP_LOGI(TAG,
"M010 resources: label=%s active=%u queue=%u free=%u largest=%u min_free=%u "
"min_largest=%u",
label, active_connection_count(gateway), (unsigned int)gateway->outgoing_count,
(unsigned int)free_bytes, (unsigned int)largest_block,
(unsigned int)gateway->metrics.minimum_internal_free_bytes,
(unsigned int)gateway->metrics.minimum_internal_largest_block_bytes);
}
}
static maze32_gateway_connection_t* find_connection(maze32_gateway_t* gateway, int socket_fd) {
for (size_t index = 0U; index < MAZE32_GATEWAY_CONNECTION_LIMIT; ++index) {
if (gateway->connections[index].active &&
@@ -42,19 +83,14 @@ static maze32_gateway_connection_t* open_connection(maze32_gateway_t* gateway, i
connection->active = 1;
connection->socket_fd = socket_fd;
connection->rate_window_started_ms = now_ms();
unsigned int active_count = 0U;
for (size_t count_index = 0U; count_index < MAZE32_GATEWAY_CONNECTION_LIMIT;
++count_index) {
if (gateway->connections[count_index].active) {
active_count++;
}
unsigned int active_count = active_connection_count(gateway);
if (active_count > gateway->metrics.connection_high_water) {
gateway->metrics.connection_high_water = active_count;
}
ESP_LOGI(TAG, "WebSocket connected: fd=%d, active=%u/%u", socket_fd, active_count,
(unsigned int)MAZE32_GATEWAY_CONNECTION_LIMIT);
if (active_count == MAZE32_GATEWAY_CONNECTION_LIMIT) {
ESP_LOGI(TAG, "Six-client heap: free=%u bytes, largest=%u bytes",
(unsigned int)heap_caps_get_free_size(MALLOC_CAP_8BIT),
(unsigned int)heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
sample_resources(gateway, "six_clients");
}
return connection;
}
@@ -112,10 +148,16 @@ static uint32_t next_server_sequence(maze32_gateway_t* gateway) {
return gateway->server_sequence;
}
static int enqueue(maze32_gateway_t* gateway, int socket_fd, const char* payload) {
static int enqueue_with_policy(maze32_gateway_t* gateway, int socket_fd, const char* payload,
int drop_if_busy) {
size_t length = strlen(payload);
if (gateway->outgoing_count >= MAZE32_GATEWAY_QUEUE_LIMIT ||
length >= MAZE32_GATEWAY_OUTGOING_LIMIT) {
if (drop_if_busy) {
gateway->metrics.messages_dropped++;
} else {
gateway->metrics.send_failures++;
}
ESP_LOGW(TAG, "Bounded outgoing queue rejected a message for fd=%d", socket_fd);
return 0;
}
@@ -123,12 +165,33 @@ static int enqueue(maze32_gateway_t* gateway, int socket_fd, const char* payload
maze32_gateway_outgoing_t* entry = &gateway->outgoing[index];
entry->socket_fd = socket_fd;
entry->length = length;
entry->drop_if_busy = drop_if_busy;
memcpy(entry->payload, payload, length + 1U);
gateway->outgoing_count++;
if (gateway->outgoing_count > gateway->metrics.queue_high_water) {
gateway->metrics.queue_high_water = (uint32_t)gateway->outgoing_count;
}
return 1;
}
static void drain_queue(maze32_gateway_t* gateway) {
static int enqueue(maze32_gateway_t* gateway, int socket_fd, const char* payload) {
return enqueue_with_policy(gateway, socket_fd, payload, 0);
}
static void close_slow_connection(maze32_gateway_t* gateway, int socket_fd) {
maze32_gateway_connection_t* connection = find_connection(gateway, socket_fd);
if (connection == NULL || connection->closing) {
return;
}
connection->closing = 1;
ESP_LOGW(TAG, "Closing an unresponsive WebSocket: fd=%d", socket_fd);
if (httpd_sess_trigger_close(gateway->server, socket_fd) != ESP_OK) {
ESP_LOGW(TAG, "Could not schedule close for fd=%d", socket_fd);
}
}
static int drain_queue(maze32_gateway_t* gateway) {
int success = 1;
while (gateway->outgoing_count > 0U) {
maze32_gateway_outgoing_t* entry = &gateway->outgoing[gateway->outgoing_head];
httpd_ws_frame_t frame = {
@@ -138,14 +201,38 @@ static void drain_queue(maze32_gateway_t* gateway) {
.payload = (uint8_t*)entry->payload,
.len = entry->length,
};
esp_err_t result = httpd_ws_send_frame_async(gateway->server, entry->socket_fd, &frame);
if (result != ESP_OK) {
ESP_LOGW(TAG, "WebSocket send failed for fd=%d: %s", entry->socket_fd,
esp_err_to_name(result));
int ready = 1;
if (entry->drop_if_busy) {
fd_set write_set;
FD_ZERO(&write_set);
FD_SET(entry->socket_fd, &write_set);
struct timeval timeout = {0};
ready = select(entry->socket_fd + 1, NULL, &write_set, NULL, &timeout) > 0 &&
FD_ISSET(entry->socket_fd, &write_set);
}
if (!ready) {
gateway->metrics.messages_dropped++;
} else {
int64_t started_us = esp_timer_get_time();
esp_err_t result = httpd_ws_send_frame_async(gateway->server, entry->socket_fd, &frame);
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
if (elapsed_us > gateway->metrics.maximum_send_us) {
gateway->metrics.maximum_send_us = elapsed_us;
}
if (result == ESP_OK) {
gateway->metrics.messages_sent++;
} else {
gateway->metrics.send_failures++;
success = 0;
ESP_LOGW(TAG, "WebSocket send failed for fd=%d: %s", entry->socket_fd,
esp_err_to_name(result));
close_slow_connection(gateway, entry->socket_fd);
}
}
gateway->outgoing_head = (gateway->outgoing_head + 1U) % MAZE32_GATEWAY_QUEUE_LIMIT;
gateway->outgoing_count--;
}
return success;
}
static maze32_game_role_t game_role_for_session(const maze32_session_t* session) {
@@ -176,10 +263,13 @@ static void send_view(maze32_gateway_t* gateway, const maze32_session_t* session
return;
}
(void)written;
if (!enqueue(gateway, session->connection_id, payload)) {
int drop_if_busy = session->role == MAZE32_ROLE_OBSERVER && !full_observer_view;
if (!enqueue_with_policy(gateway, session->connection_id, payload, drop_if_busy)) {
return;
}
if (!drain_queue(gateway)) {
return;
}
drain_queue(gateway);
}
}
@@ -489,19 +579,82 @@ static void countdown_timer_callback(void* context) {
}
static void schedule_reconnect_deadline(maze32_gateway_t* gateway);
static void schedule_finish_deadline(maze32_gateway_t* gateway);
static const char* final_result_name(maze32_game_final_result_t result) {
switch (result) {
case MAZE32_GAME_FINAL_PLAYER_ONE:
return "player1";
case MAZE32_GAME_FINAL_PLAYER_TWO:
return "player2";
case MAZE32_GAME_FINAL_DRAW:
return "draw";
default:
return "none";
}
}
static void complete_match(maze32_gateway_t* gateway) {
char payload[48];
int written = snprintf(payload, sizeof(payload), "{\"result\":\"%s\"}",
final_result_name(gateway->game.final_result));
esp_timer_stop(gateway->finish_timer);
if (written >= 0 && (size_t)written < sizeof(payload)) {
broadcast_match_event(gateway, "match_result", payload);
}
broadcast_views(gateway, 0);
drain_queue(gateway);
gateway->metrics.matches_completed++;
sample_resources(gateway, "match_complete");
}
static void reconnect_deadline_work(void* context) {
maze32_gateway_t* gateway = context;
if (maze32_game_advance_time(&gateway->game, now_ms())) {
broadcast_match_event(gateway, "match_result", "{\"result\":\"draw\"}");
broadcast_views(gateway, 0);
drain_queue(gateway);
complete_match(gateway);
ESP_LOGI(TAG, "Player reconnect deadline expired; match ended in a draw");
return;
}
schedule_reconnect_deadline(gateway);
}
static void finish_deadline_work(void* context) {
maze32_gateway_t* gateway = context;
if (maze32_game_advance_time(&gateway->game, now_ms())) {
complete_match(gateway);
ESP_LOGI(TAG, "Authoritative finish window closed");
return;
}
schedule_finish_deadline(gateway);
}
static void finish_timer_callback(void* context) {
maze32_gateway_t* gateway = context;
if (gateway->server != NULL) {
esp_err_t result = httpd_queue_work(gateway->server, finish_deadline_work, gateway);
if (result != ESP_OK) {
ESP_LOGE(TAG, "Could not queue finish deadline: %s", esp_err_to_name(result));
}
}
}
static void schedule_finish_deadline(maze32_gateway_t* gateway) {
esp_timer_stop(gateway->finish_timer);
if (gateway->game.phase != MAZE32_GAME_PHASE_FINISH_PENDING ||
gateway->game.first_exited_player >= MAZE32_GAME_PLAYER_COUNT) {
return;
}
uint64_t exit_ms = gateway->game.exit_times_ms[gateway->game.first_exited_player];
uint64_t window_ms = MAZE32_GAME_DRAW_WINDOW_MS + 1U;
uint64_t deadline = exit_ms > UINT64_MAX - window_ms ? UINT64_MAX : exit_ms + window_ms;
uint64_t current_ms = now_ms();
uint64_t delay_ms = deadline > current_ms ? deadline - current_ms : 1U;
esp_err_t result = esp_timer_start_once(gateway->finish_timer, delay_ms * 1000U);
if (result != ESP_OK) {
ESP_LOGE(TAG, "Finish timer could not start: %s", esp_err_to_name(result));
}
}
static void reconnect_timer_callback(void* context) {
maze32_gateway_t* gateway = context;
if (gateway->server != NULL) {
@@ -597,11 +750,16 @@ static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_
? maze32_game_enqueue_move(&gateway->game, role, parse_direction(message->direction),
now_ms(), &command_sequence)
: maze32_game_enqueue_drop_key(&gateway->game, role, now_ms(), &command_sequence);
maze32_game_command_result_t processed = {.result = result};
if (result == MAZE32_GAME_RESULT_ACCEPTED) {
maze32_game_command_result_t processed = maze32_game_process_next(&gateway->game);
processed = maze32_game_process_next(&gateway->game);
result = processed.result;
}
int finished_now = processed.finished && gateway->game.phase == MAZE32_GAME_PHASE_FINISHED;
if (result != MAZE32_GAME_RESULT_ACCEPTED) {
if (finished_now) {
complete_match(gateway);
}
send_game_error(gateway, session->connection_id, message->request_id, result);
return;
}
@@ -622,9 +780,85 @@ static void handle_game_command(maze32_gateway_t* gateway, const maze32_session_
append_text(acknowledgement, sizeof(acknowledgement), &length, prefix)) {
enqueue(gateway, session->connection_id, acknowledgement);
}
gateway->metrics.accepted_game_commands++;
if (finished_now) {
complete_match(gateway);
return;
}
if (gateway->game.phase == MAZE32_GAME_PHASE_FINISH_PENDING) {
schedule_finish_deadline(gateway);
}
broadcast_views(gateway, 0);
}
static esp_err_t initialize_game(maze32_gateway_t* gateway) {
maze32_maze_config_t maze_config = maze32_maze_default_config();
maze32_maze_report_t maze_report;
if (maze32_maze_generate(&gateway->maze, &gateway->maze_workspace, &maze_config, esp_random(),
&maze_report) != MAZE32_MAZE_OK) {
ESP_LOGE(TAG, "Maze generation failed: %s", maze32_maze_result_name(maze_report.result));
return ESP_FAIL;
}
maze32_game_config_t game_config = maze32_game_default_config();
return maze32_game_init(&gateway->game, &gateway->maze, &game_config) ==
MAZE32_GAME_RESULT_ACCEPTED
? ESP_OK
: ESP_FAIL;
}
static void handle_restart(maze32_gateway_t* gateway, const maze32_session_t* session,
const maze32_client_message_t* message) {
if (session->role == MAZE32_ROLE_OBSERVER) {
ESP_LOGW(TAG, "Observer mutation rejected: fd=%d, type=restart", session->connection_id);
send_game_error(gateway, session->connection_id, message->request_id,
MAZE32_GAME_RESULT_READ_ONLY);
return;
}
if (session->player_slot != 1U) {
send_lobby_error(gateway, session->connection_id, message->request_id,
MAZE32_LOBBY_NOT_OWNER);
return;
}
if (gateway->game.phase != MAZE32_GAME_PHASE_FINISHED) {
send_error(gateway, session->connection_id, message->request_id, "invalid_phase",
"Новая гонка доступна после финиша текущей");
return;
}
if (!maze32_lobby_ready(&gateway->lobby)) {
send_lobby_error(gateway, session->connection_id, message->request_id,
MAZE32_LOBBY_NOT_READY);
return;
}
if (initialize_game(gateway) != ESP_OK) {
send_error(gateway, session->connection_id, message->request_id, "maze_generation_failed",
"Лабиринт отказался пересаживаться. Попробуйте ещё раз");
return;
}
esp_timer_stop(gateway->countdown_timer);
esp_timer_stop(gateway->reconnect_timer);
esp_timer_stop(gateway->finish_timer);
maze32_lobby_reset_match(&gateway->lobby);
gateway->metrics.matches_reset++;
char acknowledgement[192] = "";
char prefix[96];
size_t length = 0U;
int written = snprintf(prefix, sizeof(prefix),
"{\"v\":1,\"type\":\"ack\",\"seq\":%" PRIu32 ",\"requestId\":",
next_server_sequence(gateway));
if (written >= 0 && (size_t)written < sizeof(prefix) &&
append_text(acknowledgement, sizeof(acknowledgement), &length, prefix) &&
append_json_string(acknowledgement, sizeof(acknowledgement), &length,
message->request_id) &&
append_text(acknowledgement, sizeof(acknowledgement), &length,
",\"payload\":{\"command\":\"restart\"}}")) {
enqueue(gateway, session->connection_id, acknowledgement);
}
broadcast_match_event(gateway, "reset", "{}");
broadcast_snapshot(gateway);
drain_queue(gateway);
sample_resources(gateway, "match_reset");
}
static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_t* connection,
const maze32_client_message_t* message) {
int socket_fd = connection->socket_fd;
@@ -682,6 +916,9 @@ static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_
broadcast_match_event(gateway, "resume", "{}");
ESP_LOGI(TAG, "Player %u restored; match resumed", session->player_slot);
}
if (gateway->game.phase == MAZE32_GAME_PHASE_FINISH_PENDING) {
schedule_finish_deadline(gateway);
}
}
}
broadcast_snapshot(gateway);
@@ -711,6 +948,8 @@ static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_
send_lobby_error(gateway, socket_fd, message->request_id, result);
return;
}
gateway->metrics.matches_started++;
sample_resources(gateway, "match_start");
broadcast_snapshot(gateway);
broadcast_countdown(gateway);
esp_err_t timer_result =
@@ -724,6 +963,10 @@ static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_
handle_game_command(gateway, session, message);
return;
}
if (message->type == MAZE32_MESSAGE_RESTART) {
handle_restart(gateway, session, message);
return;
}
if (message->type == MAZE32_MESSAGE_HEARTBEAT) {
char response[MAZE32_GATEWAY_OUTGOING_LIMIT] = "";
size_t length = 0U;
@@ -741,7 +984,7 @@ static void handle_message(maze32_gateway_t* gateway, maze32_gateway_connection_
}
}
static esp_err_t websocket_handler(httpd_req_t* request) {
static esp_err_t websocket_handler_impl(httpd_req_t* request) {
maze32_gateway_t* gateway = request->user_ctx;
int socket_fd = httpd_req_to_sockfd(request);
maze32_gateway_connection_t* connection = find_connection(gateway, socket_fd);
@@ -758,7 +1001,9 @@ static esp_err_t websocket_handler(httpd_req_t* request) {
if (result != ESP_OK) {
return result;
}
gateway->metrics.messages_received++;
if (frame.len > MAZE32_PROTOCOL_MESSAGE_LIMIT) {
gateway->metrics.oversized_messages++;
ESP_LOGW(TAG, "Oversized WebSocket frame rejected: fd=%d, length=%u", socket_fd,
(unsigned int)frame.len);
shutdown(socket_fd, SHUT_RDWR);
@@ -780,8 +1025,13 @@ static esp_err_t websocket_handler(httpd_req_t* request) {
}
payload[frame.len] = '\0';
if (!rate_limit_allows(connection, now_ms())) {
gateway->metrics.rate_limited_messages++;
send_error(gateway, socket_fd, "", "rate_limited", "Слишком много команд");
drain_queue(gateway);
if (connection->messages_in_window > MAZE32_GATEWAY_RATE_LIMIT_CLOSE_THRESHOLD) {
ESP_LOGW(TAG, "Sustained rate-limit abuse; closing fd=%d", socket_fd);
close_slow_connection(gateway, socket_fd);
}
return ESP_OK;
}
@@ -789,6 +1039,7 @@ static esp_err_t websocket_handler(httpd_req_t* request) {
maze32_protocol_result_t parse_result =
maze32_protocol_parse((const char*)payload, frame.len, &message);
if (parse_result != MAZE32_PROTOCOL_OK) {
gateway->metrics.malformed_messages++;
send_error(gateway, socket_fd, "", maze32_protocol_result_name(parse_result),
"Пакет не прошёл проверку протокола");
drain_queue(gateway);
@@ -806,6 +1057,20 @@ static esp_err_t websocket_handler(httpd_req_t* request) {
return ESP_OK;
}
static esp_err_t websocket_handler(httpd_req_t* request) {
maze32_gateway_t* gateway = request->user_ctx;
int64_t started_us = esp_timer_get_time();
esp_err_t result = websocket_handler_impl(request);
uint64_t elapsed_us = (uint64_t)(esp_timer_get_time() - started_us);
if (gateway != NULL) {
if (elapsed_us > gateway->metrics.maximum_handler_us) {
gateway->metrics.maximum_handler_us = elapsed_us;
}
sample_resources(gateway, NULL);
}
return result;
}
static esp_err_t websocket_pre_handshake(httpd_req_t* request) {
maze32_gateway_t* gateway = request->user_ctx;
int socket_fd = httpd_req_to_sockfd(request);
@@ -822,17 +1087,7 @@ esp_err_t maze32_gateway_init(maze32_gateway_t* gateway) {
}
memset(gateway, 0, sizeof(*gateway));
maze32_lobby_init(&gateway->lobby);
maze32_maze_config_t maze_config = maze32_maze_default_config();
maze32_maze_report_t maze_report;
if (maze32_maze_generate(&gateway->maze, &gateway->maze_workspace, &maze_config, esp_random(),
&maze_report) != MAZE32_MAZE_OK) {
ESP_LOGE(TAG, "Initial maze generation failed: %s",
maze32_maze_result_name(maze_report.result));
return ESP_FAIL;
}
maze32_game_config_t game_config = maze32_game_default_config();
if (maze32_game_init(&gateway->game, &gateway->maze, &game_config) !=
MAZE32_GAME_RESULT_ACCEPTED) {
if (initialize_game(gateway) != ESP_OK) {
return ESP_FAIL;
}
const esp_timer_create_args_t timer_arguments = {
@@ -856,7 +1111,22 @@ esp_err_t maze32_gateway_init(maze32_gateway_t* gateway) {
result = esp_timer_create(&reconnect_timer_arguments, &gateway->reconnect_timer);
if (result != ESP_OK) {
esp_timer_delete(gateway->countdown_timer);
return result;
}
const esp_timer_create_args_t finish_timer_arguments = {
.callback = finish_timer_callback,
.arg = gateway,
.dispatch_method = ESP_TIMER_TASK,
.name = "finish_deadline",
.skip_unhandled_events = true,
};
result = esp_timer_create(&finish_timer_arguments, &gateway->finish_timer);
if (result != ESP_OK) {
esp_timer_delete(gateway->reconnect_timer);
esp_timer_delete(gateway->countdown_timer);
return result;
}
sample_resources(gateway, "startup");
return result;
}
@@ -912,6 +1182,9 @@ void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd) {
if (written >= 0 && (size_t)written < sizeof(payload)) {
broadcast_match_event(gateway, "pause", payload);
}
if (gateway->game.phase_before_pause == MAZE32_GAME_PHASE_FINISH_PENDING) {
esp_timer_stop(gateway->finish_timer);
}
schedule_reconnect_deadline(gateway);
ESP_LOGI(TAG, "Player %u disconnected; match paused for %u ms",
disconnected_slot, (unsigned int)MAZE32_GAME_RECONNECT_TIMEOUT_MS);
@@ -924,6 +1197,7 @@ void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd) {
broadcast_views(gateway, 0);
}
drain_queue(gateway);
sample_resources(gateway, NULL);
ESP_LOGI(TAG, "WebSocket disconnected: fd=%d", socket_fd);
}
}
@@ -933,3 +1207,42 @@ void maze32_gateway_close_socket(httpd_handle_t server, int socket_fd) {
int maze32_gateway_can_reconfigure(const maze32_gateway_t* gateway) {
return gateway != NULL && maze32_lobby_can_reconfigure(&gateway->lobby);
}
size_t maze32_gateway_format_diagnostics(maze32_gateway_t* gateway, char* output,
size_t output_size) {
if (gateway == NULL || output == NULL || output_size == 0U) {
return 0U;
}
sample_resources(gateway, NULL);
int written = snprintf(
output, output_size,
"{\"activeConnections\":%u,\"connectionHighWater\":%" PRIu32
",\"queueDepth\":%u,\"queueHighWater\":%" PRIu32 ",\"messagesReceived\":%" PRIu32
",\"messagesSent\":%" PRIu32 ",\"messagesDropped\":%" PRIu32 ",\"sendFailures\":%" PRIu32
",\"malformedMessages\":%" PRIu32 ",\"oversizedMessages\":%" PRIu32
",\"rateLimitedMessages\":%" PRIu32 ",\"acceptedGameCommands\":%" PRIu32
",\"matchesStarted\":%" PRIu32 ",\"matchesCompleted\":%" PRIu32 ",\"matchesReset\":%" PRIu32
",\"maximumHandlerUs\":%" PRIu64 ",\"maximumSendUs\":%" PRIu64
",\"internalFreeBytes\":%u,\"internalLargestBlockBytes\":%u,"
"\"minimumInternalFreeBytes\":%u,\"minimumInternalLargestBlockBytes\":%u,"
"\"limits\":{\"connections\":%u,\"messageBytes\":%u,\"outgoingBytes\":%u,"
"\"queueEntries\":%u,\"gameCommands\":%u,\"lwipSockets\":%u,"
"\"playerLatencyBudgetMs\":%u,\"psramRequired\":false}}",
active_connection_count(gateway), gateway->metrics.connection_high_water,
(unsigned int)gateway->outgoing_count, gateway->metrics.queue_high_water,
gateway->metrics.messages_received, gateway->metrics.messages_sent,
gateway->metrics.messages_dropped, gateway->metrics.send_failures,
gateway->metrics.malformed_messages, gateway->metrics.oversized_messages,
gateway->metrics.rate_limited_messages, gateway->metrics.accepted_game_commands,
gateway->metrics.matches_started, gateway->metrics.matches_completed,
gateway->metrics.matches_reset, gateway->metrics.maximum_handler_us,
gateway->metrics.maximum_send_us, (unsigned int)internal_free_bytes(),
(unsigned int)internal_largest_block_bytes(),
(unsigned int)gateway->metrics.minimum_internal_free_bytes,
(unsigned int)gateway->metrics.minimum_internal_largest_block_bytes,
(unsigned int)MAZE32_GATEWAY_CONNECTION_LIMIT, (unsigned int)MAZE32_PROTOCOL_MESSAGE_LIMIT,
(unsigned int)MAZE32_GATEWAY_OUTGOING_LIMIT, (unsigned int)MAZE32_GATEWAY_QUEUE_LIMIT,
(unsigned int)MAZE32_GAME_COMMAND_CAPACITY, (unsigned int)CONFIG_LWIP_MAX_SOCKETS,
(unsigned int)MAZE32_GATEWAY_PLAYER_LATENCY_BUDGET_MS);
return written < 0 || (size_t)written >= output_size ? 0U : (size_t)written;
}
+24 -1
View File
@@ -82,6 +82,20 @@ static esp_err_t send_health(httpd_req_t* request) {
return httpd_resp_send(request, response, length);
}
static esp_err_t send_diagnostics(httpd_req_t* request) {
maze32_gateway_t* gateway = request->user_ctx;
char response[1024];
size_t length = maze32_gateway_format_diagnostics(gateway, response, sizeof(response));
if (length == 0U) {
httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR,
"Диагностика временно недоступна");
return ESP_FAIL;
}
httpd_resp_set_type(request, "application/json");
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
return httpd_resp_send(request, response, (ssize_t)length);
}
esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
maze32_gateway_t* gateway) {
if (server == NULL || health == NULL || gateway == NULL) {
@@ -89,7 +103,7 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
}
httpd_config_t configuration = HTTPD_DEFAULT_CONFIG();
configuration.max_uri_handlers = 12;
configuration.max_uri_handlers = 13;
configuration.max_open_sockets = 9;
configuration.lru_purge_enable = false;
configuration.recv_wait_timeout = 5;
@@ -139,6 +153,12 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
.handler = send_health,
.user_ctx = health,
};
const httpd_uri_t diagnostics_uri = {
.uri = "/diagnostics",
.method = HTTP_GET,
.handler = send_diagnostics,
.user_ctx = gateway,
};
result = httpd_register_uri_handler(*server, &root);
if (result == ESP_OK) {
@@ -156,6 +176,9 @@ esp_err_t maze32_web_start(httpd_handle_t* server, maze32_health_t* health,
if (result == ESP_OK) {
result = httpd_register_uri_handler(*server, &health_uri);
}
if (result == ESP_OK) {
result = httpd_register_uri_handler(*server, &diagnostics_uri);
}
if (result != ESP_OK) {
httpd_stop(*server);
*server = NULL;