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