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