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