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