Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 44 additions & 14 deletions src/core/memory.c
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,40 @@ static void copy_str_bounded(char *dst, size_t dst_size, const char *src)
dst[n] = '\0';
}

static char *dup_sqlite_text(sqlite3_stmt *stmt, int col)
{
const char *src = (const char *)sqlite3_column_text(stmt, col);
return strdup(src ? src : "");
}

void cron_job_row_free(cron_job_row_t *row)
{
if (!row) return;
free(row->schedule);
free(row->message);
row->schedule = NULL;
row->message = NULL;
}

static int fill_cron_job_row(sqlite3_stmt *stmt, cron_job_row_t *out)
{
char *schedule = dup_sqlite_text(stmt, 1);
char *message = dup_sqlite_text(stmt, 2);
if (!schedule || !message) {
free(schedule);
free(message);
return -1;
}
copy_str_bounded(out->id, sizeof(out->id), (const char *)sqlite3_column_text(stmt, 0));
out->schedule = schedule;
out->message = message;
copy_str_bounded(out->channel, sizeof(out->channel), (const char *)sqlite3_column_text(stmt, 3));
copy_str_bounded(out->recipient, sizeof(out->recipient), (const char *)sqlite3_column_text(stmt, 4));
out->next_run = sqlite3_column_int64(stmt, 5);
out->enabled = sqlite3_column_int(stmt, 6);
return 0;
}

