/*
* sqlchild.c
*
* Copyright (c) 2004,2005 Martin Murray <mmurray@monkey.org>
* All rights reserved.
*
*/
#include "copyright.h"
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dbi/dbi.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>
#include "externs.h"
/* /TODO
* String sanitization.
* - dbi_driver_quote_string_copy ?
*
* Handle Timezones in DATETIME
*
* Handle final write() failing.
*
* Correct handling of TEXT data type.
*/
#define DEBUG_SQL
#ifdef DEBUG_SQL
#ifndef DEBUG
#define DEBUG
#endif
#endif
#include "debug.h"
#ifdef SQL_SUPPORT
#define MAX_QUERIES 8
static int running_queries = 0;
dbi_conn conn = NULL;
#define DBIS_EFAIL -1
#define DBIS_READY 0
#define DBIS_RESOURCE 1
static int dbi_initialized = 0;
static int dbi_state;
static int query_counter = 0;
static int recent = 0;
static struct query_state_t {
dbref thing;
int attr;
char *preserve;
char *query;
struct event ev;
char *rdelim;
char *cdelim;
struct query_state_t *next;
int serial;
struct timeval start;
char slot;
int fd;
int pid;
} *running = NULL, *pending = NULL, *pending_tail = NULL, *recent_head =
NULL, *recent_tail = NULL;
static struct timeval query_timeout = { 10, 0 };
void sqlchild_init();
void sqlchild_destruct();
static void sqlchild_kill_all();
int sqlchild_kill(int requestId);
void sqlchild_list(dbref thing);
static void sqlchild_kill_query(struct query_state_t *aqt);
static void sqlchild_child_abort_query(struct query_state_t *aqt,
char *error);
static void sqlchild_child_abort_query_dbi(struct query_state_t *aqt,
char *error);
static void sqlchild_child_execute_query(struct query_state_t *aqt);
static void sqlchild_finish_query(int fd, short events, void *arg);
static void sqlchild_check_queue();
int sqlchild_request(dbref thing, int attr, char slot, char *pres,
char *query, char *rdelim, char *cdelim);
void sqlchild_init()
{
dbi_driver *driver;
if(dbi_initialized)
return;
dprintk("initializing sqlchild.");
if(dbi_initialize(NULL) == -1) {
dprintk("dbi_initialized() failed.");
dbi_state = DBIS_EFAIL;
return;
}
dprintk("libdbi started.");
driver = dbi_driver_list(NULL);
while (driver != NULL) {
dprintk("libdbi driver '%s' ready.", dbi_driver_get_name(driver));
driver = dbi_driver_list(driver);
}
dbi_state = DBIS_READY;
}
void sqlchild_destruct()
{
dprintk("shutting down.");
dbi_state = DBIS_EFAIL;
sqlchild_kill_all();
dbi_shutdown();
}
/* do_query entrypoint */
int sqlchild_request(dbref thing, int attr, char slot, char *pres,
char *query, char *rdelim, char *cdelim)
{
int fds[2];
struct query_state_t *aqt;
if(dbi_state < DBIS_READY)
return -1;
aqt = malloc(sizeof(struct query_state_t));
aqt->thing = thing;
aqt->attr = attr;
aqt->preserve = strdup(pres);
aqt->query = strdup(query);
aqt->rdelim = strdup(rdelim);
aqt->cdelim = strdup(cdelim);
aqt->serial = query_counter++;
aqt->slot = slot;
if(pending == NULL) {
aqt->next = NULL;
pending = aqt;
pending_tail = aqt;
} else {
pending_tail->next = aqt;
aqt->next = NULL;
pending_tail = aqt;
}
sqlchild_check_queue();
return 1;
}
void sqlchild_kill_all()
{
dprintk("shutting down.\n");
if(!dbi_initialized)
return;
while (running) {
sqlchild_kill_query(running);
}
}
int sqlchild_kill(int requestId)
{
int status;
struct query_state_t *iter = NULL, *aqt = NULL;
dprintk("received request to terminate %d", requestId);
if(running)
iter = running;
while (iter)
if(iter->serial == requestId) {
sqlchild_kill_query(iter);
return 1;
} else
iter = iter->next;
if(pending)
iter = pending;
while (iter)
if(iter->serial == requestId) {
sqlchild_kill_query(iter);
return 1;
} else
iter = iter->next;
return 0;
}
static void sqlchild_kill_query(struct query_state_t *aqt)
{
struct query_state_t *iter;
dprintk("terminating query %d", aqt->serial);
kill(aqt->pid, SIGTERM);
waitpid(aqt->pid, NULL, 0);
iter = running;
if(running == aqt) {
running = aqt->next;
} else {
while (iter) {
if(iter->next == aqt) {
iter->next = aqt->next;
break;
}
iter = iter->next;
}
}
if(aqt->preserve)
free(aqt->preserve);
if(aqt->query)
free(aqt->query);
if(aqt->rdelim)
free(aqt->rdelim);
if(aqt->cdelim)
free(aqt->cdelim);
close(aqt->fd);
free(aqt);
running_queries--;
sqlchild_check_queue();
};
void sqlchild_list(dbref thing)
{
int nactive = 0, npending = 0;
int nrecent = recent;
struct query_state_t *aqt;
notify(thing, "/--------------------------- Recent Queries");
if(recent) {
aqt = recent_head;
while (aqt) {
notify_printf(thing, "%08d #%8d %40s", aqt->serial, aqt->thing,
aqt->query);
aqt = aqt->next;
}
}
notify(thing, "/--------------------------- Running Queries");
if(running) {
aqt = running;
while (aqt) {
notify_printf(thing, "%08d #%8d %40s", aqt->serial, aqt->thing,
aqt->query);
aqt = aqt->next;
nactive++;
}
} else {
notify(thing, "- No active queries.");
}
notify(thing, "/--------------------------- Pending Queries");
if(pending) {
aqt = pending;
while (aqt) {
notify_printf(thing, "%08d #%-8d %40s", aqt->serial, aqt->thing,
aqt->query);
aqt = aqt->next;
npending++;
}
} else {
notify(thing, "- No pending queries.");
}
notify_printf(thing, "%d active and %d pending queries.", nactive,
pending);
}
struct query_response {
int status;
int n_chars;
};
/* HOST FUNCTIONS */
static void sqlchild_finish_query(int fd, short events, void *arg)
{
pid_t pchild;
int status;
char *argv[5];
struct query_state_t *aqt = (struct query_state_t *) arg, *iter;
struct query_response resp = { -1, 0 };
char buffer[LBUF_SIZE];
buffer[0] = '\0';
dprintk("receiving response for query %d", aqt->serial);
if(read(aqt->fd, &resp, sizeof(struct query_response)) < 0) {
log_perror("SQL", "FAIL", NULL, "sqlchild_finish_query");
argv[0] = "-1";
argv[1] = "";
argv[3] = "serious braindamage";
goto fail;
}
if(resp.n_chars >= LBUF_SIZE) {
resp.n_chars = LBUF_SIZE - 1;
}
if(resp.n_chars) {
if(read(aqt->fd, buffer, resp.n_chars) < 0) {
log_perror("SQL", "FAIL", NULL, "sqlchild_finish_query");
argv[0] = "-1";
argv[1] = "";
argv[3] = "serious braindamage";
goto fail;
}
}
if(resp.status == 0) {
argv[0] = "1";
argv[1] = buffer;
argv[3] = "Success";
} else {
argv[0] = "0";
argv[1] = "";
if(resp.n_chars) {
argv[3] = buffer;
} else {
argv[3] = "minor braindamage, no error and no result reported.";
}
}
fail:
argv[2] = aqt->preserve;
did_it(GOD, aqt->thing, 0, NULL, 0, NULL, aqt->attr, argv, 4);
hardfail:
if(running == aqt) {
running = aqt->next;
} else {
iter = running;
while (iter) {
if(iter->next == aqt) {
iter->next = aqt->next;
break;
}
iter = iter->next;
}
}
close(aqt->fd);
dprintk("waiting on %d.", aqt->pid);
pchild = waitpid(aqt->pid, &status, WNOHANG);
if(pchild) {
dprintk("%d exited normally.", aqt->pid);
}
recent++;
if(recent_tail == NULL) {
aqt->next = NULL;
recent_head = recent_tail = aqt;
} else {
recent_tail->next = aqt;
recent_tail = aqt;
}
if(recent > 20) {
aqt = recent_head;
recent_head = aqt->next;
if(aqt->preserve) {
free(aqt->preserve);
aqt->preserve = NULL;
}
if(aqt->query) {
free(aqt->query);
aqt->query = NULL;
}
if(aqt->rdelim) {
free(aqt->rdelim);
aqt->rdelim = NULL;
}
if(aqt->cdelim) {
free(aqt->cdelim);
aqt->cdelim = NULL;
}
free(aqt);
aqt = NULL;
recent--;
}
running_queries--;
sqlchild_check_queue();
return;
}
static void sqlchild_check_queue()
{
int fds[2];
struct query_state_t *aqt;
if(running_queries >= mudconf.sqlDB_max_queries)
return;
if(pending == NULL)
return;
if(dbi_state != DBIS_READY)
return;
aqt = pending;
pending = aqt->next;
if(pending == NULL)
pending_tail = NULL;
if(pipe(fds) < 0) {
log_perror("SQL", "FAIL", NULL, "pipe");
return;
}
if((aqt->pid = fork()) == 0) {
aqt->fd = fds[1];
close(fds[0]);
unbind_signals();
sqlchild_child_execute_query(aqt);
exit(0);
} else {
running_queries++;
aqt->fd = fds[0];
close(fds[1]);
}
dprintk("waiting on sqlchild pid %d executing request %d", aqt->pid,
aqt->serial);
if(running)
aqt->next = running;
running = aqt;
event_set(&aqt->ev, aqt->fd, EV_READ, sqlchild_finish_query, aqt);
event_add(&aqt->ev, &query_timeout);
return;
}
/* CHILD FUNCTIONS */
static void sqlchild_child_abort_query(struct query_state_t *aqt, char *error)
{
struct query_response resp = { DBIS_EFAIL, 0 };
if(error) {
resp.n_chars = strlen(error) + 1;
write(aqt->fd, &resp, sizeof(resp));
write(aqt->fd, error, resp.n_chars);
} else {
write(aqt->fd, &resp, sizeof(resp));
}
close(aqt->fd);
exit(0);
return;
}
static void sqlchild_child_abort_query_dbi(struct query_state_t *aqt,
char *error)
{
char *error_ptr;
if(dbi_conn_error(conn, (const char **) &error_ptr) != -1)
sqlchild_child_abort_query(aqt, error_ptr);
else
sqlchild_child_abort_query(aqt, error);
return;
}
static void sqlchild_make_connection(char db_slot)
{
char *db_type, *db_hostname, *db_username, *db_password, *db_database;
switch (db_slot) {
case 'A':
db_type = mudconf.sqlDB_type_A;
db_hostname = mudconf.sqlDB_hostname_A;
db_username = mudconf.sqlDB_username_A;
db_password = mudconf.sqlDB_password_A;
db_database = mudconf.sqlDB_dbname_A;
break;
case 'B':
db_type = mudconf.sqlDB_type_B;
db_hostname = mudconf.sqlDB_hostname_B;
db_username = mudconf.sqlDB_username_B;
db_password = mudconf.sqlDB_password_B;
db_database = mudconf.sqlDB_dbname_B;
break;
case 'C':
db_type = mudconf.sqlDB_type_C;
db_hostname = mudconf.sqlDB_hostname_C;
db_username = mudconf.sqlDB_username_C;
db_password = mudconf.sqlDB_password_C;
db_database = mudconf.sqlDB_dbname_C;
break;
case 'D':
db_type = mudconf.sqlDB_type_D;
db_hostname = mudconf.sqlDB_hostname_D;
db_username = mudconf.sqlDB_username_D;
db_password = mudconf.sqlDB_password_D;
db_database = mudconf.sqlDB_dbname_D;
break;
case 'E':
db_type = mudconf.sqlDB_type_E;
db_hostname = mudconf.sqlDB_hostname_E;
db_username = mudconf.sqlDB_username_E;
db_password = mudconf.sqlDB_password_E;
db_database = mudconf.sqlDB_dbname_E;
break;
default:
return;
}
conn = dbi_conn_new(db_type);
if(!conn) {
dprintk("dbi_conn_new() failed with db_type %s.", db_type);
dbi_state = DBIS_EFAIL;
return;
}
if(strncmp(db_type, "mysql", 128) == 0
&& strnlen(mudconf.sqlDB_mysql_socket, 128) > 0
&& dbi_conn_set_option(conn, "mysql_unix_socket",
mudconf.sqlDB_mysql_socket)) {
dprintk("failed to set mysql_unix_socket");
dbi_state = DBIS_EFAIL;
return;
}
if(strncmp(db_type, "sqlite", 128) == 0
&& strnlen(mudconf.sqlDB_sqlite_dbdir, 128) > 0
&& dbi_conn_set_option(conn, "sqlite_dbdir",
mudconf.sqlDB_sqlite_dbdir)) {
dprintk("failed to set sqlite dir.");
dbi_state = DBIS_EFAIL;
return;
}
if(db_hostname && dbi_conn_set_option(conn, "host", db_hostname)) {
dprintk("failed to set hostname");
dbi_state = DBIS_EFAIL;
return;
}
if(db_username && dbi_conn_set_option(conn, "username", db_username)) {
dprintk("failed to set username");
dbi_state = DBIS_EFAIL;
return;
}
if(db_password && dbi_conn_set_option(conn, "password", db_password)) {
dprintk("failed to set password");
dbi_state = DBIS_EFAIL;
return;
}
if(db_database && dbi_conn_set_option(conn, "dbname", db_database)) {
dprintk("failed to set database");
dbi_state = DBIS_EFAIL;
return;
}
return;
}
static char *sqlchild_sanitize_string(char *input, int length)
{
char *retval = malloc(length + 1);
int i = 0;
memset(retval, 0, length + 1);
for(i = 0; input[i] && i < length; i++) {
if(isprint(input[i])) {
retval[i] = input[i];
} else {
retval[i] = ' ';
}
}
dprintk("length: %d, i is %d, terimnal character is 0x%02x.", length, i,
input[i]);
free(input);
return retval;
}
static void sqlchild_child_execute_query(struct query_state_t *aqt)
{
struct query_response resp = { DBIS_READY, -1 };
dbi_result result;
int rows, fields, i, ii, retval;
char output_buffer[LBUF_SIZE], *ptr, *eptr, *delim;
int binary_length;
char time_buffer[64];
int length = 0;
struct tm tm;
long long type_int;
double type_fp;
char *type_string;
time_t type_time;
ptr = output_buffer;
eptr = ptr + LBUF_SIZE;
*ptr = '\0';
dprintk("executing query %d.", aqt->serial);
sqlchild_make_connection(aqt->slot);
if(!conn) {
sqlchild_child_abort_query(aqt, "failed to create connection");
return;
}
if(dbi_state != DBIS_READY) {
sqlchild_child_abort_query_dbi(aqt,
"unknown error in sqlchild_make_connection");
return;
}
if(!conn) {
sqlchild_child_abort_query_dbi(aqt,
"unknown error in sqlchild_make_connection");
return;
}
if(dbi_conn_connect(conn) != 0) {
sqlchild_child_abort_query(aqt, "dbi_conn_connect failed");
return;
}
result = dbi_conn_query(conn, aqt->query);
if(result == NULL) {
sqlchild_child_abort_query_dbi(aqt,
"unknown error in dbi_conn_query");
return;
}
rows = dbi_result_get_numrows(result);
fields = dbi_result_get_numfields(result);
delim = NULL;
while (dbi_result_next_row(result)) {
if(delim != NULL) {
ptr += snprintf(ptr, eptr - ptr, aqt->rdelim);
}
for(i = 1; i <= fields; i++) {
if(fields == i)
delim = "";
else
delim = aqt->cdelim;
// XXX: handle error values form snprintf()
switch (dbi_result_get_field_type_idx(result, i)) {
case DBI_TYPE_INTEGER:
type_int = dbi_result_get_longlong_idx(result, i);
ptr += snprintf(ptr, eptr - ptr, "%lld%s", type_int, delim);
break;
case DBI_TYPE_DECIMAL:
type_fp = dbi_result_get_double_idx(result, i);
ptr += snprintf(ptr, eptr - ptr, "%f%s", type_fp, delim);
break;
case DBI_TYPE_STRING:
type_string = dbi_result_get_string_copy_idx(result, i);
ptr += snprintf(ptr, eptr - ptr, "%s%s", type_string, delim);
free(type_string);
break;
case DBI_TYPE_BINARY:
binary_length = dbi_result_get_field_length_idx(result, i);
if(binary_length) {
type_string = (char *)dbi_result_get_binary_copy_idx(result, i);
type_string = sqlchild_sanitize_string(type_string,
dbi_result_get_field_length_idx
(result, i));
ptr +=
snprintf(ptr, eptr - ptr, "%s%s", type_string, delim);
free(type_string);
} else {
ptr += snprintf(ptr, eptr - ptr, "%s", delim);
}
break;
case DBI_TYPE_DATETIME:
type_time = dbi_result_get_datetime_idx(result, i);
localtime_r(&type_time, &tm);
asctime_r(&tm, time_buffer);
ptr += snprintf(ptr, eptr - ptr, "%s%s", time_buffer, delim);
break;
default:
sqlchild_child_abort_query(aqt, "unknown type");
return;
}
if(eptr - ptr < 1) {
sqlchild_child_abort_query(aqt, "result too large");
return;
}
}
}
*ptr++ = '\0';
resp.n_chars = eptr - ptr;
eptr = ptr;
// XXX: handle failure
write(aqt->fd, &resp, sizeof(struct query_response));
ptr = output_buffer;
while (ptr < eptr) {
retval = write(aqt->fd, ptr, eptr - ptr);
ptr += retval;
}
close(aqt->fd);
return;
}
#endif /* SQL_SUPPORT */