mirror of
https://github.com/pissnet/pissircd.git
synced 2025-08-09 03:35:24 +01:00
Add support for persistent channel history, if the channel is +P and +H.
This is not enabled by default and requires additional configuration, documentation will follow later.
This commit is contained in:
parent
95cfafcd51
commit
3bf0c9e653
1 changed files with 658 additions and 6 deletions
|
@ -1,5 +1,5 @@
|
|||
/* src/modules/history_backend_mem.c - History Backend: memory
|
||||
* (C) Copyright 2019 Bram Matthys (Syzop) and the UnrealIRCd team
|
||||
* (C) Copyright 2019-2021 Bram Matthys (Syzop) and the UnrealIRCd team
|
||||
* License: GPLv2
|
||||
*/
|
||||
#include "unrealircd.h"
|
||||
|
@ -34,13 +34,38 @@ ModuleHeader MOD_HEADER
|
|||
* HISTORY_MAX_OFF_SECS: how many seconds may the history be 'off',
|
||||
* that is: how much may we store the history longer than required.
|
||||
* The other 2 macros are calculated based on that target.
|
||||
*
|
||||
* Update April 2021: these values are now also used for saving the
|
||||
* history if the persistent option is enabled. Therefore changed the
|
||||
* values to spread it even more out: from 16/128 to 60/300 so
|
||||
* in case of persistent it will save every 5 minutes.
|
||||
*/
|
||||
#define HISTORY_SPREAD 16
|
||||
#define HISTORY_MAX_OFF_SECS 128
|
||||
#ifdef DEBUGMODE
|
||||
#define HISTORY_CLEAN_PER_LOOP HISTORY_BACKEND_MEM_HASH_TABLE_SIZE
|
||||
#define HISTORY_TIMER_EVERY 5
|
||||
#else
|
||||
#define HISTORY_SPREAD 60
|
||||
#define HISTORY_MAX_OFF_SECS 300
|
||||
#define HISTORY_CLEAN_PER_LOOP (HISTORY_BACKEND_MEM_HASH_TABLE_SIZE/HISTORY_SPREAD)
|
||||
#define HISTORY_TIMER_EVERY (HISTORY_MAX_OFF_SECS/HISTORY_SPREAD)
|
||||
#endif
|
||||
|
||||
/* Some magic numbers used in the database format */
|
||||
#define HISTORYDB_MAGIC_FILE_START 0xFEFEFEFE
|
||||
#define HISTORYDB_MAGIC_FILE_END 0xEFEFEFEF
|
||||
#define HISTORYDB_MAGIC_ENTRY_START 0xFFFFFFFF
|
||||
#define HISTORYDB_MAGIC_ENTRY_END 0xEEEEEEEE
|
||||
|
||||
/* Definitions (structs, etc.) -- all for persistent history */
|
||||
struct cfgstruct {
|
||||
int persist;
|
||||
char *directory;
|
||||
char *masterdb; /* Autogenerated for convenience, not a real config item */
|
||||
char *db_secret;
|
||||
char *prehash;
|
||||
char *posthash;
|
||||
};
|
||||
|
||||
/* Definitions (structs, etc.) */
|
||||
typedef struct HistoryLogObject HistoryLogObject;
|
||||
struct HistoryLogObject {
|
||||
HistoryLogObject *prev, *next;
|
||||
|
@ -50,20 +75,47 @@ struct HistoryLogObject {
|
|||
time_t oldest_t; /**< Oldest time in log */
|
||||
int max_lines; /**< Maximum number of lines permitted */
|
||||
long max_time; /**< Maximum number of seconds to retain history */
|
||||
int dirty; /**< Dirty flag, used for disk writing */
|
||||
char name[OBJECTLEN+1];
|
||||
};
|
||||
|
||||
/* Global variables */
|
||||
struct cfgstruct cfg;
|
||||
struct cfgstruct test;
|
||||
static char siphashkey_history_backend_mem[SIPHASH_KEY_LENGTH];
|
||||
HistoryLogObject *history_hash_table[HISTORY_BACKEND_MEM_HASH_TABLE_SIZE];
|
||||
static int already_loaded = 0;
|
||||
|
||||
/* Forward declarations */
|
||||
int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs);
|
||||
int hbm_config_posttest(int *errs);
|
||||
int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type);
|
||||
static void setcfg(struct cfgstruct *cfg);
|
||||
static void freecfg(struct cfgstruct *cfg);
|
||||
int hbm_history_add(char *object, MessageTag *mtags, char *line);
|
||||
int hbm_history_cleanup(HistoryLogObject *h);
|
||||
HistoryResult *hbm_history_request(char *object, HistoryFilter *filter);
|
||||
int hbm_history_destroy(char *object);
|
||||
int hbm_history_set_limit(char *object, int max_lines, long max_time);
|
||||
EVENT(history_mem_clean);
|
||||
EVENT(history_mem_init);
|
||||
static int hbm_read_masterdb(void);
|
||||
static void hbm_read_dbs(void);
|
||||
static int hbm_read_db(char *fname);
|
||||
static int hbm_write_masterdb(void);
|
||||
static int hbm_write_db(HistoryLogObject *h);
|
||||
static void hbm_delete_db(HistoryLogObject *h);
|
||||
|
||||
MOD_TEST()
|
||||
{
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
memset(&test, 0, sizeof(test));
|
||||
setcfg(&test);
|
||||
HookAdd(modinfo->handle, HOOKTYPE_CONFIGTEST, 0, hbm_config_test);
|
||||
HookAdd(modinfo->handle, HOOKTYPE_CONFIGPOSTTEST, 0, hbm_config_posttest);
|
||||
|
||||
return MOD_SUCCESS;
|
||||
}
|
||||
|
||||
MOD_INIT()
|
||||
{
|
||||
|
@ -72,6 +124,10 @@ MOD_INIT()
|
|||
MARK_AS_OFFICIAL_MODULE(modinfo);
|
||||
ModuleSetOptions(modinfo->handle, MOD_OPT_PERM, 1);
|
||||
|
||||
setcfg(&cfg);
|
||||
|
||||
HookAdd(modinfo->handle, HOOKTYPE_CONFIGRUN, 0, hbm_config_run);
|
||||
|
||||
memset(&history_hash_table, 0, sizeof(history_hash_table));
|
||||
siphash_generate_key(siphashkey_history_backend_mem);
|
||||
|
||||
|
@ -89,15 +145,193 @@ MOD_INIT()
|
|||
|
||||
MOD_LOAD()
|
||||
{
|
||||
EventAdd(modinfo->handle, "history_mem_init", history_mem_init, NULL, 1, 1);
|
||||
EventAdd(modinfo->handle, "history_mem_clean", history_mem_clean, NULL, HISTORY_TIMER_EVERY*1000, 0);
|
||||
return MOD_SUCCESS;
|
||||
}
|
||||
|
||||
/* Read the .db if 'persist' mode is enabled.
|
||||
* Normally this would be in MOD_LOAD, but the load order always
|
||||
* must be: channeldb first, this module second, and since we
|
||||
* cannot influence the load order we do this silly trick
|
||||
* with a one-time 1msec event.
|
||||
*/
|
||||
EVENT(history_mem_init)
|
||||
{
|
||||
if (!already_loaded)
|
||||
{
|
||||
/* Initial boot / load of the module... */
|
||||
already_loaded = 1;
|
||||
if (cfg.persist)
|
||||
hbm_read_dbs();
|
||||
}
|
||||
}
|
||||
|
||||
MOD_UNLOAD()
|
||||
{
|
||||
freecfg(&test);
|
||||
freecfg(&cfg);
|
||||
return MOD_SUCCESS;
|
||||
}
|
||||
|
||||
/** Set cfg->masterdb based on cfg->directory, for convenience */
|
||||
static void hbm_set_masterdb_filename(struct cfgstruct *cfg)
|
||||
{
|
||||
char buf[512];
|
||||
|
||||
safe_free(cfg->masterdb);
|
||||
if (cfg->directory)
|
||||
{
|
||||
snprintf(buf, sizeof(buf), "%s/master.db", cfg->directory);
|
||||
safe_strdup(cfg->masterdb, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/** Default configuration for set::history::channel */
|
||||
static void setcfg(struct cfgstruct *cfg)
|
||||
{
|
||||
safe_strdup(cfg->directory, "history");
|
||||
convert_to_absolute_path(&cfg->directory, PERMDATADIR);
|
||||
hbm_set_masterdb_filename(cfg);
|
||||
}
|
||||
|
||||
static void freecfg(struct cfgstruct *cfg)
|
||||
{
|
||||
safe_free(cfg->directory);
|
||||
safe_free(cfg->db_secret);
|
||||
safe_free(cfg->prehash);
|
||||
safe_free(cfg->posthash);
|
||||
}
|
||||
|
||||
|
||||
/** Test the set::history::channel configuration */
|
||||
int hbm_config_test(ConfigFile *cf, ConfigEntry *ce, int type, int *errs)
|
||||
{
|
||||
int errors = 0;
|
||||
|
||||
if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->ce_varname)
|
||||
return 0;
|
||||
|
||||
if (!strcmp(ce->ce_varname, "persist"))
|
||||
{
|
||||
if (!ce->ce_vardata)
|
||||
{
|
||||
config_error("%s:%i: missing parameter",
|
||||
ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
|
||||
errors++;
|
||||
} else {
|
||||
test.persist = config_checkval(ce->ce_vardata, CFG_YESNO);
|
||||
}
|
||||
} else
|
||||
if (!strcmp(ce->ce_varname, "db-secret"))
|
||||
{
|
||||
char *err;
|
||||
if ((err = unrealdb_test_secret(ce->ce_vardata)))
|
||||
{
|
||||
config_error("%s:%i: set::history::channel::db-secret: %s", ce->ce_fileptr->cf_filename, ce->ce_varlinenum, err);
|
||||
errors++;
|
||||
}
|
||||
safe_strdup(test.db_secret, ce->ce_vardata);
|
||||
} else
|
||||
if (!strcmp(ce->ce_varname, "directory")) // or "path" ?
|
||||
{
|
||||
if (!ce->ce_vardata)
|
||||
{
|
||||
config_error("%s:%i: missing parameter",
|
||||
ce->ce_fileptr->cf_filename, ce->ce_varlinenum);
|
||||
errors++;
|
||||
} else
|
||||
{
|
||||
safe_strdup(test.directory, ce->ce_vardata);
|
||||
hbm_set_masterdb_filename(&test);
|
||||
}
|
||||
} else
|
||||
{
|
||||
return 0; /* unknown option to us, let another module handle it */
|
||||
}
|
||||
|
||||
*errs = errors;
|
||||
return errors ? -1 : 1;
|
||||
}
|
||||
|
||||
/** Post-configuration test on set::history::channel */
|
||||
int hbm_config_posttest(int *errs)
|
||||
{
|
||||
int errors = 0;
|
||||
|
||||
if (test.db_secret && !test.persist)
|
||||
{
|
||||
config_error("set::history::channel::db-secret is set but set::history::channel::persist is disabled, this makes no sense. "
|
||||
"Either use 'persist yes' or comment out / delete 'db-secret'.");
|
||||
errors++;
|
||||
} else
|
||||
if (!test.db_secret && test.persist)
|
||||
{
|
||||
config_error("set::history::channel::db-secret needs to be set."); // TODO: REFER TO FAQ OR OTHER ENTRY!!!!
|
||||
errors++;
|
||||
} else
|
||||
if (test.db_secret && test.persist)
|
||||
{
|
||||
/* Configuration is good, now check if the password is correct
|
||||
* (if we can check at all, that is)...
|
||||
*/
|
||||
char *errstr = NULL;
|
||||
if (test.masterdb && ((errstr = unrealdb_test_db(test.masterdb, test.db_secret))))
|
||||
{
|
||||
config_error("[history] %s", errstr);
|
||||
errors++;
|
||||
}
|
||||
|
||||
/* Ensure directory exists and is writable */
|
||||
#ifdef _WIN32
|
||||
mkdir(test.directory); /* (errors ignored) */
|
||||
#else
|
||||
mkdir(test.directory, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
|
||||
#endif
|
||||
if (!file_exists(test.directory))
|
||||
{
|
||||
config_error("[history] Directory %s does not exist and could not be created",
|
||||
test.directory);
|
||||
errors++;
|
||||
} else
|
||||
{
|
||||
/* Only do this if directory actually exists, hence in the 'else' block */
|
||||
if (!hbm_read_masterdb())
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
*errs = errors;
|
||||
return errors ? -1 : 1;
|
||||
}
|
||||
|
||||
/** Configure ourselves based on the set::history::channel settings */
|
||||
int hbm_config_run(ConfigFile *cf, ConfigEntry *ce, int type)
|
||||
{
|
||||
if ((type != CONFIG_SET_HISTORY_CHANNEL) || !ce || !ce->ce_varname)
|
||||
return 0;
|
||||
|
||||
if (!strcmp(ce->ce_varname, "persist"))
|
||||
{
|
||||
cfg.persist = config_checkval(ce->ce_vardata, CFG_YESNO);
|
||||
} else
|
||||
if (!strcmp(ce->ce_varname, "directory")) // or "path" ?
|
||||
{
|
||||
safe_strdup(cfg.directory, ce->ce_vardata);
|
||||
convert_to_absolute_path(&cfg.directory, PERMDATADIR);
|
||||
hbm_set_masterdb_filename(&cfg);
|
||||
} else
|
||||
if (!strcmp(ce->ce_varname, "db-secret"))
|
||||
{
|
||||
safe_strdup(cfg.db_secret, ce->ce_vardata);
|
||||
} else
|
||||
{
|
||||
return 0; /* unknown option to us, let another module handle it */
|
||||
}
|
||||
|
||||
return 1; /* handled by us */
|
||||
}
|
||||
|
||||
uint64_t hbm_hash(char *object)
|
||||
{
|
||||
return siphash_nocase(object, siphashkey_history_backend_mem) % HISTORY_BACKEND_MEM_HASH_TABLE_SIZE;
|
||||
|
@ -135,8 +369,12 @@ HistoryLogObject *hbm_find_or_add_object(char *object)
|
|||
|
||||
void hbm_delete_object_hlo(HistoryLogObject *h)
|
||||
{
|
||||
int hashv = hbm_hash(h->name);
|
||||
int hashv;
|
||||
|
||||
if (cfg.persist)
|
||||
hbm_delete_db(h);
|
||||
|
||||
hashv = hbm_hash(h->name);
|
||||
DelListItem(h, history_hash_table[hashv]);
|
||||
safe_free(h);
|
||||
}
|
||||
|
@ -199,6 +437,7 @@ void hbm_history_add_line(HistoryLogObject *h, MessageTag *mtags, char *line)
|
|||
/* no tail, no head */
|
||||
h->head = h->tail = l;
|
||||
}
|
||||
h->dirty = 1;
|
||||
h->num_lines++;
|
||||
if ((l->t < h->oldest_t) || (h->oldest_t == 0))
|
||||
h->oldest_t = l->t;
|
||||
|
@ -225,6 +464,7 @@ void hbm_history_del_line(HistoryLogObject *h, HistoryLogLine *l)
|
|||
free_message_tags(l->mtags);
|
||||
safe_free(l);
|
||||
|
||||
h->dirty = 1;
|
||||
h->num_lines--;
|
||||
|
||||
/* IMPORTANT: updating h->oldest_t takes place at the caller
|
||||
|
@ -402,6 +642,298 @@ int hbm_history_set_limit(char *object, int max_lines, long max_time)
|
|||
return 1;
|
||||
}
|
||||
|
||||
/** Read the master.db file, this is done at the INIT stage so we can still
|
||||
* reject the configuration / boot attempt.
|
||||
*/
|
||||
static int hbm_read_masterdb(void)
|
||||
{
|
||||
UnrealDB *db;
|
||||
uint32_t mdb_version;
|
||||
|
||||
db = unrealdb_open(test.masterdb, UNREALDB_MODE_READ, test.db_secret);
|
||||
|
||||
if (!db)
|
||||
{
|
||||
if (unrealdb_get_error_code() == UNREALDB_ERROR_FILENOTFOUND)
|
||||
{
|
||||
/* Database does not exist. Could be first boot */
|
||||
config_warn("[history] No database present at '%s', will start a new one", cfg.masterdb);
|
||||
// TODO: maybe check for condition where 'master.db' does not exist but
|
||||
// there are other .db files.
|
||||
if (!hbm_write_masterdb())
|
||||
return 0; /* fatal error */
|
||||
return 1;
|
||||
} else
|
||||
{
|
||||
config_warn("[history] Unable to open the database file '%s' for reading: %s", cfg.masterdb, unrealdb_get_error_string());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
safe_free(cfg.prehash);
|
||||
safe_free(cfg.posthash);
|
||||
|
||||
/* Master db has an easy format:
|
||||
* 64 bits: version number
|
||||
* string: pre hash
|
||||
* string: post hash
|
||||
*/
|
||||
if (!unrealdb_read_int32(db, &mdb_version) ||
|
||||
!unrealdb_read_str(db, &cfg.prehash) ||
|
||||
!unrealdb_read_str(db, &cfg.posthash))
|
||||
{
|
||||
config_error("[history] Read error from database file '%s': %s",
|
||||
cfg.masterdb, unrealdb_get_error_string());
|
||||
unrealdb_close(db);
|
||||
return 0;
|
||||
}
|
||||
unrealdb_close(db);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Write the master.db file. Only call this if it does not exist yet! */
|
||||
static int hbm_write_masterdb(void)
|
||||
{
|
||||
UnrealDB *db;
|
||||
uint32_t mdb_version;
|
||||
char buf[512];
|
||||
|
||||
db = unrealdb_open(test.masterdb, UNREALDB_MODE_WRITE, test.db_secret);
|
||||
if (!db)
|
||||
{
|
||||
config_error("[history] Unable to write to '%s': %s",
|
||||
test.masterdb, unrealdb_get_error_string());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!cfg.prehash)
|
||||
{
|
||||
gen_random_alnum(buf, 128);
|
||||
safe_strdup(cfg.prehash, buf);
|
||||
}
|
||||
if (!cfg.posthash)
|
||||
{
|
||||
gen_random_alnum(buf, 128);
|
||||
safe_strdup(cfg.posthash, buf);
|
||||
}
|
||||
|
||||
mdb_version = 5000;
|
||||
if (!unrealdb_write_int32(db, mdb_version) ||
|
||||
!unrealdb_write_str(db, cfg.prehash) ||
|
||||
!unrealdb_write_str(db, cfg.posthash))
|
||||
{
|
||||
config_error("[history] Unable to write to '%s': %s",
|
||||
test.masterdb, unrealdb_get_error_string());
|
||||
return 0;
|
||||
}
|
||||
unrealdb_close(db);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Read all database files (except master.db, which is already loaded) */
|
||||
static void hbm_read_dbs(void)
|
||||
{
|
||||
char buf[512];
|
||||
#ifndef _WIN32
|
||||
struct dirent *dir;
|
||||
DIR *fd = opendir(cfg.directory);
|
||||
|
||||
if (!fd)
|
||||
return;
|
||||
|
||||
while ((dir = readdir(fd)))
|
||||
{
|
||||
char *fname = dir->d_name;
|
||||
#else
|
||||
/* Windows */
|
||||
WIN32_FIND_DATA hData;
|
||||
HANDLE hFile;
|
||||
char xbuf[512];
|
||||
|
||||
snprintf(xbuf, sizeof(xbuf), "%s/*.db");
|
||||
|
||||
hFile = FindFirstFile(xbuf, &hData);
|
||||
if (hFile == INVALID_HANDLE_VALUE)
|
||||
return;
|
||||
|
||||
do
|
||||
{
|
||||
char *fname = hData.cFileName;
|
||||
#endif
|
||||
|
||||
/* Common section for both *NIX and Windows */
|
||||
|
||||
snprintf(buf, sizeof(buf), "%s/%s", cfg.directory, fname);
|
||||
if (filename_has_suffix(fname, ".db") && strcmp(fname, "master.db"))
|
||||
{
|
||||
if (!hbm_read_db(buf))
|
||||
{
|
||||
/* On error, we move the file to the 'bad' subdirectory,
|
||||
* eg data/history/bad/xyz.db
|
||||
*/
|
||||
char buf2[512];
|
||||
snprintf(buf2, sizeof(buf2), "%s/bad", cfg.directory);
|
||||
#ifdef _WIN32
|
||||
mkdir(buf2); /* (errors ignored) */
|
||||
#else
|
||||
mkdir(buf2, S_IRUSR|S_IWUSR|S_IXUSR); /* (errors ignored) */
|
||||
#endif
|
||||
snprintf(buf2, sizeof(buf2), "%s/bad/%s", cfg.directory, fname);
|
||||
unlink(buf2);
|
||||
rename(buf, buf2);
|
||||
}
|
||||
}
|
||||
|
||||
/* End of common section */
|
||||
#ifndef _WIN32
|
||||
}
|
||||
closedir(fd);
|
||||
#else
|
||||
} while (FindNextFile(hFile, &hData));
|
||||
FindClose(hFile);
|
||||
#endif
|
||||
}
|
||||
|
||||
#define RESET_VALUES_LOOP(x) do { \
|
||||
safe_free(mtag_name); \
|
||||
safe_free(mtag_value); \
|
||||
safe_free(line); \
|
||||
free_message_tags(mtags); \
|
||||
mtags = NULL; \
|
||||
magic = 0; \
|
||||
line_ts = 0; \
|
||||
} while(0)
|
||||
|
||||
#define R_SAFE_CLEANUP(x) do { \
|
||||
unrealdb_close(db); \
|
||||
RESET_VALUES_LOOP(); \
|
||||
safe_free(object); \
|
||||
} while(0)
|
||||
#define R_SAFE(x) \
|
||||
do { \
|
||||
if (!(x)) { \
|
||||
config_warn("[history] Read error from database file '%s' (possible corruption): %s", fname, unrealdb_get_error_string()); \
|
||||
R_SAFE_CLEANUP(); \
|
||||
return 0; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
|
||||
/** Read a channel history db file */
|
||||
static int hbm_read_db(char *fname)
|
||||
{
|
||||
UnrealDB *db = NULL;
|
||||
// header
|
||||
uint32_t magic = 0;
|
||||
uint32_t version = 0;
|
||||
char *object = NULL;
|
||||
uint64_t max_lines = 0;
|
||||
uint64_t max_time = 0;
|
||||
// then, for each entry:
|
||||
// (magic)
|
||||
uint64_t line_ts;
|
||||
char *mtag_name = NULL;
|
||||
char *mtag_value = NULL;
|
||||
MessageTag *mtags = NULL, *m;
|
||||
char *line = NULL;
|
||||
HistoryLogObject *h;
|
||||
|
||||
db = unrealdb_open(fname, UNREALDB_MODE_READ, cfg.db_secret);
|
||||
if (!db)
|
||||
{
|
||||
config_warn("[history] Unable to open the database file '%s' for reading: %s", fname, unrealdb_get_error_string());
|
||||
return 0;
|
||||
}
|
||||
|
||||
R_SAFE(unrealdb_read_int32(db, &magic));
|
||||
if (magic != HISTORYDB_MAGIC_FILE_START)
|
||||
{
|
||||
config_warn("[history] Database '%s' has wrong magic value, possibly corrupt (0x%lx), expected HISTORYDB_MAGIC_FILE_START.",
|
||||
fname, (long)magic);
|
||||
unrealdb_close(db);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Now do a version check */
|
||||
R_SAFE(unrealdb_read_int32(db, &version));
|
||||
if (version < 4999)
|
||||
{
|
||||
config_warn("[history] Database '%s' uses an unsupported - possibly old - format (%ld).", fname, (long)version);
|
||||
unrealdb_close(db);
|
||||
return 0;
|
||||
}
|
||||
if (version > 5000)
|
||||
{
|
||||
config_warn("[history] Database '%s' has version %lu while we only support %lu. Did you just downgrade UnrealIRCd? Sorry this is not suported",
|
||||
fname, (unsigned long)version, (unsigned long)5000);
|
||||
unrealdb_close(db);
|
||||
return 0;
|
||||
}
|
||||
|
||||
R_SAFE(unrealdb_read_str(db, &object));
|
||||
R_SAFE(unrealdb_read_int64(db, &max_lines));
|
||||
R_SAFE(unrealdb_read_int64(db, &max_time));
|
||||
h = hbm_find_object(object);
|
||||
if (!h)
|
||||
{
|
||||
config_warn("Channel %s does not have +H set, deleting history", object);
|
||||
R_SAFE_CLEANUP();
|
||||
unlink(fname);
|
||||
return 1; /* No problem */
|
||||
}
|
||||
|
||||
while(1)
|
||||
{
|
||||
RESET_VALUES_LOOP();
|
||||
R_SAFE(unrealdb_read_int32(db, &magic));
|
||||
if (magic == HISTORYDB_MAGIC_FILE_END)
|
||||
break; /* We're done, end gracefully */
|
||||
if (magic != HISTORYDB_MAGIC_ENTRY_START)
|
||||
{
|
||||
config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_START",
|
||||
fname, (long)magic);
|
||||
R_SAFE_CLEANUP();
|
||||
return 0;
|
||||
}
|
||||
|
||||
R_SAFE(unrealdb_read_int64(db, &line_ts));
|
||||
while(1)
|
||||
{
|
||||
R_SAFE(unrealdb_read_str(db, &mtag_name));
|
||||
R_SAFE(unrealdb_read_str(db, &mtag_value));
|
||||
if (!mtag_name && !mtag_value)
|
||||
break; /* We're done reading mtags for this particular line */
|
||||
m = safe_alloc(sizeof(MessageTag));
|
||||
safe_strdup(m->name, mtag_name);
|
||||
safe_strdup(m->value, mtag_value);
|
||||
AppendListItem(m, mtags);
|
||||
safe_free(mtag_name);
|
||||
safe_free(mtag_value);
|
||||
}
|
||||
R_SAFE(unrealdb_read_str(db, &line));
|
||||
R_SAFE(unrealdb_read_int32(db, &magic));
|
||||
if (magic != HISTORYDB_MAGIC_ENTRY_END)
|
||||
{
|
||||
config_warn("[history] Read error from database file '%s': wrong magic value in entry (0x%lx), expected HISTORYDB_MAGIC_ENTRY_END",
|
||||
fname, (long)magic);
|
||||
R_SAFE_CLEANUP();
|
||||
return 0;
|
||||
}
|
||||
hbm_history_add(object, mtags, line);
|
||||
}
|
||||
|
||||
/* Prevent directly rewriting the channel, now that we have just read it.
|
||||
* This could cause things not to fire in case of corner issues like
|
||||
* hot-loading but that should be acceptable. The alternative is that
|
||||
* all log files are written again with identical contents for no reason,
|
||||
* which is a waste of resources.
|
||||
*/
|
||||
h->dirty = 0;
|
||||
|
||||
R_SAFE_CLEANUP();
|
||||
return 1;
|
||||
}
|
||||
|
||||
/** Periodically clean the history.
|
||||
* Instead of doing all channels in 1 go, we do a limited number
|
||||
* of channels each call, hence the 'static int' and the do { } while
|
||||
|
@ -418,8 +950,12 @@ EVENT(history_mem_clean)
|
|||
|
||||
do
|
||||
{
|
||||
for (h = history_hash_table[hashnum++]; h; h = h->next)
|
||||
for (h = history_hash_table[hashnum]; h; h = h->next)
|
||||
{
|
||||
hbm_history_cleanup(h);
|
||||
if (cfg.persist && h->dirty)
|
||||
hbm_write_db(h);
|
||||
}
|
||||
|
||||
hashnum++;
|
||||
|
||||
|
@ -427,3 +963,119 @@ EVENT(history_mem_clean)
|
|||
hashnum = 0;
|
||||
} while(loopcnt++ < HISTORY_CLEAN_PER_LOOP);
|
||||
}
|
||||
|
||||
char *hbm_history_filename(HistoryLogObject *h)
|
||||
{
|
||||
static char fname[512];
|
||||
char oname[OBJECTLEN+1];
|
||||
char hashdata[512];
|
||||
char hash[64];
|
||||
|
||||
if (!cfg.prehash || !cfg.posthash)
|
||||
abort(); /* impossible */
|
||||
|
||||
strtolower_safe(oname, h->name, sizeof(oname));
|
||||
snprintf(hashdata, sizeof(hashdata), "%s %s %s", cfg.prehash, oname, cfg.posthash);
|
||||
md5hash(hash, hashdata, strlen(hashdata));
|
||||
|
||||
snprintf(fname, sizeof(fname), "%s/%s.db", cfg.directory, hash);
|
||||
return fname;
|
||||
}
|
||||
|
||||
#define WARN_WRITE_ERROR(fname) \
|
||||
do { \
|
||||
sendto_realops_and_log("[history] Error writing to temporary database file " \
|
||||
"'%s': %s (DATABASE NOT SAVED)", \
|
||||
fname, unrealdb_get_error_string()); \
|
||||
} while(0)
|
||||
|
||||
#define W_SAFE(x) \
|
||||
do { \
|
||||
if (!(x)) { \
|
||||
WARN_WRITE_ERROR(tmpfname); \
|
||||
unrealdb_close(db); \
|
||||
return 0; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
|
||||
// FIXME: the code below will cause massive floods on disk or I/O errors if hundreds of
|
||||
// channel logs fail to write... fun.
|
||||
static int hbm_write_db(HistoryLogObject *h)
|
||||
{
|
||||
UnrealDB *db;
|
||||
char *realfname;
|
||||
char tmpfname[512];
|
||||
HistoryLogLine *l;
|
||||
MessageTag *m;
|
||||
Channel *channel;
|
||||
|
||||
channel = find_channel(h->name, NULL);
|
||||
if (!channel || !has_channel_mode(channel, 'P'))
|
||||
return 1; /* Don't save this channel, pretend success */
|
||||
|
||||
realfname = hbm_history_filename(h);
|
||||
snprintf(tmpfname, sizeof(tmpfname), "%s.tmp", realfname);
|
||||
|
||||
#ifdef DEBUGMODE
|
||||
ircd_log(LOG_ERROR, "Writing to: %s...", tmpfname);
|
||||
#endif
|
||||
|
||||
db = unrealdb_open(tmpfname, UNREALDB_MODE_WRITE, cfg.db_secret);
|
||||
if (!db)
|
||||
{
|
||||
WARN_WRITE_ERROR(tmpfname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_START));
|
||||
W_SAFE(unrealdb_write_int32(db, 5000)); /* VERSION */
|
||||
W_SAFE(unrealdb_write_str(db, h->name));
|
||||
|
||||
W_SAFE(unrealdb_write_int64(db, h->max_lines));
|
||||
W_SAFE(unrealdb_write_int64(db, h->max_time));
|
||||
|
||||
for (l = h->head; l; l = l->next)
|
||||
{
|
||||
W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_START));
|
||||
W_SAFE(unrealdb_write_int64(db, l->t));
|
||||
for (m = l->mtags; m; m = m->next)
|
||||
{
|
||||
W_SAFE(unrealdb_write_str(db, m->name));
|
||||
W_SAFE(unrealdb_write_str(db, m->value)); /* can be NULL */
|
||||
}
|
||||
W_SAFE(unrealdb_write_str(db, NULL));
|
||||
W_SAFE(unrealdb_write_str(db, NULL));
|
||||
W_SAFE(unrealdb_write_str(db, l->line));
|
||||
W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_ENTRY_END));
|
||||
}
|
||||
W_SAFE(unrealdb_write_int32(db, HISTORYDB_MAGIC_FILE_END));
|
||||
|
||||
if (!unrealdb_close(db))
|
||||
{
|
||||
WARN_WRITE_ERROR(tmpfname);
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* The rename operation cannot be atomic on Windows as it will cause a "file exists" error */
|
||||
unlink(realfname);
|
||||
#endif
|
||||
if (rename(tmpfname, realfname) < 0)
|
||||
{
|
||||
sendto_realops_and_log("[history] Error renaming '%s' to '%s': %s (HISTORY NOT SAVED)",
|
||||
tmpfname, realfname, strerror(errno));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Now that everything was successful, clear the dirty flag */
|
||||
h->dirty = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void hbm_delete_db(HistoryLogObject *h)
|
||||
{
|
||||
UnrealDB *db;
|
||||
char *fname = hbm_history_filename(h);
|
||||
unlink(fname);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue