diff --git a/CMakeLists.txt b/CMakeLists.txt index faa8a27..d450750 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -487,7 +487,20 @@ file(COPY ${_libs} DESTINATION "@CONCORD_PREFIX@/lib") # the environment rather than the make command line because Concord's # Makefile appends its own -I flags to it; a command-line assignment would # override those and break the build. - set(CONCORD_CFLAGS "-O2 -g $") + # ... 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 $ $<${HAMMY_IS_SANITIZED}:${CONCORD_UB_CARVEOUTS}>") # gencodecs/Makefile hardcodes CC/HOSTCC/CPP to "cc" for its host-side code # generator, with plain '=' assignments that the environment cannot override. diff --git a/include/hammy/bot.h b/include/hammy/bot.h index 142ae91..3755a89 100644 --- a/include/hammy/bot.h +++ b/include/hammy/bot.h @@ -10,25 +10,43 @@ struct hammy_bot_t { 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. - vector_t* commands; // vector_t of commands. Owning. + bool commandsRegistered; // A flag if commands have been registered. Avoid re-registering every reconnect. + 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 hammy_bot_t* hammy_bot_create(); - + // 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)); - + +// 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. bool hammy_bot_run(hammy_bot_t* bot); - -// Adds a command to the vector as a COPY, get rid of the old struct. -bool hammy_bot_add_command(hammy_bot_t* bot, hammy_command_t* command); - -// Registers all commands. Return true if succeeded or already registered; false if failure. + +// Adds a command to the vector as a COPY. Commands own nothing, so the source +// struct needs no cleanup and may be a compound literal. +bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command); + +// 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); - + // Destroy bot, NULLs the passing reference bool hammy_bot_destroy(hammy_bot_t** bot); diff --git a/include/hammy/job.h b/include/hammy/job.h index 865e54b..ef262ae 100644 --- a/include/hammy/job.h +++ b/include/hammy/job.h @@ -30,12 +30,14 @@ struct hammy_job_t { hammy_arg_t* args; // Array of size nArgs. Owning, array and contents. size_t nArgs; 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 // event may be freed by Concord the moment the handler returns. // 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. // 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. 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 diff --git a/include/hammy/pool.h b/include/hammy/pool.h index 3abf162..e570151 100644 --- a/include/hammy/pool.h +++ b/include/hammy/pool.h @@ -43,6 +43,9 @@ struct hammy_pool_t { // 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. // 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); // 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); // 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); // Frees the pool. Runs hammy_pool_shutdown() first if it has not happened yet. diff --git a/include/hammy/worker.h b/include/hammy/worker.h index e7fff9b..5a4f6e4 100644 --- a/include/hammy/worker.h +++ b/include/hammy/worker.h @@ -15,6 +15,8 @@ struct hammy_worker_t { // Clones client, spawns the thread. Returns false on clone or spawn failure, // 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); // Joins the thread if it was started and cleans up the clone. diff --git a/src/commands/ping.c b/src/commands/ping.c new file mode 100644 index 0000000..6bb3da2 --- /dev/null +++ b/src/commands/ping.c @@ -0,0 +1,30 @@ +#include +#include +#include + +#include +#include + +// 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); +} diff --git a/src/hammy/bot.c b/src/hammy/bot.c index c18bef2..82fbd80 100644 --- a/src/hammy/bot.c +++ b/src/hammy/bot.c @@ -1,14 +1,103 @@ +#include +#include +#include +#include +#include +#include + #include +#include +#include +#include + +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, ¶ms, 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* bot = (hammy_bot_t*)malloc(sizeof(hammy_bot_t)); + hammy_bot_t* bot = (hammy_bot_t*)calloc(1, sizeof(*bot)); if (!bot) { return NULL; } // Set defaults bot->client = NULL; - bot->commands_registered = false; + bot->commandsRegistered = false; bot->commands = vector_create(sizeof(hammy_command_t)); // Check if vector allocation errored @@ -17,37 +106,60 @@ hammy_bot_t* hammy_bot_create() { 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; } -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; } +bool hammy_bot_add_command(hammy_bot_t* bot, const hammy_command_t* command) { + if (!bot || !bot->commands || !command || !command->name) { 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; } - - 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 + // Copy the command into the vector. The command owns nothing, so this is safe. 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; } @@ -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) { - 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, ¶ms, 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, ¶ms, 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) { if (!bot || !(*bot)) { return false; } - if ((*bot)->client) { - discord_cleanup((*bot)->client); + hammy_bot_t* b = *bot; + + // 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); *bot = NULL; + + return true; } diff --git a/src/hammy/command.c b/src/hammy/command.c index a1bcb6c..7a78ad4 100644 --- a/src/hammy/command.c +++ b/src/hammy/command.c @@ -6,11 +6,7 @@ // 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 // table. -void hammy_cmd_ping(const hammy_job_t* job, struct discord* client) { - (void)job; - (void)client; - log_info("[command] ping"); -} +void hammy_cmd_ping(const hammy_job_t* job, struct discord* client); // 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 diff --git a/src/hammy/job.c b/src/hammy/job.c index 6c0171b..45d1254 100644 --- a/src/hammy/job.c +++ b/src/hammy/job.c @@ -4,6 +4,8 @@ #include #include +#include +#include #include // 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; } -hammy_job_t* hammy_job_create(struct discord* client, const struct discord_interaction* event) { - if (!client || !event) { return NULL; } +hammy_job_t* hammy_job_create(hammy_bot_t* bot, const struct discord_interaction* event) { + if (!bot || !bot->client || !event) { return NULL; } hammy_job_t* job = (hammy_job_t*)calloc(1, sizeof(*job)); if (!job) { return NULL; } + job->bot = bot; job->id = event->id; - job->appId = event->application_id; job->user = hammy_job_extract_user(event); 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) { 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, ¶ms, 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) { 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); + const hammy_command_t* command = hammy_bot_find_command(job->bot, job->command); + 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; + } - hammy_job_reply(job, client, "This is a placeholder reply. The command handler is not yet implemented."); + command->handler(job, client); } diff --git a/src/hammy/pool.c b/src/hammy/pool.c index 231d5e5..34b37fd 100644 --- a/src/hammy/pool.c +++ b/src/hammy/pool.c @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -104,13 +105,22 @@ static void hammy_pool_stop(hammy_pool_t* pool, bool drain) { 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) { hammy_job_t* job = pool->jobs[pool->head]; pool->head = (pool->head + 1) % pool->cap; 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_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++) { hammy_worker_join(&pool->workers[i]); } diff --git a/src/hammy/worker.c b/src/hammy/worker.c index b1a1bdf..fffd9ac 100644 --- a/src/hammy/worker.c +++ b/src/hammy/worker.c @@ -1,11 +1,26 @@ #include +#include #include #include - + #include #include #include +// 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) { hammy_worker_t* worker = (hammy_worker_t*)arg; 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->started = false; - // According to the concord spec, each thread must have its own discord client, so we clone it here. - // However, concord's buffers, URLs, headers, etc. are NOT shared-safe. They're per-client. + // Each thread needs its own client. The queues, pollers and timers are all + // 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); if (!worker->clientCopy) { 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) { log_error("[worker %d] Failed to create thread", id); - discord_cleanup(worker->clientCopy); + hammy_worker_cleanup_clone(worker->clientCopy); worker->clientCopy = NULL; return false; @@ -93,7 +111,7 @@ void hammy_worker_join(hammy_worker_t* worker) { } if (worker->clientCopy) { - discord_cleanup(worker->clientCopy); + hammy_worker_cleanup_clone(worker->clientCopy); worker->clientCopy = NULL; } } diff --git a/src/main.c b/src/main.c index 62bd59c..db554ad 100644 --- a/src/main.c +++ b/src/main.c @@ -1,44 +1,90 @@ -#include -#include +#include #include +#include +#include +#include #include +#include -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; - 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) { - (void)client; - log_info("[master] Logged in as %s", event->user->username); +static void hammy_on_ready(struct discord* client, const struct discord_ready* event) { + hammy_bot_t* bot = (hammy_bot_t*)discord_get_data(client); + 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) { - 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(); hammy_bot_t* bot = hammy_bot_create(); if (!bot) { - log_error("[master] Hammy Bot creation returned NULL! Bailing!"); + fprintf(stderr, "could not create bot\n"); ccord_global_cleanup(); - return 1; + return EXIT_FAILURE; } - hammy_bot_set_on_ready(bot, &on_ready); - hammy_bot_run(bot); // This will block and exit afterwards + // Token and logging settings both come out of config.json. + 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); - ccord_global_cleanup(); - - return 0; + + return ok ? EXIT_SUCCESS : EXIT_FAILURE; + +fail: + hammy_bot_destroy(&bot); + ccord_global_cleanup(); + + return EXIT_FAILURE; }