Initial import (again)
[srvx.git] / src / saxdb.c
1 /* saxdb.c - srvx database manager
2  * Copyright 2002-2004 srvx Development Team
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 2 of the License, or
7  * (at your option) any later version.  Important limitations are
8  * listed in the COPYING file that accompanies this software.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, email srvx-maintainers@srvx.net.
17  */
18
19 #include "conf.h"
20 #include "hash.h"
21 #include "modcmd.h"
22 #include "saxdb.h"
23 #include "timeq.h"
24
25 DECLARE_LIST(int_list, int);
26 DEFINE_LIST(int_list, int);
27
28 struct saxdb {
29     char *name;
30     char *filename;
31     char *mondo_section;
32     saxdb_reader_func_t *reader;
33     saxdb_writer_func_t *writer;
34     unsigned int write_interval;
35     time_t last_write;
36     unsigned int last_write_duration;
37     struct saxdb *prev;
38 };
39
40 struct saxdb_context {
41     FILE *output;
42     unsigned int indent;
43     struct int_list complex;
44     jmp_buf jbuf;
45     /* XXX: If jbuf is ever used, places that use saxdb_open_context() and
46      * saxdb_close_context() must be modified to fill it in */
47 };
48
49 #define COMPLEX(CTX) ((CTX)->complex.used ? ((CTX)->complex.list[(CTX)->complex.used-1]) : 1)
50
51 static struct saxdb *last_db;
52 static struct dict *saxdbs; /* -> struct saxdb */
53 static struct dict *mondo_db;
54 static struct module *saxdb_module;
55
56 static SAXDB_WRITER(saxdb_mondo_writer);
57 static void saxdb_timed_write(void *data);
58
59 static void
60 saxdb_read_db(struct saxdb *db) {
61     struct dict *data;
62
63     assert(db);
64     assert(db->filename);
65     data = parse_database(db->filename);
66     if (!data) return;
67     if (db->writer == saxdb_mondo_writer) {
68         mondo_db = data;
69     } else {
70         db->reader(data);
71         free_database(data);
72     }
73 }
74
75 struct saxdb *
76 saxdb_register(const char *name, saxdb_reader_func_t *reader, saxdb_writer_func_t *writer) {
77     struct saxdb *db;
78     struct dict *conf;
79     int ii;
80     const char *filename = NULL, *str;
81     char conf_path[MAXLEN];
82
83     db = calloc(1, sizeof(*db));
84     db->name = strdup(name);
85     db->reader = reader;
86     db->writer = writer;
87     /* Look up configuration */
88     sprintf(conf_path, "dbs/%s", name);
89     if ((conf = conf_get_data(conf_path, RECDB_OBJECT))) {
90         if ((str = database_get_data(conf, "mondo_section", RECDB_QSTRING))) {
91             db->mondo_section = strdup(str);
92         }
93         str = database_get_data(conf, "frequency", RECDB_QSTRING);
94         db->write_interval = str ? ParseInterval(str) : 1800;
95         filename = database_get_data(conf, "filename", RECDB_QSTRING);
96     } else {
97         db->write_interval = 1800;
98     }
99     /* Schedule database writes */
100     if (db->write_interval && !db->mondo_section) {
101         timeq_add(now + db->write_interval, saxdb_timed_write, db);
102     }
103     /* Insert filename */
104     if (filename) {
105         db->filename = strdup(filename);
106     } else {
107         db->filename = malloc(strlen(db->name)+4);
108         for (ii=0; db->name[ii]; ++ii) db->filename[ii] = tolower(db->name[ii]);
109         strcpy(db->filename+ii, ".db");
110     }
111     /* Read from disk (or mondo DB) */
112     if (db->mondo_section) {
113         if (mondo_db && (conf = database_get_data(mondo_db, db->mondo_section, RECDB_OBJECT))) {
114             db->reader(conf);
115         }
116     } else {
117         saxdb_read_db(db);
118     }
119     /* Remember the database */
120     dict_insert(saxdbs, db->name, db);
121     db->prev = last_db;
122     last_db = db;
123     return db;
124 }
125
126 static int
127 saxdb_write_db(struct saxdb *db) {
128     struct saxdb_context ctx;
129     char tmp_fname[MAXLEN];
130     int res, res2;
131     time_t start, finish;
132
133     assert(db->filename);
134     sprintf(tmp_fname, "%s.new", db->filename);
135     memset(&ctx, 0, sizeof(ctx));
136     ctx.output = fopen(tmp_fname, "w+");
137     int_list_init(&ctx.complex);
138     if (!ctx.output) {
139         log_module(MAIN_LOG, LOG_ERROR, "Unable to write to %s: %s", tmp_fname, strerror(errno));
140         int_list_clean(&ctx.complex);
141         return 1;
142     }
143     start = time(NULL);
144     if ((res = setjmp(ctx.jbuf)) || (res2 = db->writer(&ctx))) {
145         if (res) {
146             log_module(MAIN_LOG, LOG_ERROR, "Exception %d caught while writing to %s", res, tmp_fname);
147         } else {
148             log_module(MAIN_LOG, LOG_ERROR, "Internal error %d while writing to %s", res2, tmp_fname);
149         }
150         int_list_clean(&ctx.complex);
151         fclose(ctx.output);
152         remove(tmp_fname);
153         return 2;
154     }
155     finish = time(NULL);
156     assert(ctx.complex.used == 0);
157     int_list_clean(&ctx.complex);
158     fclose(ctx.output);
159     if (rename(tmp_fname, db->filename) < 0) {
160         log_module(MAIN_LOG, LOG_ERROR, "Unable to rename %s to %s: %s", tmp_fname, db->filename, strerror(errno));
161     }
162     db->last_write = now;
163     db->last_write_duration = finish - start;
164     log_module(MAIN_LOG, LOG_INFO, "Wrote %s database to disk.", db->name);
165     return 0;
166 }
167
168 static void
169 saxdb_timed_write(void *data) {
170     struct saxdb *db = data;
171     saxdb_write_db(db);
172     timeq_add(now + db->write_interval, saxdb_timed_write, db);
173 }
174
175 void
176 saxdb_write(const char *db_name) {
177     struct saxdb *db;
178     db = dict_find(saxdbs, db_name, NULL);
179     if (db) saxdb_write_db(db);
180 }
181
182 void
183 saxdb_write_all(void) {
184     dict_iterator_t it;
185     struct saxdb *db;
186
187     for (it = dict_first(saxdbs); it; it = iter_next(it)) {
188         db = iter_data(it);
189         if (!db->mondo_section) saxdb_write_db(db);
190     }
191 }
192
193 #define saxdb_put_char(DEST, CH)   fputc(CH, (DEST)->output)
194 #define saxdb_put_string(DEST, CH) fputs(CH, (DEST)->output)
195
196 static inline void
197 saxdb_put_nchars(struct saxdb_context *dest, const char *name, int len) {
198     while (len--) {
199         fputc(*name++, dest->output);
200     }
201 }
202
203 static void
204 saxdb_put_qstring(struct saxdb_context *dest, const char *str) {
205     const char *esc;
206
207     assert(str);
208     saxdb_put_char(dest, '"');
209     while ((esc = strpbrk(str, "\\\a\b\t\n\v\f\r\""))) {
210         if (esc != str) saxdb_put_nchars(dest, str, esc-str);
211         saxdb_put_char(dest, '\\');
212         switch (*esc) {
213         case '\a': saxdb_put_char(dest, 'a'); break;
214         case '\b': saxdb_put_char(dest, 'b'); break;
215         case '\t': saxdb_put_char(dest, 't'); break;
216         case '\n': saxdb_put_char(dest, 'n'); break;
217         case '\v': saxdb_put_char(dest, 'v'); break;
218         case '\f': saxdb_put_char(dest, 'f'); break;
219         case '\r': saxdb_put_char(dest, 'r'); break;
220         case '\\': saxdb_put_char(dest, '\\'); break;
221         case '"': saxdb_put_char(dest, '"'); break;
222         }
223         str = esc + 1;
224     }
225     saxdb_put_string(dest, str);
226     saxdb_put_char(dest, '"');
227 }
228
229 #ifndef NDEBUG
230 static void
231 saxdb_pre_object(struct saxdb_context *dest) {
232     unsigned int ii;
233     if (COMPLEX(dest)) {
234         for (ii=0; ii<dest->indent; ++ii) saxdb_put_char(dest, '\t');
235     }
236 }
237 #else
238 #define saxdb_pre_object(DEST) 
239 #endif
240
241 static inline void
242 saxdb_post_object(struct saxdb_context *dest) {
243     saxdb_put_char(dest, ';');
244     saxdb_put_char(dest, COMPLEX(dest) ? '\n' : ' ');
245 }
246
247 void
248 saxdb_start_record(struct saxdb_context *dest, const char *name, int complex) {
249     saxdb_pre_object(dest);
250     saxdb_put_qstring(dest, name);
251     saxdb_put_string(dest, " { ");
252     int_list_append(&dest->complex, complex);
253     if (complex) {
254         dest->indent++;
255         saxdb_put_char(dest, '\n');
256     }
257 }
258
259 void
260 saxdb_end_record(struct saxdb_context *dest) {
261     assert(dest->complex.used > 0);
262     if (COMPLEX(dest)) dest->indent--;
263     saxdb_pre_object(dest);
264     dest->complex.used--;
265     saxdb_put_char(dest, '}');
266     saxdb_post_object(dest);
267 }
268
269 void
270 saxdb_write_string_list(struct saxdb_context *dest, const char *name, struct string_list *list) {
271     unsigned int ii;
272
273     saxdb_pre_object(dest);
274     saxdb_put_qstring(dest, name);
275     saxdb_put_string(dest, " (");
276     if (list->used) {
277         for (ii=0; ii<list->used-1; ++ii) {
278             saxdb_put_qstring(dest, list->list[ii]);
279             saxdb_put_string(dest, ", ");
280         }
281         saxdb_put_qstring(dest, list->list[list->used-1]);
282     }
283     saxdb_put_string(dest, ")");
284     saxdb_post_object(dest);
285 }
286
287 void
288 saxdb_write_string(struct saxdb_context *dest, const char *name, const char *value) {
289     saxdb_pre_object(dest);
290     saxdb_put_qstring(dest, name);
291     saxdb_put_char(dest, ' ');
292     saxdb_put_qstring(dest, value);
293     saxdb_post_object(dest);
294 }
295
296 void
297 saxdb_write_int(struct saxdb_context *dest, const char *name, unsigned long value) {
298     unsigned char buf[16];
299     /* we could optimize this to take advantage of the fact that buf will never need escapes */
300     snprintf(buf, sizeof(buf), "%lu", value);
301     saxdb_write_string(dest, name, buf);
302 }
303
304 static void
305 saxdb_free(void *data) {
306     struct saxdb *db = data;
307     free(db->name);
308     free(db->filename);
309     free(db->mondo_section);
310     free(db);
311 }
312
313 static int
314 saxdb_mondo_read(struct dict *db, struct saxdb *saxdb) {
315     int res;
316     struct dict *subdb;
317
318     if (saxdb->prev && (res = saxdb_mondo_read(db, saxdb->prev))) return res;
319     if (saxdb->mondo_section
320         && (subdb = database_get_data(db, saxdb->mondo_section, RECDB_OBJECT))
321         && (res = saxdb->reader(subdb))) {
322         log_module(MAIN_LOG, LOG_INFO, " mondo section read for %s failed: %d", saxdb->mondo_section, res);
323         return res;
324     }
325     return 0;
326 }
327
328 static SAXDB_READER(saxdb_mondo_reader) {
329     return saxdb_mondo_read(db, last_db);
330 }
331
332 static int
333 saxdb_mondo_write(struct saxdb_context *ctx, struct saxdb *saxdb) {
334     int res;
335     if (saxdb->prev && (res = saxdb_mondo_write(ctx, saxdb->prev))) return res;
336     if (saxdb->mondo_section) {
337         saxdb_start_record(ctx, saxdb->mondo_section, 1);
338         if ((res = saxdb->writer(ctx))) {
339             log_module(MAIN_LOG, LOG_INFO, " mondo section write for %s failed: %d", saxdb->mondo_section, res);
340             return res;
341         }
342         saxdb_end_record(ctx);
343         /* cheat a little here to put a newline between mondo sections */
344         saxdb_put_char(ctx, '\n');
345     }
346     return 0;
347 }
348
349 static SAXDB_WRITER(saxdb_mondo_writer) {
350     return saxdb_mondo_write(ctx, last_db);
351 }
352
353 static MODCMD_FUNC(cmd_write) {
354     struct timeval start, stop;
355     unsigned int ii, written;
356     struct saxdb *db;
357
358     assert(argc >= 2);
359     written = 0;
360     for (ii=1; ii<argc; ++ii) {
361         if (!(db = dict_find(saxdbs, argv[ii], NULL))) {
362             reply("MSG_DB_UNKNOWN", argv[ii]);
363             continue;
364         }
365         if (db->mondo_section) {
366             reply("MSG_DB_IS_MONDO", db->name);
367             continue;
368         }
369         gettimeofday(&start, NULL);
370         if (saxdb_write_db(db)) {
371             reply("MSG_DB_WRITE_ERROR", db->name);
372         } else {
373             gettimeofday(&stop, NULL);
374             stop.tv_sec -= start.tv_sec;
375             stop.tv_usec -= start.tv_usec;
376             if (stop.tv_usec < 0) {
377                 stop.tv_sec -= 1;
378                 stop.tv_usec += 1000000;
379             }
380             reply("MSG_DB_WROTE_DB", db->name, stop.tv_sec, stop.tv_usec);
381             written++;
382         }
383     }
384     return written;
385 }
386
387 static MODCMD_FUNC(cmd_writeall) {
388     struct timeval start, stop;
389
390     gettimeofday(&start, NULL);
391     saxdb_write_all();
392     gettimeofday(&stop, NULL);
393     stop.tv_sec -= start.tv_sec;
394     stop.tv_usec -= start.tv_usec;
395     if (stop.tv_usec < 0) {
396         stop.tv_sec -= 1;
397         stop.tv_usec += 1000000;
398     }
399     reply("MSG_DB_WROTE_ALL", stop.tv_sec, stop.tv_usec);
400     return 1;
401 }
402
403 static MODCMD_FUNC(cmd_stats_databases) {
404     struct helpfile_table tbl;
405     dict_iterator_t it;
406     unsigned int ii;
407
408     tbl.length = dict_size(saxdbs) + 1;
409     tbl.width = 5;
410     tbl.flags = TABLE_NO_FREE;
411     tbl.contents = calloc(tbl.length, sizeof(tbl.contents[0]));
412     tbl.contents[0] = calloc(tbl.width, sizeof(tbl.contents[0][0]));
413     tbl.contents[0][0] = "Database";
414     tbl.contents[0][1] = "Filename/Section";
415     tbl.contents[0][2] = "Interval";
416     tbl.contents[0][3] = "Last Written";
417     tbl.contents[0][4] = "Last Duration";
418     for (ii=1, it=dict_first(saxdbs); it; it=iter_next(it), ++ii) {
419         struct saxdb *db = iter_data(it);
420         char *buf = malloc(INTERVALLEN*3);
421         tbl.contents[ii] = calloc(tbl.width, sizeof(tbl.contents[ii][0]));
422         tbl.contents[ii][0] = db->name;
423         tbl.contents[ii][1] = db->mondo_section ? db->mondo_section : db->filename;
424         if (db->write_interval) {
425             intervalString(buf, db->write_interval);
426         } else {
427             strcpy(buf, "Never");
428         }
429         tbl.contents[ii][2] = buf;
430         if (db->last_write) {
431             intervalString(buf+INTERVALLEN, now - db->last_write);
432             intervalString(buf+INTERVALLEN*2, db->last_write_duration);
433         } else {
434             strcpy(buf+INTERVALLEN, "Never");
435             strcpy(buf+INTERVALLEN*2, "Never");
436         }
437         tbl.contents[ii][3] = buf+INTERVALLEN;
438         tbl.contents[ii][4] = buf+INTERVALLEN*2;
439     }
440     table_send(cmd->parent->bot, user->nick, 0, 0, tbl);
441     free(tbl.contents[0]);
442     for (ii=1; ii<tbl.length; ++ii) {
443         free((char*)tbl.contents[ii][2]);
444         free(tbl.contents[ii]);
445     }
446     free(tbl.contents);
447     return 0;
448 }
449
450 static void
451 saxdb_cleanup(void) {
452     dict_delete(saxdbs);
453 }
454
455 static struct helpfile_expansion
456 saxdb_expand_help(const char *variable) {
457     struct helpfile_expansion exp;
458     if (!strcasecmp(variable, "dblist")) {
459         dict_iterator_t it;
460         struct string_buffer sbuf;
461         struct saxdb *db;
462
463         exp.type = HF_STRING;
464         string_buffer_init(&sbuf);
465         for (it = dict_first(saxdbs); it; it = iter_next(it)) {
466             db = iter_data(it);
467             if (db->mondo_section) continue;
468             if (sbuf.used) string_buffer_append_string(&sbuf, ", ");
469             string_buffer_append_string(&sbuf, iter_key(it));
470         }
471         exp.value.str = sbuf.list;
472     } else {
473         exp.type = HF_STRING;
474         exp.value.str = NULL;
475     }
476     return exp;
477 }
478
479 void
480 saxdb_init(void) {
481     reg_exit_func(saxdb_cleanup);
482     saxdbs = dict_new();
483     dict_set_free_data(saxdbs, saxdb_free);
484     saxdb_register("mondo", saxdb_mondo_reader, saxdb_mondo_writer);
485     saxdb_module = module_register("saxdb", MAIN_LOG, "saxdb.help", saxdb_expand_help);
486     modcmd_register(saxdb_module, "write", cmd_write, 2, MODCMD_REQUIRE_AUTHED, "level", "800", NULL);
487     modcmd_register(saxdb_module, "writeall", cmd_writeall, 0, MODCMD_REQUIRE_AUTHED, "level", "800", NULL);
488     modcmd_register(saxdb_module, "stats databases", cmd_stats_databases, 0, 0, NULL);
489 }
490
491 void
492 saxdb_finalize(void) {
493     free_database(mondo_db);
494 }
495
496 static void
497 write_database_helper(struct saxdb_context *ctx, struct dict *db) {
498     dict_iterator_t it;
499     struct record_data *rd;
500
501     for (it = dict_first(db); it; it = iter_next(it)) {
502         rd = iter_data(it);
503         switch (rd->type) {
504         case RECDB_INVALID: break;
505         case RECDB_QSTRING: saxdb_write_string(ctx, iter_key(it), rd->d.qstring); break;
506         case RECDB_STRING_LIST: saxdb_write_string_list(ctx, iter_key(it), rd->d.slist); break;
507         case RECDB_OBJECT:
508             saxdb_start_record(ctx, iter_key(it), 1);
509             write_database_helper(ctx, rd->d.object);
510             saxdb_end_record(ctx);
511             break;
512         }
513     }
514 }
515
516 int
517 write_database(FILE *out, struct dict *db) {
518     struct saxdb_context ctx;
519     int res;
520
521     ctx.output = out;
522     ctx.indent = 0;
523     int_list_init(&ctx.complex);
524     if (!(res = setjmp(ctx.jbuf))) {
525         write_database_helper(&ctx, db);
526     } else {
527         log_module(MAIN_LOG, LOG_ERROR, "Exception %d caught while writing to stream", res);
528         int_list_clean(&ctx.complex);
529         return 1;
530     }
531     assert(ctx.complex.used == 0);
532     int_list_clean(&ctx.complex);
533     return 0;
534 }
535
536 struct saxdb_context *
537 saxdb_open_context(FILE *file) {
538     struct saxdb_context *ctx;
539
540     assert(file);
541     ctx = calloc(1, sizeof(*ctx));
542     ctx->output = file;
543     int_list_init(&ctx->complex);
544
545     return ctx;
546 }
547
548 void
549 saxdb_close_context(struct saxdb_context *ctx) {
550     assert(ctx->complex.used == 0);
551     int_list_clean(&ctx->complex);
552     free(ctx);
553 }