int cron_job_create(const char *id, const char *schedule, const char *message,
const char *channel, const char *recipient, long long next_run, int enabled)
{
Expand Down Expand Up @@ -400,13 +434,12 @@ int cron_job_list(cron_job_row_t *out, int max_count)
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
int count = 0;
while (count < max_count && sqlite3_step(stmt) == SQLITE_ROW) {
copy_str_bounded(out[count].id, sizeof(out[count].id), (const char *)sqlite3_column_text(stmt, 0));
copy_str_bounded(out[count].schedule, sizeof(out[count].schedule), (const char *)sqlite3_column_text(stmt, 1));
copy_str_bounded(out[count].message, sizeof(out[count].message), (const char *)sqlite3_column_text(stmt, 2));
copy_str_bounded(out[count].channel, sizeof(out[count].channel), (const char *)sqlite3_column_text(stmt, 3));
copy_str_bounded(out[count].recipient, sizeof(out[count].recipient), (const char *)sqlite3_column_text(stmt, 4));
out[count].next_run = sqlite3_column_int64(stmt, 5);
out[count].enabled = sqlite3_column_int(stmt, 6);
if (fill_cron_job_row(stmt, &out[count]) != 0) {
for (int i = 0; i < count; i++)
cron_job_row_free(&out[i]);
sqlite3_finalize(stmt);
return -1;
}
count++;
}
sqlite3_finalize(stmt);
Expand All @@ -423,13 +456,10 @@ int cron_job_get_next_due(long long now, cron_job_row_t *out)
sqlite3_bind_int64(stmt, 1, now);
int ret = 0;
if (sqlite3_step(stmt) == SQLITE_ROW) {
copy_str_bounded(out->id, sizeof(out->id), (const char *)sqlite3_column_text(stmt, 0));
copy_str_bounded(out->schedule, sizeof(out->schedule), (const char *)sqlite3_column_text(stmt, 1));
copy_str_bounded(out->message, sizeof(out->message), (const char *)sqlite3_column_text(stmt, 2));
copy_str_bounded(out->channel, sizeof(out->channel), (const char *)sqlite3_column_text(stmt, 3));
copy_str_bounded(out->recipient, sizeof(out->recipient), (const char *)sqlite3_column_text(stmt, 4));
out->next_run = sqlite3_column_int64(stmt, 5);
out->enabled = sqlite3_column_int(stmt, 6);
if (fill_cron_job_row(stmt, out) != 0) {
sqlite3_finalize(stmt);
return -1;
}
ret = 1;
}
sqlite3_finalize(stmt);
Expand Down
28 changes: 23 additions & 5 deletions src/core/memory.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,34 @@ int config_kv_get(const char *key, char *value_out, size_t max_len);
*/
int config_kv_set(const char *key, const char *value);

/** Row from cron_jobs table for list/get operations. */
/**
* Row from cron_jobs table for list/get operations.
*
* schedule and message are heap copies of the SQLite TEXT columns. The
* caller must cron_job_row_free() each filled row. Example:
* cron_job_row_t row;
* memset(&row, 0, sizeof(row));
* if (cron_job_get_next_due(now, &row) == 1) {
* cron_job_row_free(&row);
* }
*/
typedef struct cron_job_row {
char id[128];
char schedule[128];
char message[512];
char *schedule;
char *message;
char channel[64];
char recipient[64];
long long next_run;
int enabled;
} cron_job_row_t;

/**
* Free heap fields on a cron job row. Safe on NULL, zeroed, or already-freed rows.
*
* @param row Row to release (may be NULL).
*/
void cron_job_row_free(cron_job_row_t *row);

/**
* Create a cron job.
*
Expand Down Expand Up @@ -140,7 +157,8 @@ int cron_job_update_next_run(const char *id, long long next_run);
/**
* List cron jobs into output array.
*
* @param out Array to fill (caller-allocated).
* @param out Array to fill (caller-allocated). Heap fields are owned
* by the caller on success; on -1, no row is owned.
* @param max_count Maximum jobs to return.
* @return Number of jobs written, or -1 on error.
*/
Expand All @@ -150,7 +168,7 @@ int cron_job_list(cron_job_row_t *out, int max_count);
* Get the next due job (next_run <= now, enabled).
*
* @param now Current Unix timestamp.
* @param out Filled with job data if found.
* @param out Filled with job data if found. Caller must cron_job_row_free().
* @return 1 if found, 0 if none, -1 on error.
*/
int cron_job_get_next_due(long long now, cron_job_row_t *out);
Expand Down
25 changes: 22 additions & 3 deletions src/gateway/routes.c
Original file line number Diff line number Diff line change
Expand Up @@ -399,30 +399,49 @@ static void handle_session_delete(const char *id, char *buf, size_t size, int *s
json_response(buf, size, status, "{\"ok\":true}");
}

static void free_cron_job_rows(cron_job_row_t *rows, int n)
{
if (!rows || n <= 0) return;
for (int i = 0; i < n; i++)
cron_job_row_free(&rows[i]);
}

static void handle_cron_list(char *buf, size_t size, int *status)
{
cron_job_row_t *rows = calloc(64, sizeof(cron_job_row_t));
if (!rows) { json_error(buf, size, status, 500, "Out of memory"); return; }
int n = cron_job_list(rows, 64);
if (n < 0) {
free(rows);
json_error(buf, size, status, 500, "Internal error");
return;
}
cJSON *arr = cJSON_CreateArray();
if (!arr) { free(rows); json_error(buf, size, status, 500, "Internal error"); return; }
if (!arr) {
free_cron_job_rows(rows, n);
free(rows);
json_error(buf, size, status, 500, "Internal error");
return;
}
for (int i = 0; i < n; i++) {
cJSON *obj = cJSON_CreateObject();
if (!obj) {
free_cron_job_rows(rows, n);
free(rows);
cJSON_Delete(arr);
json_error(buf, size, status, 500, "Internal error");
return;
}
cJSON_AddItemToObject(obj, "id", cJSON_CreateString(rows[i].id));
cJSON_AddItemToObject(obj, "schedule", cJSON_CreateString(rows[i].schedule));
cJSON_AddItemToObject(obj, "message", cJSON_CreateString(rows[i].message));
cJSON_AddItemToObject(obj, "schedule", cJSON_CreateString(rows[i].schedule ? rows[i].schedule : ""));
cJSON_AddItemToObject(obj, "message", cJSON_CreateString(rows[i].message ? rows[i].message : ""));
cJSON_AddItemToObject(obj, "channel", cJSON_CreateString(rows[i].channel));
cJSON_AddItemToObject(obj, "recipient", cJSON_CreateString(rows[i].recipient));
cJSON_AddItemToObject(obj, "next_run", cJSON_CreateNumber((double)rows[i].next_run));
cJSON_AddItemToObject(obj, "enabled", cJSON_CreateBool(rows[i].enabled));
cJSON_AddItemToArray(arr, obj);
}
free_cron_job_rows(rows, n);
free(rows);
json_print_to_buf(arr, buf, size, status);
cJSON_Delete(arr);
Expand Down
21 changes: 17 additions & 4 deletions src/tools/cron.c
Original file line number Diff line number Diff line change
Expand Up @@ -194,9 +194,10 @@ static int cron_poll(channel_incoming_msg_t *out, int timeout_ms)
row.recipient[0] ? row.recipient : "default");
out->session_id = strdup(session_id);
out->user_id = strdup(row.id);
out->text = strdup(row.message);
out->text = strdup(row.message ? row.message : "");
out->attachments = NULL;
out->attachments_count = 0;
cron_job_row_free(&row);
return 1;
}

