Fix command registration and add command infra

This commit is contained in:
2026-08-29 18:46:38 +02:00
parent ee3aa1e488
commit 57ff0d0378
12 changed files with 463 additions and 89 deletions
+14 -1
View File
@@ -487,7 +487,20 @@ file(COPY ${_libs} DESTINATION "@CONCORD_PREFIX@/lib")
# the environment rather than the make command line because Concord's # the environment rather than the make command line because Concord's
# Makefile appends its own -I flags to it; a command-line assignment would # Makefile appends its own -I flags to it; a command-line assignment would
# override those and break the build. # override those and break the build.
set(CONCORD_CFLAGS "-O2 -g $<JOIN:${HAMMY_INSTRUMENT_COMPILE}, >") # ... with one carve-out. chash.h's string hash is djb2-style:
# (hash) = (((hash) << 1) + (hash)) + key[i];
# on a signed accumulator, so it deliberately relies on wraparound. That is
# two counts of UB per character -- the shift of a negative value and the
# overflowing add -- plus a third in the __chash_abs() that follows. With
# -fno-sanitize-recover it aborts the first time a ratelimit key hashes past
# LONG_MAX, which a guild command endpoint manages immediately.
#
# The wraparound is benign and the code is not ours to fix, so drop these
# two checks for Concord's build only. Everything else UBSan looks at stays
# on, and our own translation units keep the full set -- these flags are not
# in HAMMY_SANITIZER_FLAGS, only here.
set(CONCORD_UB_CARVEOUTS "-fno-sanitize=signed-integer-overflow -fno-sanitize=shift")
set(CONCORD_CFLAGS "-O2 -g $<JOIN:${HAMMY_INSTRUMENT_COMPILE}, > $<${HAMMY_IS_SANITIZED}:${CONCORD_UB_CARVEOUTS}>")
# gencodecs/Makefile hardcodes CC/HOSTCC/CPP to "cc" for its host-side code # gencodecs/Makefile hardcodes CC/HOSTCC/CPP to "cc" for its host-side code
# generator, with plain '=' assignments that the environment cannot override. # generator, with plain '=' assignments that the environment cannot override.
+23 -5
View File
@@ -10,8 +10,10 @@
struct hammy_bot_t { struct hammy_bot_t {
struct discord* client; // Owning reference to the client - handoff from main.c struct discord* client; // Owning reference to the client - handoff from main.c
bool commands_registered; // A flag if commands have been registered. Avoid re-registering every reconnect. bool commandsRegistered; // A flag if commands have been registered. Avoid re-registering every reconnect.
vector_t* commands; // vector_t of commands. Owning. vector_t* commands; // vector_t of commands. Owning the vector, NOT the elements.
hammy_pool_t* pool; // Owning reference to the thread pool.
u64snowflake appId; // Learned from the ready event.
}; };
// Alloc on heap and create // Alloc on heap and create
@@ -20,13 +22,29 @@ hammy_bot_t* hammy_bot_create();
// Sets the on_ready() call function pointer // Sets the on_ready() call function pointer
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)); bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event));
// Copies every entry from hammy_command_builtins() into the vector.
bool hammy_bot_load_builtins(hammy_bot_t* bot);
// Starts the worker pool. Pass 0 for the defaults. Call this from the ready
// callback, NOT before hammy_bot_run(): the workers are built on
// discord_clone(), which only succeeds inside a gateway dispatch.
// Returns true if the pool is already running.
bool hammy_bot_start_pool(hammy_bot_t* bot, size_t nWorkers, size_t queueCap);
// Runs the bot. Return true on succeed, false on failure. // Runs the bot. Return true on succeed, false on failure.
bool hammy_bot_run(hammy_bot_t* bot); bool hammy_bot_run(hammy_bot_t* bot);
// Adds a command to the vector as a COPY, get rid of the old struct. // Adds a command to the vector as a COPY. Commands own nothing, so the source
bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command); // struct needs no cleanup and may be a compound literal.
bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command);
// Registers all commands. Return true if succeeded or already registered; false if failure. // Looks a command up by name. Returns a pointer into the vector's storage, or
// NULL. Valid until the vector is next modified, which after startup is never.
const hammy_command_t* hammy_bot_find_command(const hammy_bot_t* bot, const char* name);
// Registers all commands with Discord. Return true if succeeded or already
// registered; false if failure. Requires bot->application_id to be set, so call
// this from on_ready, not before.
bool hammy_bot_register_commands(hammy_bot_t* bot); bool hammy_bot_register_commands(hammy_bot_t* bot);
// Destroy bot, NULLs the passing reference // Destroy bot, NULLs the passing reference
+7 -1
View File
@@ -30,12 +30,14 @@ struct hammy_job_t {
hammy_arg_t* args; // Array of size nArgs. Owning, array and contents. hammy_arg_t* args; // Array of size nArgs. Owning, array and contents.
size_t nArgs; size_t nArgs;
int64_t queuedAt; // Staleness checks, in ms. From discord_timestamp(). int64_t queuedAt; // Staleness checks, in ms. From discord_timestamp().
hammy_bot_t* bot; // NOT owning, used to access the command table
}; };
// Deep-copies everything the job needs out of the interaction event, so the // Deep-copies everything the job needs out of the interaction event, so the
// event may be freed by Concord the moment the handler returns. // event may be freed by Concord the moment the handler returns.
// Returns NULL on allocation failure. // Returns NULL on allocation failure.
hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event); hammy_job_t* hammy_job_create(hammy_bot_t* bot, const struct discord_interaction* event);
// Frees the job and everything it owns. NULLs the passing reference. // Frees the job and everything it owns. NULLs the passing reference.
// Safe to call with NULL or with a pointer to NULL. // Safe to call with NULL or with a pointer to NULL.
@@ -56,4 +58,8 @@ void hammy_job_run(hammy_job_t* job, struct discord* client);
// Used by hammy_job_run() and by the pool's error paths. // Used by hammy_job_run() and by the pool's error paths.
void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char* content); void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char* content);
// Sends a fresh interaction response with a plain text body.
// Only valid on the instant path, where nothing has been sent yet.
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* content);
#endif #endif
+5 -1
View File
@@ -43,6 +43,9 @@ struct hammy_pool_t {
// Creates the pool and starts n_workers threads, each with its own // Creates the pool and starts n_workers threads, each with its own
// discord_clone() of client. Pass 0 for either size to take the defaults. // discord_clone() of client. Pass 0 for either size to take the defaults.
// Returns NULL on failure; no threads are left running in that case. // Returns NULL on failure; no threads are left running in that case.
// MUST be called from inside a gateway dispatch callback (on_ready is the
// earliest one): discord_clone() copies the gateway's current payload and
// fails with CCORD_ERRNO when there isn't one.
hammy_pool_t* hammy_pool_create(struct discord* client, size_t nWorkers, size_t queueCap); hammy_pool_t* hammy_pool_create(struct discord* client, size_t nWorkers, size_t queueCap);
// Enqueues a job. See hammy_push_result_t for who owns the job afterwards. // Enqueues a job. See hammy_push_result_t for who owns the job afterwards.
@@ -56,7 +59,8 @@ hammy_push_result_t hammy_pool_push(hammy_pool_t* pool, hammy_job_t* job);
void hammy_pool_shutdown(hammy_pool_t* pool); void hammy_pool_shutdown(hammy_pool_t* pool);
// As above but discards anything still queued (each dropped job gets an // As above but discards anything still queued (each dropped job gets an
// apology reply if it can be sent quickly). // apology reply). Call from the gateway thread only - the apologies are
// serialised through the original client, which that thread owns.
void hammy_pool_shutdown_now(hammy_pool_t* pool); void hammy_pool_shutdown_now(hammy_pool_t* pool);
// Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet. // Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet.
+2
View File
@@ -15,6 +15,8 @@ struct hammy_worker_t {
// Clones client, spawns the thread. Returns false on clone or spawn failure, // Clones client, spawns the thread. Returns false on clone or spawn failure,
// leaving the worker safe to pass to hammy_worker_join(). // leaving the worker safe to pass to hammy_worker_join().
// Inherits hammy_pool_create()'s precondition: the clone only succeeds from
// inside a gateway dispatch callback.
bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct discord* client, int id); bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct discord* client, int id);
// Joins the thread if it was started and cleans up the clone. // Joins the thread if it was started and cleans up the clone.
+30
View File
@@ -0,0 +1,30 @@
#include <concord/discord.h>
#include <inttypes.h>
#include <stdio.h>
#include <hammy/command.h>
#include <hammy/job.h>
// Discord epoch, for turning a snowflake back into a wall-clock time.
#define HAMMY_DISCORD_EPOCH_MS 1420070400000LL
static int64_t hammy_snowflake_to_ms(u64snowflake id) {
return (int64_t)(id >> 22) + HAMMY_DISCORD_EPOCH_MS;
}
// Instant command: runs on the gateway thread, sends a fresh response.
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) {
// Gateway heartbeat round trip, as measured by Concord.
int gatewayMs = discord_get_ping(client);
// Time from Discord minting the interaction to us handling it. This is the
// number that degrades under load, so it is the one worth showing.
int64_t handledMs = (int64_t)discord_timestamp(client) - hammy_snowflake_to_ms(job->id);
char body[256];
snprintf(body, sizeof(body),
"Pong!\nGateway: `%d ms`\nHandled in: `%" PRId64 " ms`",
gatewayMs, handledMs);
hammy_job_respond(job, client, body);
}
+227 -34
View File
@@ -1,14 +1,103 @@
#include <concord/discord.h>
#include <concord/log.h>
#include <dlibc/vector.h>
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <hammy/bot.h> #include <hammy/bot.h>
#include <hammy/command.h>
#include <hammy/job.h>
#include <hammy/pool.h>
static void hammy_bot_attach(struct discord* client, hammy_bot_t* bot) {
discord_set_data(client, bot);
}
static hammy_bot_t* hammy_bot_from_client(struct discord* client) {
return (hammy_bot_t*)discord_get_data(client);
}
static void hammy_bot_on_interaction(struct discord* client, const struct discord_interaction* event) {
if (event->type != DISCORD_INTERACTION_APPLICATION_COMMAND) {
return;
}
hammy_bot_t* bot = hammy_bot_from_client(client);
if (!bot) {
log_warn("[bot] No bot data attached to client. Cannot handle interaction.");
return;
}
const char* name = (event->data && event->data->name) ? event->data->name : NULL;
const hammy_command_t* command = hammy_bot_find_command(bot, name);
if (!command) {
log_warn("[bot] No command found for interaction '%s'.", name ? name : "(null)");
struct discord_interaction_response params = {
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
.data = &(struct discord_interaction_callback_data){
.content = "I don't know that command!"
}
};
CCORDcode code = discord_create_interaction_response(client, event->id, event->token, &params, NULL);
if (code != CCORD_OK) {
log_warn("[job] Failed to send interaction error response for interaction %" PRIu64 ": %d", event->id, code);
}
return;
}
hammy_job_t* job = hammy_job_create(bot, event);
if (!job) {
log_error("[bot] Failed to create job for interaction '%s'.", name ? name : "(null)");
return;
}
// Instant path: no defer or queue, answered on the spot
if (command->instant) {
command->handler(job, client);
hammy_job_destroy(&job);
return;
}
// Deferred path: ACK first, 3 second window by discord, then queue to worker pool
struct discord_interaction_response deferred = {
.type = DISCORD_INTERACTION_DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE,
};
discord_create_interaction_response(client, event->id, event->token, &deferred, NULL);
switch (hammy_pool_push(bot->pool, job)) {
case HAMMY_PUSH_OK:
log_info("[bot] Queued job for interaction '%s'.", name ? name : "(null)");
break;
case HAMMY_PUSH_FULL:
log_warn("[bot] Queue pool full. Cannot queue job for interaction '%s'.", name ? name : "(null)");
hammy_job_reply(job, client, "I am a bit busy right now! Please try again later.");
hammy_job_destroy(&job);
break;
case HAMMY_PUSH_SHUTDOWN:
// Also covers the window before on_ready starts the pool, since
// hammy_pool_push() reports a NULL pool the same way.
log_error("[bot] Worker pool unavailable. Cannot queue job for interaction '%s'.", name ? name : "(null)");
hammy_job_reply(job, client, "Hammy is not accepting commands right now. Please try again shortly.");
hammy_job_destroy(&job);
break;
}
}
hammy_bot_t* hammy_bot_create() { hammy_bot_t* hammy_bot_create() {
hammy_bot_t* bot = (hammy_bot_t*)malloc(sizeof(hammy_bot_t)); hammy_bot_t* bot = (hammy_bot_t*)calloc(1, sizeof(*bot));
if (!bot) { if (!bot) {
return NULL; return NULL;
} }
// Set defaults // Set defaults
bot->client = NULL; bot->client = NULL;
bot->commands_registered = false; bot->commandsRegistered = false;
bot->commands = vector_create(sizeof(hammy_command_t)); bot->commands = vector_create(sizeof(hammy_command_t));
// Check if vector allocation errored // Check if vector allocation errored
@@ -17,37 +106,60 @@ hammy_bot_t* hammy_bot_create() {
return NULL; return NULL;
} }
// Init the discord client - even though this is an owning reference, the main() and others may call functions on it (if we don't stay singlethread anymore then FIX THIS)
bot->client = discord_from_json("config.json");
if (!bot->client) {
vector_destroy(&bot->commands);
free(bot);
return NULL;
}
return bot; return bot;
} }
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)) { bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command) {
if (!bot || !func || !bot->client) { return false; } if (!bot || !bot->commands || !command || !command->name) { return false; }
discord_set_on_ready(bot->client, func); // Copy the command into the vector. The command owns nothing, so this is safe.
return true;
}
bool hammy_bot_run(hammy_bot_t* bot) {
if (!bot || !bot->client) { return false; }
discord_run(bot->client);
return true;
}
bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command) {
if (!bot || !command) { return false; }
// Push into commands vector
if (vector_push_back(bot->commands, command) < 0) { if (vector_push_back(bot->commands, command) < 0) {
log_error("[bot] Failed to add command '%s' to bot.", command->name);
return false;
}
return true;
}
bool hammy_bot_load_builtins(hammy_bot_t* bot) {
if (!bot) { return false; }
size_t count = 0;
const hammy_command_t* builtins = hammy_command_builtins(&count);
for (size_t i = 0; i < count; i++) {
if (!hammy_bot_add_command(bot, &builtins[i])) {
log_error("[bot] Failed to load builtin command '%s'.", builtins[i].name);
return false;
}
}
log_info("[bot] Loaded %zu builtin commands.", count);
return true;
}
const hammy_command_t* hammy_bot_find_command(const hammy_bot_t* bot, const char* name) {
if (!bot || !bot->commands || !name) { return NULL; }
// Vector guarantees contiguous and gapless storage, so a plain-array search is fine.
const hammy_command_t* commands = (const hammy_command_t*)vector_as_c_array(bot->commands);
return hammy_command_find(commands, vector_size(bot->commands), name);
}
bool hammy_bot_start_pool(hammy_bot_t* bot, size_t nWorkers, size_t queueCap) {
if (!bot || !bot->client) { return false; }
if (bot->pool) {
// A reconnect re-fires READY, so this is routine rather than a problem.
log_debug("[bot] Worker pool already started. No-op.");
return true; // Already started
}
bot->pool = hammy_pool_create(bot->client, nWorkers, queueCap);
if (!bot->pool) {
log_error("[bot] Failed to create worker pool.");
return false; return false;
} }
@@ -55,20 +167,101 @@ bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command) {
} }
bool hammy_bot_register_commands(hammy_bot_t* bot) { bool hammy_bot_register_commands(hammy_bot_t* bot) {
if (!bot) { return false; } if (!bot || !bot->client) { return false; }
if (bot->commandsRegistered) { return true; } // Already registered
// TODO: Figure out registration logic if (!bot->appId) {
log_error("[bot] No application ID set. Cannot register commands. This should be set in the on_ready() callback.");
return false;
}
// TODO: Temporary - dev guild ID for testing.
const char* devGuild = getenv("HAMMY_DEV_GUILD");
u64snowflake guildId = devGuild ? (u64snowflake)strtoull(devGuild, NULL, 10) : 0;
size_t count = vector_size(bot->commands);
for (size_t i = 0; i < count; i++) {
const hammy_command_t* cmd = (const hammy_command_t*)vector_get_const(bot->commands, i);
if (!cmd || !cmd->name || !cmd->description) {
log_warn("[bot] Invalid command at index %zu. Cannot register with Discord. Skipping!", i);
continue; // Skip this command, but try to register the rest
}
CCORDcode code;
if (guildId) {
struct discord_create_guild_application_command params = {
.name = (char*)cmd->name,
.description = (char*)cmd->description,
.options = cmd->options
};
code = discord_create_guild_application_command(bot->client, bot->appId, guildId, &params, NULL);
} else {
struct discord_create_global_application_command params = {
.name = (char*)cmd->name,
.description = (char*)cmd->description,
.options = cmd->options
};
code = discord_create_global_application_command(bot->client, bot->appId, &params, NULL);
}
// Passing NULL for the return handle makes these fire-and-forget, so
// a successful enqueue reports CCORD_PENDING rather than CCORD_OK.
if (code != CCORD_OK && code != CCORD_PENDING) {
log_error("[bot] Failed to register command '%s' with Discord. Error code: %d", cmd->name, code);
return false;
}
}
bot->commandsRegistered = true;
log_info("[bot] Successfully registered %zu commands with Discord %s.", count, guildId ? "to dev guild" : "globally");
return true;
}
bool hammy_bot_set_on_ready(hammy_bot_t* bot, void (*func)(struct discord* client, const struct discord_ready* event)) {
if (!bot || !func || !bot->client) { return false; }
discord_set_on_ready(bot->client, func);
return true;
}
bool hammy_bot_run(hammy_bot_t* bot) {
if (!bot || !bot->client) { return false; }
hammy_bot_attach(bot->client, bot);
discord_set_on_interaction_create(bot->client, &hammy_bot_on_interaction);
return discord_run(bot->client) == CCORD_OK;
} }
bool hammy_bot_destroy(hammy_bot_t** bot) { bool hammy_bot_destroy(hammy_bot_t** bot) {
if (!bot || !(*bot)) { return false; } if (!bot || !(*bot)) { return false; }
if ((*bot)->client) { hammy_bot_t* b = *bot;
discord_cleanup((*bot)->client);
// Workers hold clones of the client
if (b->pool) {
hammy_pool_destroy(&b->pool);
}
// No element destructor, commands own nothing.
if (b->commands) {
vector_destroy(&b->commands);
}
if (b->client) {
discord_cleanup(b->client);
b->client = NULL;
} }
// TODO: Potential other stuff here
vector_destroy(&(*bot)->commands); // commands is NULL form hereon, calls destructors on commands automatically
free(*bot); free(*bot);
*bot = NULL; *bot = NULL;
return true;
} }
+1 -5
View File
@@ -6,11 +6,7 @@
// Handlers live in src/hammy/commands/. Declared here rather than in a header // Handlers live in src/hammy/commands/. Declared here rather than in a header
// so adding a command touches exactly two places: its own .c file and this // so adding a command touches exactly two places: its own .c file and this
// table. // table.
void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) { void hammy_cmd_ping(const hammy_job_t* job, struct discord* client);
(void)job;
(void)client;
log_info("[command] ping");
}
// The command table. Pure data - no allocation, no lifetime, no destructor. // The command table. Pure data - no allocation, no lifetime, no destructor.
// Copying an entry into the bot's vector is a plain struct copy, and the vector // Copying an entry into the bot's vector is a plain struct copy, and the vector
+33 -9
View File
@@ -4,6 +4,8 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <hammy/bot.h>
#include <hammy/command.h>
#include <hammy/job.h> #include <hammy/job.h>
// strdup() is POSIX, so we'll keep a local and keep the code portable. // strdup() is POSIX, so we'll keep a local and keep the code portable.
@@ -28,17 +30,19 @@ static u64snowflake hammy_job_extract_user(const struct discord_interaction* eve
return 0; return 0;
} }
hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event) { hammy_job_t* hammy_job_create(hammy_bot_t* bot, const struct discord_interaction* event) {
if (!client || !event) { return NULL; } if (!bot || !bot->client || !event) { return NULL; }
hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job)); hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job));
if (!job) { return NULL; } if (!job) { return NULL; }
job->bot = bot;
job->id = event->id; job->id = event->id;
job->appId = event->application_id;
job->user = hammy_job_extract_user(event); job->user = hammy_job_extract_user(event);
job->token = hammy_strdup(event->token); job->token = hammy_strdup(event->token);
job->queuedAt = (int64_t)discord_timestamp(client); job->queuedAt = (int64_t)discord_timestamp(bot->client);
job->appId = event->application_id ? event->application_id : bot->appId;
if (!job->token) { if (!job->token) {
goto fail; goto fail;
@@ -135,13 +139,33 @@ void hammy_job_reply(const hammy_job_t* job, struct discord* client, const char*
} }
} }
void hammy_job_respond(const hammy_job_t* job, struct discord* client, const char* content) {
if (!job || !client || !content) { return; }
struct discord_interaction_response params = {
.type = DISCORD_INTERACTION_CHANNEL_MESSAGE_WITH_SOURCE,
.data = &(struct discord_interaction_callback_data){
.content = (char*)content
}
};
CCORDcode code = discord_create_interaction_response(client, job->id, job->token, &params, NULL);
if (code != CCORD_OK) {
log_warn("[job] Failed to send interaction response for interaction %" PRIu64 ": %d", job->id, code);
}
}
void hammy_job_run(hammy_job_t* job, struct discord* client) { void hammy_job_run(hammy_job_t* job, struct discord* client) {
if (!job || !client) { return; } if (!job || !client) { return; }
// TODO: look job->command up in the bot's command vector and call its
// handler with (job, client). Placeholder until command.h grows a
// dispatch entry point.
log_info("[job] Running command '%s' for interaction %" PRIu64, job->command ? job->command : "unknown", job->id); log_info("[job] Running command '%s' for interaction %" PRIu64, job->command ? job->command : "unknown", job->id);
const hammy_command_t* command = hammy_bot_find_command(job->bot, job->command);
hammy_job_reply(job, client, "This is a placeholder reply. The command handler is not yet implemented."); if (!command || !command->handler) {
log_warn("[job] No handler found for command '%s'.", job->command ? job->command : "unknown");
hammy_job_reply(job, client, "I don't know that command!");
return;
}
command->handler(job, client);
} }
+26 -2
View File
@@ -2,6 +2,7 @@
#include <concord/log.h> #include <concord/log.h>
#include <stdlib.h> #include <stdlib.h>
#include <hammy/bot.h>
#include <hammy/job.h> #include <hammy/job.h>
#include <hammy/pool.h> #include <hammy/pool.h>
#include <hammy/worker.h> #include <hammy/worker.h>
@@ -104,13 +105,22 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
pool->shutdown = true; pool->shutdown = true;
if (!drain) { // Detach the queue under the lock, but apologise outside it - a REST
// enqueue has no business happening while the workers are blocked on it.
hammy_job_t** dropped = NULL;
size_t nDropped = 0;
if (!drain && pool->count > 0) {
dropped = (hammy_job_t**)calloc(pool->count, sizeof(*dropped));
while (pool->count > 0) { while (pool->count > 0) {
hammy_job_t* job = pool->jobs[pool->head]; hammy_job_t* job = pool->jobs[pool->head];
pool->head = (pool->head + 1) % pool->cap; pool->head = (pool->head + 1) % pool->cap;
pool->count--; pool->count--;
hammy_job_destroy(&job); // Out of memory on the way out is not worth a leak, so drop it quietly.
if (dropped) { dropped[nDropped++] = job; }
else { hammy_job_destroy(&job); }
} }
} }
@@ -118,6 +128,20 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) {
pthread_cond_broadcast(&pool->notEmpty); pthread_cond_broadcast(&pool->notEmpty);
pthread_mutex_unlock(&pool->lock); pthread_mutex_unlock(&pool->lock);
for (size_t i = 0; i < nDropped; i++) {
hammy_job_t* job = dropped[i];
// The job's bot back-reference carries the original client, which is
// the one this thread is allowed to serialise through.
if (job->bot && job->bot->client) {
hammy_job_reply(job, job->bot->client, "Hammy is shutting down, so this command was dropped. Please try again once it is back.");
}
hammy_job_destroy(&job);
}
free(dropped);
for (size_t i = 0; i < pool->nWorkers; i++) { for (size_t i = 0; i < pool->nWorkers; i++) {
hammy_worker_join(&pool->workers[i]); hammy_worker_join(&pool->workers[i]);
} }
+22 -4
View File
@@ -1,4 +1,5 @@
#include <concord/discord.h> #include <concord/discord.h>
#include <concord/discord-internal.h>
#include <concord/log.h> #include <concord/log.h>
#include <stdlib.h> #include <stdlib.h>
@@ -6,6 +7,20 @@
#include <hammy/pool.h> #include <hammy/pool.h>
#include <hammy/worker.h> #include <hammy/worker.h>
// Workaround for a concord 3.0.1 bug. _discord_clone_gateway_cleanup()
// (discord-client.c:833) frees payload.json.table and then frees payload.data
// as well, but payload.data is a jsmnf_pair pointing INTO that table, not a
// separate allocation. Every discord_cleanup() of a clone is therefore an
// invalid free. Clearing the field first skips that free; the table it points
// into is still released, so nothing leaks.
static void hammy_worker_cleanup_clone(struct discord* clone) {
if (!clone) { return; }
clone->gw.payload.data = NULL;
discord_cleanup(clone);
}
static void* hammy_worker_main(void* arg) { static void* hammy_worker_main(void* arg) {
hammy_worker_t* worker = (hammy_worker_t*)arg; hammy_worker_t* worker = (hammy_worker_t*)arg;
hammy_pool_t* pool = worker->pool; hammy_pool_t* pool = worker->pool;
@@ -63,8 +78,11 @@ bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct disco
worker->clientCopy = NULL; worker->clientCopy = NULL;
worker->started = false; worker->started = false;
// According to the concord spec, each thread must have its own discord client, so we clone it here. // Each thread needs its own client. The queues, pollers and timers are all
// However, concord's buffers, URLs, headers, etc. are NOT shared-safe. They're per-client. // shared through pointers, but client->registry - the reflect-c registry
// that serialises request bodies on the calling thread - is not.
// discord_clone() copies the gateway's *current* payload, so this only
// works while a dispatch is in flight. See hammy_pool_create().
worker->clientCopy = discord_clone(client); worker->clientCopy = discord_clone(client);
if (!worker->clientCopy) { if (!worker->clientCopy) {
log_error("[worker %d] Failed to clone client", id); log_error("[worker %d] Failed to clone client", id);
@@ -73,7 +91,7 @@ bool hammy_worker_start(hammy_worker_t* worker, hammy_pool_t* pool, struct disco
if (pthread_create(&worker->thread, NULL, &hammy_worker_main, worker) != 0) { if (pthread_create(&worker->thread, NULL, &hammy_worker_main, worker) != 0) {
log_error("[worker %d] Failed to create thread", id); log_error("[worker %d] Failed to create thread", id);
discord_cleanup(worker->clientCopy); hammy_worker_cleanup_clone(worker->clientCopy);
worker->clientCopy = NULL; worker->clientCopy = NULL;
return false; return false;
@@ -93,7 +111,7 @@ void hammy_worker_join(hammy_worker_t* worker) {
} }
if (worker->clientCopy) { if (worker->clientCopy) {
discord_cleanup(worker->clientCopy); hammy_worker_cleanup_clone(worker->clientCopy);
worker->clientCopy = NULL; worker->clientCopy = NULL;
} }
} }
+67 -21
View File
@@ -1,44 +1,90 @@
#include <string.h> #include <concord/discord.h>
#include <signal.h>
#include <concord/log.h> #include <concord/log.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <hammy/bot.h> #include <hammy/bot.h>
#include <hammy/pool.h>
static void on_signal(int sig) { #define HAMMY_CONFIG_PATH "config.json"
static hammy_bot_t* g_bot = NULL;
static void hammy_on_sigint(int sig) {
(void)sig; (void)sig;
discord_shutdown_all();
// Only sets a flag inside Concord and makes discord_run() return. The real
// teardown happens back in main().
if (g_bot && g_bot->client) discord_shutdown(g_bot->client);
} }
void on_ready(struct discord* client, const struct discord_ready* event) { static void hammy_on_ready(struct discord* client, const struct discord_ready* event) {
(void)client; hammy_bot_t* bot = (hammy_bot_t*)discord_get_data(client);
log_info("[master] Logged in as %s", event->user->username); if (!bot) return;
log_info("[hammy] logged in as %s", event->user->username);
// The application id is only available now, and both registration and
// response editing need it.
bot->appId = event->application->id;
// discord_clone() deep-copies the gateway's *current* payload, so it only
// succeeds from inside a dispatch callback. READY is the first one we get.
if (!hammy_bot_start_pool(bot, 0, 0)) { // 0, 0 = defaults
log_fatal("[hammy] Could not start the worker pool. Shutting down.");
discord_shutdown(client);
return;
}
hammy_bot_register_commands(bot);
} }
int main(void) { int main(void) {
struct sigaction sa = { 0 };
sa.sa_handler = &on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* deliberately no SA_RESTART */
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
ccord_global_init(); ccord_global_init();
hammy_bot_t* bot = hammy_bot_create(); hammy_bot_t* bot = hammy_bot_create();
if (!bot) { if (!bot) {
log_error("[master] Hammy Bot creation returned NULL! Bailing!"); fprintf(stderr, "could not create bot\n");
ccord_global_cleanup(); ccord_global_cleanup();
return 1; return EXIT_FAILURE;
} }
hammy_bot_set_on_ready(bot, &on_ready); // Token and logging settings both come out of config.json.
hammy_bot_run(bot); // This will block and exit afterwards bot->client = discord_config_init(HAMMY_CONFIG_PATH);
if (!bot->client) {
fprintf(stderr, "could not init discord client from %s\n", HAMMY_CONFIG_PATH);
hammy_bot_destroy(&bot);
ccord_global_cleanup();
return EXIT_FAILURE;
}
bool ok = false; // Declared before the gotos so they don't jump over it.
g_bot = bot;
signal(SIGINT, &hammy_on_sigint);
signal(SIGTERM, &hammy_on_sigint);
// Order matters: commands loaded before the ready callback can register
// them. The pool starts from on_ready, not here - see the comment there.
if (!hammy_bot_load_builtins(bot)) goto fail;
if (!hammy_bot_set_on_ready(bot, &hammy_on_ready)) goto fail;
// Blocks until shutdown. A NULL pool afterwards means on_ready bailed out
// before it could start one.
ok = hammy_bot_run(bot) && bot->pool != NULL;
hammy_bot_destroy(&bot); hammy_bot_destroy(&bot);
ccord_global_cleanup(); ccord_global_cleanup();
return 0; return ok ? EXIT_SUCCESS : EXIT_FAILURE;
fail:
hammy_bot_destroy(&bot);
ccord_global_cleanup();
return EXIT_FAILURE;
} }