- Add !wp <search terms> command: searches Wikipedia via MediaWiki API (action=query&list=search), displays article titles, snippets, and links. Single-result mode fetches the first chapter via Extracts API (exchars=800); multi-result mode shows a numbered list with short snippets and per-result Read full article links. - Add self-contained JSON parser helpers: wp_json_get_string(), wp_json_next_array_item(), wp_decode_json_string() with \uXXXX Unicode support. - Add wp_url_encode() (RFC 3986), wp_codepoint_to_utf8(), wp_strip_html(), wp_collapse_spaces(), wp_hex_digit(). - Add WITH_WP CMake feature toggle (ON by default, HAS_CMD_WP compile def). - Add !wp to the !help command listing. - Add doc comments to all 9 functions in cmd_wp.h. - Update CHANGELOG.md with 0.43.0 entry. - Update README.md: add Wikipedia to features list, feature toggles table, IRC commands utilities table, and minimal build command.
1062 lines
34 KiB
C
1062 lines
34 KiB
C
/*
|
|
* Copyright (c) armin, hanez
|
|
*
|
|
* Licensed under the terms of the MIT License.
|
|
*/
|
|
|
|
#include "ai.h"
|
|
#include "net.h"
|
|
#include "tls.h"
|
|
#include "zynk.h"
|
|
|
|
int tls_verify = 1;
|
|
|
|
/* ---- logging ---- */
|
|
|
|
/*
|
|
* log_stamp - Print a timestamped log prefix to stderr.
|
|
*
|
|
* Outputs the current local time in "[YY-MM-DD HH:MM:SS]" format using
|
|
* ANSI dim styling, followed by a space. Intended to be called before any
|
|
* log message on the same fprintf line. Uses localtime() and strftime()
|
|
* with a 64-byte stack buffer.
|
|
*
|
|
* Returns: void (output goes to stderr).
|
|
*/
|
|
void log_stamp(void) {
|
|
struct tm *tm;
|
|
time_t t = time(NULL);
|
|
tm = localtime(&t);
|
|
char buf[64];
|
|
strftime(buf, sizeof buf, "%Y-%m-%d %H:%M:%S", tm);
|
|
fprintf(stderr, CLR_DIM "[%s]" CLR_RESET " ", buf);
|
|
}
|
|
|
|
/* ---- hot reload ---- */
|
|
|
|
/*
|
|
* ZYNK_RELOAD_FILE - Temporary file used to persist TLS session state
|
|
* across hot reloads. Written by reload_save() with the full SSL session
|
|
* (hex-encoded) and all connection parameters (host, port, nick, etc.),
|
|
* then read back by reload_try_restore() so the new process can resume
|
|
* the IRC connection without a visible disconnect. The file is created
|
|
* with mode 0600 via secure_state_open() and unlinked after use.
|
|
*/
|
|
#define ZYNK_RELOAD_FILE "/tmp/zynk_reload"
|
|
|
|
/*
|
|
* secure_state_open - Open a file descriptor with security hardening.
|
|
*
|
|
* Opens the given path with O_CLOEXEC and O_NOFOLLOW appended to the
|
|
* caller-supplied flags, and mode 0600 (owner read/write only). After
|
|
* opening, verifies the file is a regular file (S_ISREG) owned by the
|
|
* effective user (geteuid). If O_CREAT is among the flags, fchmod is
|
|
* called to enforce S_IRUSR | S_IWUSR. This prevents TOCTOU races,
|
|
* symlink attacks, and world-writable state files.
|
|
*
|
|
* Parameters:
|
|
* path - The filesystem path to open.
|
|
* flags - open() flags (O_CLOEXEC and O_NOFOLLOW are added implicitly).
|
|
*
|
|
* Returns: A valid file descriptor on success, or -1 on failure (with
|
|
* errno set to EPERM for ownership/type checks).
|
|
*/
|
|
int secure_state_open(const char *path, int flags) {
|
|
int fd = open(path, flags | O_CLOEXEC | O_NOFOLLOW, 0600);
|
|
if (fd < 0) return -1;
|
|
struct stat st;
|
|
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode) || st.st_uid != geteuid()) {
|
|
close(fd);
|
|
errno = EPERM;
|
|
return -1;
|
|
}
|
|
if (flags & O_CREAT) fchmod(fd, S_IRUSR | S_IWUSR);
|
|
return fd;
|
|
}
|
|
|
|
/*
|
|
* reload_read_line - Read a single line from a file, stripping newlines.
|
|
*
|
|
* Helper for parsing the reload state file. Reads up to sz-1 bytes from
|
|
* the file handle, then strips trailing \n and \r characters.
|
|
*
|
|
* Parameters:
|
|
* f - The FILE handle to read from.
|
|
* buf - Destination buffer.
|
|
* sz - Size of the destination buffer.
|
|
*
|
|
* Returns: 0 on success, -1 on read failure or EOF.
|
|
*/
|
|
static int reload_read_line(FILE *f, char *buf, size_t sz) {
|
|
if (!fgets(buf, sz, f)) return -1;
|
|
size_t len = strlen(buf);
|
|
while (len > 0 && (buf[len-1] == '\n' || buf[len-1] == '\r')) buf[--len] = 0;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* reload_save - Save the current TLS session state to disk for hot reload.
|
|
*
|
|
* Serializes the SSL session (via i2d_SSL_SESSION) and all connection
|
|
* parameters (host, port, nick, user, real, pass, auto_join, tls settings)
|
|
* into the /tmp/zynk_reload file. The session data is hex-encoded. This
|
|
* allows the new process to resume the TLS connection without re-handshaking,
|
|
* avoiding a visible disconnect in the IRC channel.
|
|
*
|
|
* Parameters:
|
|
* s - The current IRC session to serialize.
|
|
*
|
|
* Returns: 0 on success, -1 on any failure.
|
|
*/
|
|
static int reload_save(Session *s) {
|
|
if (!s->ssl || s->fd < 0) return -1;
|
|
SSL_SESSION *sess = SSL_get1_session(s->ssl);
|
|
if (!sess) return -1;
|
|
int slen = i2d_SSL_SESSION(sess, NULL);
|
|
if (slen <= 0) { SSL_SESSION_free(sess); return -1; }
|
|
unsigned char *sbuf = malloc(slen);
|
|
if (!sbuf) { SSL_SESSION_free(sess); return -1; }
|
|
unsigned char *p = sbuf;
|
|
i2d_SSL_SESSION(sess, &p);
|
|
SSL_SESSION_free(sess);
|
|
int fd = secure_state_open(ZYNK_RELOAD_FILE, O_WRONLY | O_CREAT | O_TRUNC);
|
|
FILE *f = fd >= 0 ? fdopen(fd, "w") : NULL;
|
|
if (!f) { free(sbuf); return -1; }
|
|
fprintf(f, "%d\n%s\n%d\n%s\n%s\n%s\n%s\n%s\n%d\n%d\n",
|
|
slen, s->host, s->port, s->nick, s->user, s->real,
|
|
s->pass, s->auto_join, s->use_tls, tls_verify);
|
|
for (int i = 0; i < slen; i++) fprintf(f, "%02x", sbuf[i]);
|
|
fprintf(f, "\n");
|
|
fclose(f);
|
|
free(sbuf);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* reload_try_restore - Restore previously saved connection settings from disk.
|
|
*
|
|
* Reads the /tmp/zynk_reload file, restores all connection parameters, and
|
|
* checks whether the restart was triggered by a code change. The caller then
|
|
* establishes a fresh IRC and TLS connection.
|
|
*
|
|
* Parameters:
|
|
* s - The IRC session to populate with restored state.
|
|
*
|
|
* Returns: 1 when settings were restored, -1 if no reload file or on error.
|
|
*/
|
|
static int reload_try_restore(Session *s) {
|
|
int fd = secure_state_open(ZYNK_RELOAD_FILE, O_RDONLY);
|
|
FILE *f = fd >= 0 ? fdopen(fd, "r") : NULL;
|
|
if (!f) return -1;
|
|
char line[1024];
|
|
int slen = 0, port = 0, use_tls_f = 0, tls_verify_f = 0;
|
|
char host[256] = "", nick[32] = "", user[32] = "", real[256] = "";
|
|
char pass[256] = "", auto_join[256] = "";
|
|
if (reload_read_line(f, line, sizeof line) < 0 || (slen = atoi(line)) <= 0 || slen > 65536) goto fail;
|
|
if (reload_read_line(f, host, sizeof host) < 0 || !host[0]) goto fail;
|
|
if (reload_read_line(f, line, sizeof line) < 0) goto fail;
|
|
port = atoi(line);
|
|
if (reload_read_line(f, nick, sizeof nick) < 0 || !nick[0]) goto fail;
|
|
if (reload_read_line(f, user, sizeof user) < 0 || !user[0]) goto fail;
|
|
if (reload_read_line(f, real, sizeof real) < 0) goto fail;
|
|
if (reload_read_line(f, pass, sizeof pass) < 0) goto fail;
|
|
if (reload_read_line(f, auto_join, sizeof auto_join) < 0) goto fail;
|
|
if (reload_read_line(f, line, sizeof line) < 0) goto fail;
|
|
use_tls_f = atoi(line);
|
|
if (reload_read_line(f, line, sizeof line) < 0) goto fail;
|
|
tls_verify_f = atoi(line);
|
|
unsigned char *sbuf = malloc(slen);
|
|
if (!sbuf) goto fail;
|
|
for (int i = 0; i < slen; i++) {
|
|
unsigned int byte;
|
|
if (fscanf(f, "%02x", &byte) != 1) { free(sbuf); goto fail; }
|
|
sbuf[i] = (unsigned char)byte;
|
|
}
|
|
fclose(f);
|
|
unlink(ZYNK_RELOAD_FILE);
|
|
snprintf(s->host, sizeof s->host, "%s", host);
|
|
s->port = port;
|
|
snprintf(s->nick, sizeof s->nick, "%s", nick);
|
|
snprintf(s->user, sizeof s->user, "%s", user);
|
|
snprintf(s->real, sizeof s->real, "%s", real);
|
|
if (pass[0]) snprintf(s->pass, sizeof s->pass, "%s", pass);
|
|
snprintf(s->auto_join, sizeof s->auto_join, "%s", auto_join);
|
|
s->use_tls = use_tls_f;
|
|
tls_verify = tls_verify_f;
|
|
free(sbuf);
|
|
int cfd = secure_state_open("/tmp/zynk_code_restart", O_RDONLY);
|
|
FILE *cf = cfd >= 0 ? fdopen(cfd, "r") : NULL;
|
|
if (cf) {
|
|
fclose(cf);
|
|
unlink("/tmp/zynk_code_restart");
|
|
s->code_restart = 1;
|
|
}
|
|
log_stamp(); fprintf(stderr, CLR_GREEN "Restart state restored for %s:%d" CLR_RESET "\n", s->host, s->port);
|
|
return 1;
|
|
fail:
|
|
fclose(f);
|
|
unlink(ZYNK_RELOAD_FILE);
|
|
return -1;
|
|
}
|
|
|
|
/*
|
|
* reload_do - Execute a hot reload: save state, fork-in-place or fork+exec.
|
|
*
|
|
* Orchestrates the full hot reload sequence: saves TLS session state via
|
|
* reload_save(), then either forks-in-place (exec_new=0: child inherits the
|
|
* live socket and SSL session, continues the event loop) or fork+execs a
|
|
* new binary (exec_new=1: sends QUIT, child exec's the same binary).
|
|
*
|
|
* Parameters:
|
|
* s - The IRC session.
|
|
* reply_target - Channel or nick to send status messages to.
|
|
*
|
|
* Returns: void (parent exits, child continues).
|
|
*/
|
|
void reload_do(Session *s, const char *reply_target, int exec_new) {
|
|
if (!s->ssl || s->fd < 0) {
|
|
irc_msg(s, reply_target, "Cannot reload: not connected with TLS");
|
|
return;
|
|
}
|
|
if (exec_new && reload_save(s) < 0) {
|
|
irc_msg(s, reply_target, "Failed to save session state");
|
|
return;
|
|
}
|
|
irc_msg(s, reply_target, exec_new ? "Reloading..." : "Reloading (session preserved)...");
|
|
struct timespec ts_short = { .tv_sec = 0, .tv_nsec = 100000000 };
|
|
nanosleep(&ts_short, NULL);
|
|
if (exec_new) {
|
|
int old_fl = fcntl(s->fd, F_GETFL, 0);
|
|
fcntl(s->fd, F_SETFL, old_fl & ~O_NONBLOCK);
|
|
net_send(s, "QUIT :restarting\r\n");
|
|
struct timespec ts_quit = { .tv_sec = 0, .tv_nsec = 500000000 };
|
|
nanosleep(&ts_quit, NULL);
|
|
}
|
|
pid_t pid = fork();
|
|
if (pid < 0) {
|
|
unlink(ZYNK_RELOAD_FILE);
|
|
irc_msg(s, reply_target, "Fork failed, continuing");
|
|
return;
|
|
}
|
|
if (pid == 0) {
|
|
if (exec_new) {
|
|
close(s->fd);
|
|
char self[4096];
|
|
ssize_t n = readlink("/proc/self/exe", self, sizeof self - 1);
|
|
if (n > 0) {
|
|
self[n] = 0;
|
|
char *deleted = strstr(self, " (deleted)");
|
|
if (deleted) *deleted = 0;
|
|
execl(self, "zynk", NULL);
|
|
}
|
|
_exit(1);
|
|
}
|
|
unlink(ZYNK_RELOAD_FILE);
|
|
return;
|
|
}
|
|
_exit(0);
|
|
}
|
|
|
|
/* ---- signal handling ---- */
|
|
|
|
/*
|
|
* running - Main loop control flag, set to 0 by signal handlers.
|
|
*
|
|
* Checked by the main poll loop to determine when to shut down.
|
|
* Declared volatile sig_atomic_t for async-signal-safety: SIGINT and
|
|
* SIGTERM handlers set it to 0, causing the loop to exit gracefully.
|
|
*/
|
|
volatile sig_atomic_t running = 1;
|
|
|
|
/*
|
|
* start_time - Timestamp when the bot process started.
|
|
*
|
|
* Set once during initialization, used by the !uptime command to
|
|
* calculate and display the bot's running time.
|
|
*/
|
|
time_t start_time;
|
|
|
|
/*
|
|
* handle_sigterm - Signal handler for SIGINT and SIGTERM.
|
|
*
|
|
* Sets the global running flag to 0, which causes the main poll loop to
|
|
* exit gracefully. Uses sig_atomic_t for async-signal-safety.
|
|
*
|
|
* Parameters:
|
|
* sig - The signal number (unused).
|
|
*
|
|
* Returns: void.
|
|
*/
|
|
static void handle_sigterm(int sig) { (void)sig; running = 0; }
|
|
|
|
/* ---- rate limiting ---- */
|
|
|
|
/*
|
|
* current_userhost - Userhost extracted from the current IRC message prefix.
|
|
*
|
|
* Set by irc_handle() from the nick!user@host prefix before dispatching
|
|
* to command handlers. Used by rate_limit_check() to key rate limiting
|
|
* on userhost instead of nick, preventing trivial bypass via nick cycling.
|
|
*/
|
|
char current_userhost[128];
|
|
|
|
/*
|
|
* RateLimitEntry - A single rate-limit tracking slot.
|
|
*
|
|
* Records the IRC userhost and the timestamp of its most recent command.
|
|
* Used by rate_limit_check() in a circular buffer of RATE_LIMIT_MAX
|
|
* entries to enforce per-userhost rate limiting. Also tracks violation
|
|
* count for escalating penalties.
|
|
*
|
|
* Fields:
|
|
* userhost - The IRC user@host string (NUL-terminated, max 127 chars).
|
|
* t - The time() timestamp of the last command from this userhost.
|
|
* violations - Number of rate limit violations within the penalty window.
|
|
*/
|
|
typedef struct {
|
|
char userhost[128];
|
|
time_t t;
|
|
int violations;
|
|
} RateLimitEntry;
|
|
|
|
/*
|
|
* rate_limits - Circular buffer of rate-limit entries.
|
|
*
|
|
* Holds RATE_LIMIT_MAX slots. When the buffer is full and a new entry
|
|
* is needed, the oldest slot is evicted (LRU). A nick whose most
|
|
* recent timestamp is within RATE_LIMIT_WINDOW seconds is rejected.
|
|
*/
|
|
static RateLimitEntry rate_limits[RATE_LIMIT_MAX];
|
|
|
|
/*
|
|
* rate_limit_check - Enforce per-userhost rate limiting on IRC commands.
|
|
*
|
|
* Maintains a fixed-size (RATE_LIMIT_MAX) circular buffer of (userhost, timestamp)
|
|
* entries. When a userhost is checked, if its most recent entry is within the
|
|
* RATE_LIMIT_WINDOW seconds, the request is rejected and the violation count is
|
|
* incremented. After 3 violations within 60 seconds, a warning is sent. After
|
|
* 5, a KICK is requested (if the bot has ops). This prevents any single user
|
|
* from flooding the bot with commands, and prevents trivial bypass via nick
|
|
* cycling.
|
|
*
|
|
* Channel operators are exempt from rate limiting. If channel is non-NULL
|
|
* and starts with '#', checks chan_is_op() and returns 0 (allowed) for ops.
|
|
*
|
|
* Parameters:
|
|
* nick - The IRC nick (used for ops exemption check).
|
|
* channel - The channel name (or NULL for DMs). Ops in this channel
|
|
* are exempt from rate limiting.
|
|
* userhost - The IRC user@host string from the message prefix. Used as
|
|
* the rate-limit key instead of nick to prevent nick cycling.
|
|
*
|
|
* Returns: 0 if the command is allowed, -1 if rate-limited.
|
|
*/
|
|
int rate_limit_check(const char *nick, const char *channel, const char *userhost) {
|
|
if (channel && channel[0] == '#' && chan_is_op(channel, nick)) return 0;
|
|
const char *key = (userhost && *userhost) ? userhost : nick;
|
|
if (!key || !*key) return 0;
|
|
time_t now = time(NULL);
|
|
int slot = -1, oldest = 0;
|
|
for (int i = 0; i < RATE_LIMIT_MAX; i++) {
|
|
if (rate_limits[i].t == 0) { slot = i; break; }
|
|
if (rate_limits[i].t < rate_limits[oldest].t) oldest = i;
|
|
if (strcmp(rate_limits[i].userhost, key) == 0) {
|
|
if (now - rate_limits[i].t < RATE_LIMIT_WINDOW) {
|
|
rate_limits[i].violations++;
|
|
if (rate_limits[i].violations == 3 && channel && channel[0] == '#') {
|
|
char warn[256];
|
|
snprintf(warn, sizeof warn, "%s: please slow down.", nick);
|
|
/* irc_msg not available here; return -1 and let caller handle warning */
|
|
}
|
|
return -1;
|
|
}
|
|
slot = i; break;
|
|
}
|
|
}
|
|
if (slot < 0) slot = oldest;
|
|
strncpy(rate_limits[slot].userhost, key, sizeof rate_limits[slot].userhost - 1);
|
|
rate_limits[slot].userhost[sizeof rate_limits[slot].userhost - 1] = 0;
|
|
rate_limits[slot].t = now;
|
|
if (strcmp(rate_limits[slot].userhost, key) == 0 && rate_limits[slot].violations > 0) {
|
|
rate_limits[slot].violations = 0;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* ---- text utilities ---- */
|
|
|
|
/*
|
|
* strip_formatting - Strip markdown, ANSI escape codes, and IRC formatting.
|
|
*
|
|
* Removes: ANSI escape sequences, IRC mIRC color codes (\x03), bold/italic/
|
|
* underline/reverse/control characters, markdown code blocks (triple backticks
|
|
* kept as content), inline code backticks, bold (**), italic (*), and
|
|
* markdown links [text](url) -> text. Collapses excessive newlines (max 2
|
|
* consecutive) and double spaces. Processes in-place via a two-pointer
|
|
* approach for efficiency.
|
|
*
|
|
* Parameters:
|
|
* s - The string to strip in-place. Must be writable.
|
|
*
|
|
* Returns: void. The string is modified in-place.
|
|
*/
|
|
void strip_formatting(char *s) {
|
|
char *r = s, *start = s;
|
|
int in_code_block = 0;
|
|
while (*s) {
|
|
if (*s == '\x1b') {
|
|
s++;
|
|
while (*s && (*s == '[' || (*s >= '0' && *s <= '?'))) s++;
|
|
if (*s && *s != 'm') while (*s && *s != 'm' && *s != '\x1b') s++;
|
|
if (*s == 'm') s++;
|
|
continue;
|
|
}
|
|
if (*s == '\x02' || *s == '\x0f' || *s == '\x16' || *s == '\x1d' || *s == '\x1f') { s++; continue; }
|
|
if (*s == '\x03') {
|
|
s++;
|
|
if (*s >= '0' && *s <= '9') { s++; if (*s >= '0' && *s <= '9') s++; }
|
|
if (*s == ',') { s++; if (*s >= '0' && *s <= '9') { s++; if (*s >= '0' && *s <= '9') s++; } }
|
|
continue;
|
|
}
|
|
if (s[0] == '`' && s[1] == '`' && s[2] == '`') {
|
|
in_code_block = !in_code_block;
|
|
s += 3;
|
|
if (*s == '\n') s++;
|
|
continue;
|
|
}
|
|
if (in_code_block) { *r++ = *s++; continue; }
|
|
if (*s == '`') { s++; continue; }
|
|
if (s[0] == '*' && s[1] == '*') {
|
|
s += 2;
|
|
while (*s && !(s[0] == '*' && s[1] == '*')) s++;
|
|
if (*s) s += 2;
|
|
continue;
|
|
}
|
|
if (*s == '*' && s[1] && s[1] != '*') {
|
|
s++;
|
|
while (*s && *s != '*') s++;
|
|
if (*s == '*') s++;
|
|
continue;
|
|
}
|
|
if (*s == '[') {
|
|
char *rb = strchr(s, ']');
|
|
if (rb && *(rb+1) == '(') {
|
|
char *lp = strchr(rb+2, ')');
|
|
if (lp) { s++; while (s < rb) *r++ = *s++; s = lp + 1; continue; }
|
|
}
|
|
}
|
|
*r++ = *s++;
|
|
}
|
|
*r = 0;
|
|
char *src = start, *dst = start;
|
|
int nlc = 0;
|
|
while (*src) {
|
|
if (*src == '\n') { nlc++; if (nlc > 2) { src++; continue; } }
|
|
else nlc = 0;
|
|
*dst++ = *src++;
|
|
}
|
|
*dst = 0;
|
|
src = start; dst = start;
|
|
while (*src) {
|
|
if (*src == ' ' && *(src+1) == ' ') { src++; continue; }
|
|
*dst++ = *src++;
|
|
}
|
|
*dst = 0;
|
|
}
|
|
|
|
/*
|
|
* has_crlf - Check if a string contains CR or LF characters.
|
|
*
|
|
* Used as a security check to prevent IRC protocol injection via
|
|
* embedded newlines in user-provided data.
|
|
*
|
|
* Parameters:
|
|
* s - The string to check (may be NULL).
|
|
*
|
|
* Returns: 1 if \r or \n is found, 0 if not found or s is NULL.
|
|
*/
|
|
int has_crlf(const char *s) {
|
|
return s && strpbrk(s, "\r\n") != NULL;
|
|
}
|
|
|
|
/*
|
|
* has_ctl - Check if a string contains ASCII control characters.
|
|
*
|
|
* Scans for bytes below 0x20 (excluding common whitespace) or 0x7f (DEL).
|
|
* Used to validate IRC inputs and reject strings with embedded control
|
|
* codes that could disrupt the protocol.
|
|
*
|
|
* Parameters:
|
|
* s - The string to check (may be NULL).
|
|
*
|
|
* Returns: 1 if a control character is found, 0 otherwise.
|
|
*/
|
|
int has_ctl(const char *s) {
|
|
if (!s) return 0;
|
|
for (const unsigned char *p = (const unsigned char*)s; *p; p++)
|
|
if (*p < 0x20 || *p == 0x7f) return 1;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* normalize_city - Normalize a user-provided city name for display and URLs.
|
|
*
|
|
* Produces two output forms: a display form with spaces, and a URL form
|
|
* with '+' replacing spaces. Strips non-alphanumeric characters except
|
|
* dots, commas, hyphens, and underscores. Collapses multiple spaces into
|
|
* single spaces. Defaults to "Berlin" if the input is empty or results
|
|
* in an empty string after sanitization.
|
|
*
|
|
* Parameters:
|
|
* in - The raw city name from the user.
|
|
* display - Output buffer for the display form (e.g. "New York").
|
|
* dsz - Size of the display buffer.
|
|
* url - Output buffer for the URL form (e.g. "New+York").
|
|
* usz - Size of the URL buffer.
|
|
*
|
|
* Returns: void.
|
|
*/
|
|
void normalize_city(const char *in, char *display, size_t dsz, char *url, size_t usz) {
|
|
size_t dl = 0, ul = 0;
|
|
int pending_space = 0;
|
|
if (!in || !*in) in = "Berlin";
|
|
for (const unsigned char *p = (const unsigned char *)in; *p; p++) {
|
|
int word = isalnum(*p) || *p == '.' || *p == ',' || *p == '-' || *p == '_';
|
|
int space = *p == ' ' || *p == '\t' || *p == '+';
|
|
if (!word && !space) continue;
|
|
if (space) {
|
|
pending_space = dl > 0;
|
|
continue;
|
|
}
|
|
if (pending_space) {
|
|
if (dl + 1 < dsz) display[dl++] = ' ';
|
|
if (ul + 1 < usz) url[ul++] = '+';
|
|
pending_space = 0;
|
|
}
|
|
if (dl + 1 < dsz) display[dl++] = *p;
|
|
if (ul + 1 < usz) url[ul++] = *p;
|
|
}
|
|
if (dl == 0) {
|
|
snprintf(display, dsz, "Berlin");
|
|
snprintf(url, usz, "Berlin");
|
|
} else {
|
|
display[dl] = 0;
|
|
url[ul] = 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* collapse_spaces - Collapse runs of whitespace into single spaces.
|
|
*
|
|
* Modifies a string in place, replacing each run of one or more spaces
|
|
* with a single space. Leading and trailing spaces are also removed.
|
|
* No-op if the string is NULL or empty.
|
|
*
|
|
* Parameters:
|
|
* s - The string to modify in place.
|
|
*
|
|
* Returns: void.
|
|
*/
|
|
void collapse_spaces(char *s) {
|
|
char *r = s, *w = s;
|
|
int in_space = 1;
|
|
while (*r) {
|
|
if (*r == ' ' || *r == '\t' || *r == '\n' || *r == '\r') {
|
|
if (!in_space) *w++ = ' ';
|
|
in_space = 1;
|
|
} else {
|
|
*w++ = *r;
|
|
in_space = 0;
|
|
}
|
|
r++;
|
|
}
|
|
*w = 0;
|
|
while (w > s && *(w-1) == ' ') *--w = 0;
|
|
}
|
|
|
|
/* ---- curl helper ---- */
|
|
|
|
/*
|
|
* run_curl - Execute curl to fetch a URL and capture stdout.
|
|
*
|
|
* Forks a child process that exec's curl with a 10-second timeout,
|
|
* suppresses stderr to /dev/null, and uses a fake Firefox User-Agent.
|
|
* The parent reads up to 32KB of output from the pipe, then waits for
|
|
* the child to exit. Used as a lightweight HTTP client without linking
|
|
* libcurl.
|
|
*
|
|
* Parameters:
|
|
* url - The URL to fetch.
|
|
*
|
|
* Returns: A newly allocated string with the response body on success,
|
|
* or NULL on failure. Caller must free() the result.
|
|
*/
|
|
static char *run_curl(const char *url) {
|
|
int fd[2];
|
|
if (pipe(fd) < 0) return NULL;
|
|
pid_t pid = fork();
|
|
if (pid < 0) { close(fd[0]); close(fd[1]); return NULL; }
|
|
if (pid == 0) {
|
|
close(fd[0]);
|
|
dup2(fd[1], STDOUT_FILENO);
|
|
close(fd[1]);
|
|
int dn = open("/dev/null", O_WRONLY);
|
|
if (dn >= 0) { dup2(dn, STDERR_FILENO); close(dn); }
|
|
execlp("curl", "curl", "-s", "--max-time", "10", "-A", "zynk/" VERSION, url, (char *)NULL);
|
|
_exit(127);
|
|
}
|
|
close(fd[1]);
|
|
char buf[32768], tmp[4096];
|
|
size_t len = 0;
|
|
ssize_t n;
|
|
while ((n = read(fd[0], tmp, sizeof tmp)) > 0) {
|
|
size_t avail = sizeof buf - 1 - len;
|
|
if (avail > 0) {
|
|
size_t copy = (size_t)n < avail ? (size_t)n : avail;
|
|
memcpy(buf + len, tmp, copy);
|
|
len += copy;
|
|
}
|
|
}
|
|
close(fd[0]);
|
|
int status;
|
|
waitpid(pid, &status, 0);
|
|
buf[len] = 0;
|
|
return status == 0 ? strdup(buf) : NULL;
|
|
}
|
|
|
|
/*
|
|
* fetch_url - Fetch content from a URL via curl.
|
|
*
|
|
* Thin wrapper around run_curl() for semantic clarity. Used by weather,
|
|
* forecast, and stock commands to fetch data from external APIs.
|
|
*
|
|
* Parameters:
|
|
* url - The URL to fetch.
|
|
*
|
|
* Returns: A newly allocated string with the response body, or NULL on
|
|
* failure. Caller must free() the result.
|
|
*/
|
|
char *fetch_url(const char *url) { return run_curl(url); }
|
|
|
|
/*
|
|
* try_compile_and_restart - Attempt to compile zynk and restart if successful.
|
|
*
|
|
* Runs "make" (with stderr redirected to /tmp/zynk_make_err). If compilation
|
|
* succeeds, notifies the IRC channel and triggers a hot reload via reload_do().
|
|
* If compilation fails, reads the first line of the error output and sends it
|
|
* to IRC as a notification. Used after code-change AI commands to auto-deploy.
|
|
*
|
|
* Parameters:
|
|
* s - The IRC session.
|
|
* target - The channel or nick to send compilation status to.
|
|
*
|
|
* Returns: 1 if the restart was triggered, 0 if compilation failed.
|
|
*/
|
|
int try_compile_and_restart(Session *s, const char *target) {
|
|
log_stamp(); fprintf(stderr, CLR_GREEN "Code change detected, compiling..." CLR_RESET "\n");
|
|
int efd = secure_state_open("/tmp/zynk_make_err", O_WRONLY | O_CREAT | O_TRUNC);
|
|
if (efd < 0) {
|
|
irc_msg(s, target, "Cannot create secure build log");
|
|
return 0;
|
|
}
|
|
close(efd);
|
|
int status = system("make 2>/tmp/zynk_make_err");
|
|
if (WIFEXITED(status) && WEXITSTATUS(status) == 0) {
|
|
irc_msg(s, target, "Code compiled successfully, restarting...");
|
|
log_stamp(); fprintf(stderr, CLR_GREEN "Compilation successful, restarting..." CLR_RESET "\n");
|
|
int cfd = secure_state_open("/tmp/zynk_code_restart", O_WRONLY | O_CREAT | O_TRUNC);
|
|
FILE *cf = cfd >= 0 ? fdopen(cfd, "w") : NULL;
|
|
if (cf) { fprintf(cf, "%s\n", target); fclose(cf); }
|
|
reload_do(s, target, 1);
|
|
return 1;
|
|
} else {
|
|
char errbuf[512];
|
|
efd = secure_state_open("/tmp/zynk_make_err", O_RDONLY);
|
|
FILE *f = efd >= 0 ? fdopen(efd, "r") : NULL;
|
|
if (f) {
|
|
size_t n = fread(errbuf, 1, sizeof errbuf - 1, f);
|
|
errbuf[n] = 0;
|
|
fclose(f);
|
|
char *nl = strchr(errbuf, '\n');
|
|
if (nl) *nl = 0;
|
|
} else {
|
|
snprintf(errbuf, sizeof errbuf, "unknown error");
|
|
}
|
|
char msg[640];
|
|
snprintf(msg, sizeof msg, "Compilation failed: %s", errbuf);
|
|
irc_msg(s, target, msg);
|
|
log_stamp(); fprintf(stderr, CLR_RED "Compilation failed:" CLR_RESET " %s\n", errbuf);
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* json_find - Locate a key in a flat JSON string and return a pointer to its value.
|
|
*
|
|
* Searches for the first occurrence of key in haystack using strstr(), then
|
|
* skips past the key, any whitespace, an optional colon, more whitespace, and
|
|
* any leading '[' or space characters to return a pointer to the value. This
|
|
* is a simple helper for parsing flat JSON responses (e.g. from Yahoo Finance
|
|
* or YouTube oEmbed APIs) without a full JSON parser. Not suitable for nested
|
|
* or complex JSON structures.
|
|
*
|
|
* Parameters:
|
|
* haystack - The JSON string to search (must be NUL-terminated).
|
|
* key - The JSON key to find, including quotes (e.g. "\"title\":").
|
|
*
|
|
* Returns: A pointer into haystack at the start of the value, or NULL if the
|
|
* key is not found.
|
|
*/
|
|
const char *json_find(const char *haystack, const char *key) {
|
|
const char *p = strstr(haystack, key);
|
|
if (!p) return NULL;
|
|
p += strlen(key);
|
|
while (*p == ' ' || *p == '\t') p++;
|
|
if (*p == ':') { p++; while (*p == ' ' || *p == '\t') p++; }
|
|
while (*p == '[' || *p == ' ') p++;
|
|
return p;
|
|
}
|
|
|
|
#ifdef HAS_CMD_ZYNK
|
|
#include "cmd_zynk.h"
|
|
#endif
|
|
#ifdef HAS_CMD_PING
|
|
#include "cmd_ping.h"
|
|
#endif
|
|
#ifdef HAS_CMD_VERSION
|
|
#include "cmd_version.h"
|
|
#endif
|
|
#ifdef HAS_CMD_HELP
|
|
#include "cmd_help.h"
|
|
#endif
|
|
#ifdef HAS_CMD_QUIT
|
|
#include "cmd_quit.h"
|
|
#endif
|
|
#ifdef HAS_CMD_WEATHER
|
|
#include "cmd_weather.h"
|
|
#endif
|
|
#ifdef HAS_CMD_FORECAST
|
|
#include "cmd_forecast.h"
|
|
#endif
|
|
#ifdef HAS_CMD_STOCK
|
|
#include "cmd_stock.h"
|
|
#endif
|
|
#ifdef HAS_CMD_CALC
|
|
#include "cmd_calc.h"
|
|
#endif
|
|
#ifdef HAS_CMD_TIME
|
|
#include "cmd_time.h"
|
|
#endif
|
|
#ifdef HAS_CMD_SEEN
|
|
#include "cmd_seen.h"
|
|
#endif
|
|
#ifdef HAS_CMD_TELL
|
|
#include "cmd_tell.h"
|
|
#endif
|
|
#ifdef HAS_CMD_GITLOG
|
|
#include "cmd_gitlog.h"
|
|
#endif
|
|
#ifdef HAS_CMD_CHANGELOG
|
|
#include "cmd_changelog.h"
|
|
#endif
|
|
#ifdef HAS_CMD_AI
|
|
#include "cmd_ai.h"
|
|
#endif
|
|
#ifdef HAS_CMD_CODE
|
|
#include "cmd_code.h"
|
|
#endif
|
|
#ifdef HAS_CMD_REBUILD
|
|
#include "cmd_rebuild.h"
|
|
#endif
|
|
#ifdef HAS_CMD_UPTIME
|
|
#include "cmd_uptime.h"
|
|
#endif
|
|
#ifdef HAS_CMD_RELOAD
|
|
#include "cmd_reload.h"
|
|
#endif
|
|
#ifdef HAS_CMD_RESTART
|
|
#include "cmd_restart.h"
|
|
#endif
|
|
#ifdef HAS_CMD_YT
|
|
#include "cmd_yt.h"
|
|
#endif
|
|
#ifdef HAS_CMD_WP
|
|
#include "cmd_wp.h"
|
|
#endif
|
|
#ifdef HAS_CMD_GREETING_OR_CHAT
|
|
#include "cmd_greeting_or_chat.h"
|
|
#endif
|
|
|
|
#include "irc.h"
|
|
|
|
/* ---- usage ---- */
|
|
|
|
/*
|
|
* usage - Print command-line usage information to stderr.
|
|
*
|
|
* Displays all available command-line options (-ssl, -server, -port, -pass,
|
|
* -nick, -join, -opencode, -noverify, -h) with descriptions and defaults.
|
|
* Also lists the basic zynk store/lookup commands. Called when -h/--help
|
|
* is passed or when invalid arguments are detected.
|
|
*
|
|
* Parameters:
|
|
* prog - The program name (argv[0]).
|
|
*
|
|
* Returns: void (output goes to stderr).
|
|
*/
|
|
static void usage(const char *prog) {
|
|
fprintf(stderr,
|
|
"Usage: %s [-ssl] [-server host] [-port p] [-nick n] [-join c] [-opencode path]\n"
|
|
" -h This help\n"
|
|
" -noverify Skip TLS certificate verification\n"
|
|
" -server host IRC server (default: %s)\n"
|
|
" -port p Port (default: %d)\n"
|
|
" -pass p Server password\n"
|
|
" -nick n Nickname (default: zynk<PID>)\n"
|
|
" -join c Auto-join channels (comma-sep, default: #zynk)\n"
|
|
" -opencode p Path to opencode binary (default: %s)\n"
|
|
"\n"
|
|
"Zynk commands:\n"
|
|
" !zynk foo = bar Store 'bar' under key 'foo'\n"
|
|
" !zynk foo Retrieve value for 'foo'\n",
|
|
prog, DEFAULT_SERVER, DEF_PORT, OPENCODE_BIN);
|
|
}
|
|
|
|
/* ---- reconnect ---- */
|
|
|
|
/*
|
|
* net_reconnect - Attempt to reconnect to the IRC server after disconnection.
|
|
*
|
|
* Cleans up the existing TLS session and socket, then tries up to 10 times
|
|
* to reconnect with increasing delays (5s, 7s, 9s, ...). For each attempt,
|
|
* creates a new TCP connection, performs TLS handshake if enabled, sets
|
|
* non-blocking mode, sends PASS/NICK/USER to re-authenticate. Logs each
|
|
* attempt and gives up after 10 failures. Used by the main loop when a
|
|
* read returns 0 (clean disconnect) or a fatal error.
|
|
*
|
|
* Parameters:
|
|
* s - The IRC session to reconnect (host, port, nick, etc. are reused).
|
|
*
|
|
* Returns: 0 on successful reconnection, -1 after exhausting all attempts.
|
|
*/
|
|
static int net_reconnect(Session *s) {
|
|
tls_cleanup(s);
|
|
if (s->fd >= 0) close(s->fd);
|
|
s->fd = -1;
|
|
s->connected = 0;
|
|
s->rlen = 0;
|
|
for (int attempt = 0; attempt < 10; attempt++) {
|
|
if (attempt > 0) {
|
|
log_stamp(); fprintf(stderr, CLR_YELLOW "Reconnecting in %ds (attempt %d/10)..." CLR_RESET "\n",
|
|
5 + attempt * 2, attempt + 1);
|
|
sleep(5 + attempt * 2);
|
|
}
|
|
s->fd = net_connect(s->host, s->port);
|
|
if (s->fd < 0) {
|
|
log_stamp(); fprintf(stderr, CLR_RED "Reconnect attempt %d/10: connect failed" CLR_RESET "\n", attempt + 1);
|
|
continue;
|
|
}
|
|
if (s->use_tls && tls_handshake(s) < 0) {
|
|
log_stamp(); fprintf(stderr, CLR_RED "Reconnect attempt %d/10: TLS failed" CLR_RESET "\n", attempt + 1);
|
|
close(s->fd); s->fd = -1;
|
|
continue;
|
|
}
|
|
int fl = fcntl(s->fd, F_GETFL, 0);
|
|
fcntl(s->fd, F_SETFL, fl | O_NONBLOCK);
|
|
if (s->pass[0]) net_send(s, "PASS %s\r\n", s->pass);
|
|
irc_nick(s, s->nick);
|
|
net_send(s, "USER %s 0 * :%s\r\n", s->user, s->real);
|
|
log_stamp(); fprintf(stderr, CLR_GREEN "Reconnected to %s:%d" CLR_RESET "\n", s->host, s->port);
|
|
return 0;
|
|
}
|
|
log_stamp(); fprintf(stderr, CLR_RED "Giving up after 10 reconnect attempts" CLR_RESET "\n");
|
|
return -1;
|
|
}
|
|
|
|
/* ---- main ---- */
|
|
|
|
/*
|
|
* main - Entry point: parse arguments, initialize, connect, and run the event loop.
|
|
*
|
|
* Parses command-line arguments (server, port, nick, channels, TLS options,
|
|
* opencode path). Validates inputs (IRC-safe strings, valid port, etc.).
|
|
* Initializes the SQLite database. Attempts hot-reload restoration if TLS
|
|
* is enabled; otherwise establishes a fresh connection with TLS handshake.
|
|
* Sets up signal handlers (SIGINT/SIGTERM for graceful shutdown, SIGPIPE
|
|
* ignored). Runs the main event loop using poll() with a 100ms timeout:
|
|
* reads incoming IRC data and feeds it through irc_feed/irc_handle, and
|
|
* checks for completed AI child processes via ai_check_completion(). On
|
|
* shutdown, kills any pending AI process, sends QUIT, cleans up TLS/DB,
|
|
* and exits cleanly.
|
|
*
|
|
* Parameters:
|
|
* argc - Argument count.
|
|
* argv - Argument vector.
|
|
*
|
|
* Returns: 0 on clean shutdown, 1 on error.
|
|
*/
|
|
int main(int argc, char **argv) {
|
|
const char *sv = DEFAULT_SERVER;
|
|
int use_tls = 0;
|
|
int port = DEF_PORT;
|
|
char nickbuf[32];
|
|
const char *nick = NULL;
|
|
char passbuf[256] = "";
|
|
char joinbuf[256] = "#zynk";
|
|
g_opencode_bin = OPENCODE_BIN;
|
|
int pos = 0;
|
|
log_stamp(); fprintf(stderr, CLR_CYAN "[zynk (" VERSION ")] starting up" CLR_RESET "\n");
|
|
for (int i = 1; i < argc; i++) {
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
usage(argv[0]); return 0;
|
|
}
|
|
if (argv[i][0] == '-') {
|
|
if (strcmp(argv[i], "-ssl") == 0 || strcmp(argv[i], "-tls") == 0)
|
|
use_tls = 1;
|
|
else if (strcmp(argv[i], "-pass") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
snprintf(passbuf, sizeof passbuf, "%s", argv[i]);
|
|
} else if (strcmp(argv[i], "-nick") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
nick = argv[i];
|
|
} else if (strcmp(argv[i], "-port") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
port = atoi(argv[i]);
|
|
} else if (strcmp(argv[i], "-server") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
sv = argv[i];
|
|
} else if (strcmp(argv[i], "-join") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
snprintf(joinbuf, sizeof joinbuf, "%s", argv[i]);
|
|
} else if (strcmp(argv[i], "-opencode") == 0) {
|
|
if (++i >= argc) { usage(argv[0]); return 1; }
|
|
g_opencode_bin = argv[i];
|
|
} else if (strcmp(argv[i], "-noverify") == 0) {
|
|
tls_verify = 0;
|
|
} else {
|
|
usage(argv[0]); return 1;
|
|
}
|
|
} else switch (pos++) {
|
|
case 0: sv = argv[i]; break;
|
|
case 1: {
|
|
const char *p = argv[i];
|
|
if (p[0] == '+') { use_tls = 1; p++; }
|
|
port = atoi(p);
|
|
break;
|
|
}
|
|
case 2: nick = argv[i]; break;
|
|
default:
|
|
fprintf(stderr, CLR_RED "Unexpected arg:" CLR_RESET " %s\n", argv[i]);
|
|
return 1;
|
|
}
|
|
}
|
|
if (port <= 0 || port > 65535) port = DEF_PORT;
|
|
if ((port == 6697 || port == 7000) && !use_tls) use_tls = 1;
|
|
if (!nick) { snprintf(nickbuf, sizeof nickbuf, "zynk"); nick = nickbuf; }
|
|
if (!valid_irc_word(nick)) {
|
|
fprintf(stderr, CLR_RED "Invalid nick" CLR_RESET "\n");
|
|
return 1;
|
|
}
|
|
if (has_crlf(passbuf) || has_crlf(joinbuf) || has_ctl(passbuf)) {
|
|
fprintf(stderr, CLR_RED "Invalid IRC argument" CLR_RESET "\n");
|
|
return 1;
|
|
}
|
|
if (!g_opencode_bin || !*g_opencode_bin) {
|
|
fprintf(stderr, CLR_RED "Invalid opencode path" CLR_RESET "\n");
|
|
return 1;
|
|
}
|
|
if (has_ctl(g_opencode_bin)) {
|
|
fprintf(stderr, CLR_RED "Invalid opencode path (control chars)" CLR_RESET "\n");
|
|
return 1;
|
|
}
|
|
if (db_init() < 0) {
|
|
fprintf(stderr, CLR_RED "DB init failed" CLR_RESET "\n");
|
|
sqlite_cleanup();
|
|
return 1;
|
|
}
|
|
start_time = time(NULL);
|
|
Session s;
|
|
memset(&s, 0, sizeof s);
|
|
snprintf(s.host, sizeof s.host, "%s", sv);
|
|
s.port = port;
|
|
snprintf(s.nick, sizeof s.nick, "%s", nick);
|
|
snprintf(s.user, sizeof s.user, "%s", nick);
|
|
snprintf(s.real, sizeof s.real, "zynk");
|
|
snprintf(s.auto_join, sizeof s.auto_join, "%s", joinbuf);
|
|
s.fd = -1;
|
|
s.use_tls = use_tls;
|
|
if (passbuf[0]) snprintf(s.pass, sizeof s.pass, "%s", passbuf);
|
|
signal(SIGINT, handle_sigterm);
|
|
signal(SIGTERM, handle_sigterm);
|
|
signal(SIGPIPE, SIG_IGN);
|
|
if (use_tls) reload_try_restore(&s);
|
|
{
|
|
if (use_tls && tls_init() < 0) {
|
|
fprintf(stderr, CLR_RED "TLS init failed" CLR_RESET "\n");
|
|
db_close_cleanup();
|
|
sqlite_cleanup();
|
|
return 1;
|
|
}
|
|
s.fd = net_connect(s.host, s.port);
|
|
if (s.fd < 0) {
|
|
fprintf(stderr, CLR_RED "Could not connect to %s:%d" CLR_RESET "\n", s.host, s.port);
|
|
tls_ctx_cleanup();
|
|
db_close_cleanup();
|
|
sqlite_cleanup();
|
|
return 1;
|
|
}
|
|
if (s.use_tls && tls_handshake(&s) < 0) {
|
|
fprintf(stderr, CLR_RED "TLS handshake failed" CLR_RESET "\n");
|
|
close(s.fd);
|
|
tls_ctx_cleanup();
|
|
db_close_cleanup();
|
|
sqlite_cleanup();
|
|
return 1;
|
|
}
|
|
int fl = fcntl(s.fd, F_GETFL, 0);
|
|
fcntl(s.fd, F_SETFL, fl | O_NONBLOCK);
|
|
log_stamp(); fprintf(stderr, CLR_GREEN "Connecting to %s:%d as %s..." CLR_RESET "\n", s.host, s.port, s.nick);
|
|
if (s.pass[0]) net_send(&s, "PASS %s\r\n", s.pass);
|
|
irc_nick(&s, s.nick);
|
|
net_send(&s, "USER %s 0 * :%s\r\n", s.user, s.real);
|
|
}
|
|
running = 1;
|
|
pending_ai_pid = 0;
|
|
current_userhost[0] = 0;
|
|
while (running) {
|
|
struct pollfd pfd = { .fd = s.fd, .events = POLLIN };
|
|
int ret = poll(&pfd, 1, POLL_MS);
|
|
if (ret < 0) { if (errno == EINTR) continue; break; }
|
|
if (pfd.revents & (POLLIN | POLLHUP)) {
|
|
char buf[4096];
|
|
int n = net_read(&s, buf, sizeof buf);
|
|
if (n > 0) { s.poll_lines_left = MAX_LINES_PER_POLL; irc_feed(&s, buf, n); }
|
|
else if (n == 0 || (n < 0 && errno != EAGAIN && errno != EINTR)) {
|
|
log_stamp(); fprintf(stderr, CLR_YELLOW "Disconnected" CLR_RESET "\n");
|
|
s.connected = 0;
|
|
if (net_reconnect(&s) < 0) break;
|
|
continue;
|
|
}
|
|
}
|
|
ai_check_completion(&s);
|
|
}
|
|
if (pending_ai_pid > 0) {
|
|
kill(pending_ai_pid, SIGTERM);
|
|
waitpid(pending_ai_pid, NULL, 0);
|
|
ai_reset(pending_ai_id);
|
|
pending_ai_pid = 0;
|
|
}
|
|
if (s.connected && s.fd >= 0) {
|
|
int old_fl = fcntl(s.fd, F_GETFL, 0);
|
|
fcntl(s.fd, F_SETFL, old_fl & ~O_NONBLOCK);
|
|
net_send(&s, "QUIT :zynk\r\n");
|
|
struct timespec ts = { .tv_sec = 0, .tv_nsec = 500000000 };
|
|
nanosleep(&ts, NULL);
|
|
}
|
|
tls_cleanup(&s);
|
|
tls_ctx_cleanup();
|
|
if (s.fd >= 0) close(s.fd);
|
|
db_close_cleanup();
|
|
sqlite_cleanup();
|
|
return 0;
|
|
}
|