Expand Down Expand Up @@ -251,21 +252,33 @@ static int cron_tool_execute(const char *args_json, char *result_buf, size_t max
int ret = 0;
if (strcmp(operation, "list") == 0) {
cron_job_row_t rows[64];
memset(rows, 0, sizeof(rows));
int n = cron_job_list(rows, 64);
if (n < 0) {
cJSON_Delete(root);
snprintf(result_buf, max_len, "{\"error\":\"failed to list jobs\"}");
return -1;
}
cJSON *arr = cJSON_CreateArray();
if (!arr) { cJSON_Delete(root); snprintf(result_buf, max_len, "{\"error\":\"out of memory\"}"); return -1; }
if (!arr) {
for (int i = 0; i < n; i++) cron_job_row_free(&rows[i]);
cJSON_Delete(root);
snprintf(result_buf, max_len, "{\"error\":\"out of memory\"}");
return -1;
}
for (int i = 0; i < n; i++) {
cJSON *obj = cJSON_CreateObject();
if (!obj) break;
cJSON_AddItemToObject(obj, "id", cJSON_CreateString(rows[i].id));
cJSON_AddItemToObject(obj, "schedule", cJSON_CreateString(rows[i].schedule));
cJSON_AddItemToObject(obj, "message", cJSON_CreateString(rows[i].message));
cJSON_AddItemToObject(obj, "schedule", cJSON_CreateString(rows[i].schedule ? rows[i].schedule : ""));
cJSON_AddItemToObject(obj, "message", cJSON_CreateString(rows[i].message ? rows[i].message : ""));
cJSON_AddItemToObject(obj, "channel", cJSON_CreateString(rows[i].channel));
cJSON_AddItemToObject(obj, "recipient", cJSON_CreateString(rows[i].recipient));
cJSON_AddItemToObject(obj, "next_run", cJSON_CreateNumber((double)rows[i].next_run));
cJSON_AddItemToObject(obj, "enabled", cJSON_CreateBool(rows[i].enabled));
cJSON_AddItemToArray(arr, obj);
}
for (int i = 0; i < n; i++) cron_job_row_free(&rows[i]);
char *s = cJSON_PrintUnformatted(arr);
cJSON_Delete(arr);
if (s) {
Expand Down
69 changes: 68 additions & 1 deletion tests/test_cron.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include "tools/cron.h"
#include "core/memory.h"
#include "channels/channel.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Expand Down Expand Up @@ -74,16 +75,22 @@ static int test_cron_job_crud_and_due(void)
ASSERT(cron_parse_next_run("at:9999999999", now, &next) == 0);
ASSERT(cron_job_create("job1", "at:9999999999", "Remind me", "cli", "default", next, 1) == 0);
cron_job_row_t rows[16];
memset(rows, 0, sizeof(rows));
int n = cron_job_list(rows, 16);
ASSERT(n == 1);
ASSERT(strcmp(rows[0].id, "job1") == 0);
ASSERT(strcmp(rows[0].message, "Remind me") == 0);
for (int i = 0; i < n; i++) cron_job_row_free(&rows[i]);
cron_job_row_t due;
memset(&due, 0, sizeof(due));
ASSERT(cron_job_get_next_due(now, &due) == 0);
ASSERT(cron_job_get_next_due(9999999999, &due) == 1);
ASSERT(strcmp(due.id, "job1") == 0);
cron_job_row_free(&due);
ASSERT(cron_job_toggle("job1") == 0);
memset(&due, 0, sizeof(due));
ASSERT(cron_job_get_next_due(9999999999, &due) == 0);
cron_job_row_free(&due);
ASSERT(cron_job_toggle("job1") == 0);
ASSERT(cron_job_delete("job1") == 0);
ASSERT(cron_job_list(rows, 16) == 0);
Expand All @@ -108,9 +115,12 @@ static int test_cron_tool_execute(void)
ASSERT(cron_tool->execute("{\"operation\":\"list\"}", buf, sizeof(buf)) == 0);
ASSERT(strstr(buf, "test") != NULL);
cron_job_row_t rows[16];
memset(rows, 0, sizeof(rows));
int n = cron_job_list(rows, 16);
ASSERT(n >= 1);
const char *id = rows[0].id;
char id[128];
snprintf(id, sizeof(id), "%s", rows[0].id);
for (int i = 0; i < n; i++) cron_job_row_free(&rows[i]);
char del_json[256];
snprintf(del_json, sizeof(del_json), "{\"operation\":\"delete\",\"id\":\"%s\"}", id);
ASSERT(cron_tool->execute(del_json, buf, sizeof(buf)) == 0);
Expand All @@ -131,14 +141,69 @@ static int test_one_shot_detection(void)
ASSERT(memory_init(path) == 0);
ASSERT(cron_job_create("oneshot1", "at:9999999999", "One-shot", "cli", "default", 9999999999, 1) == 0);
cron_job_row_t due;
memset(&due, 0, sizeof(due));
ASSERT(cron_job_get_next_due(9999999999, &due) == 1);
ASSERT(cron_is_one_shot(due.schedule) == 1);
cron_job_row_free(&due);
cron_job_delete("oneshot1");
memory_cleanup();
remove(path);
return 0;
}

static int test_due_job_delivers_full_message(void)
{
const char *path = "/tmp/shellclaw_test_cron_long_message.db";
remove(path);
ASSERT(memory_init(path) == 0);
char message[600];
memset(message, 'A', 599);
message[599] = '\0';
long long now = (long long)time(NULL);
ASSERT(cron_job_create("longmsg", "interval:60", message, "cli", "default", now - 1, 1) == 0);
const channel_t *ch = channel_cron_get();
channel_incoming_msg_t msg;
memset(&msg, 0, sizeof(msg));
ASSERT(ch->poll(&msg, 0) == 1);
ASSERT(msg.text != NULL);
ASSERT(strlen(msg.text) == 599);
ASSERT(strcmp(msg.text, message) == 0);
channel_incoming_msg_clear(&msg);
memory_cleanup();
remove(path);
return 0;
}

static int test_long_interval_schedule_advances_next_run(void)
{
const char *path = "/tmp/shellclaw_test_cron_long_schedule.db";
remove(path);
ASSERT(memory_init(path) == 0);
char zeros[121];
memset(zeros, '0', 120);
zeros[120] = '\0';
char schedule[160];
snprintf(schedule, sizeof(schedule), "interval:%s60", zeros);
ASSERT(strlen(schedule) > 127);
long long parsed = 0;
long long now = (long long)time(NULL);
ASSERT(cron_parse_next_run(schedule, now, &parsed) == 0);
ASSERT(parsed == now + 60);
ASSERT(cron_job_create("longsched", schedule, "tick", "cli", "default", now - 1, 1) == 0);
const channel_t *ch = channel_cron_get();
channel_incoming_msg_t msg;
memset(&msg, 0, sizeof(msg));
ASSERT(ch->poll(&msg, 0) == 1);
ASSERT(msg.text != NULL && strcmp(msg.text, "tick") == 0);
channel_incoming_msg_clear(&msg);
memset(&msg, 0, sizeof(msg));
ASSERT(ch->poll(&msg, 0) == 0);
channel_incoming_msg_clear(&msg);
memory_cleanup();
remove(path);
return 0;
}

int main(void)
{
RUN(test_interval_next_run());
Expand All @@ -149,6 +214,8 @@ int main(void)
RUN(test_cron_job_crud_and_due());
RUN(test_cron_tool_execute());
RUN(test_one_shot_detection());
RUN(test_due_job_delivers_full_message());
RUN(test_long_interval_schedule_advances_next_run());
printf("test_cron: all tests passed\n");
return 0;
}
Loading