From 7486317497872839d1d10c3ee3241f83fee239f0 Mon Sep 17 00:00:00 2001 From: ac-stef Date: Fri, 26 Aug 2016 16:27:00 +0200 Subject: [PATCH] add changeable server parameters A list of parameters is read from config/serverparameters.cfg once per minute and the values written to corresponding server data structures. If the binary is compiled in debug mode, starting a dedicated server will write a blank config file with all possible parameters and their descriptions, sorted and grouped into chapters. The name of the config file can be set with the commandline parameter -A. So far, syntax errors are not reported. Instead, it's possible to dump all current values to a file every time something changes. --- config/servercmdline.txt | 2 +- config/serverparameters.cfg | 19 +++ source/src/protos.h | 10 +- source/src/server.cpp | 28 ++++- source/src/serverfiles.h | 233 ++++++++++++++++++++++++++++++++++++ source/src/tools.cpp | 5 +- 6 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 config/serverparameters.cfg diff --git a/config/servercmdline.txt b/config/servercmdline.txt index 3bce2d98..9af8915a 100644 --- a/config/servercmdline.txt +++ b/config/servercmdline.txt @@ -39,6 +39,7 @@ // -y // banthreshold, -1..-100, default -6 // -H // server logfile path, default logs/ // -r // server maprot file, default config/maprot.cfg +// -A // server parameter file, default config/serverparameters.cfg // -X // server passwords file, default config/serverpwd.cfg // -B // server IP blacklist file, default config/serverblacklist.cfg // -G // server geoip file, default config/geoip.cfg @@ -88,7 +89,6 @@ // 3 WARNING: log only messages of level WARNING and above // 4 ERROR: log only messages of level ERROR // 5 do not write to the log -// -A // Restricts voting for a map/mode to admins. This switch can be used several times. // these switches control the naming of demos (see -W) // --demofilenameformat="%Mmin_%G_%w_%H_%n" // default: "%w_%h_%n_%Mmin_%G" diff --git a/config/serverparameters.cfg b/config/serverparameters.cfg new file mode 100644 index 00000000..4e11f635 --- /dev/null +++ b/config/serverparameters.cfg @@ -0,0 +1,19 @@ +// serverparameters.cfg +// +// parameters from this file are read once per minute by the server +// and are effective immediately + +// each setting consists of a keyword and a value, written like this: keyword:value +// there are no spaces allowed between keyword and value +// using two colons (keyword::value) activates rich-text filtering for the value + + + +// **** Debug switches **** + +// dumpparameters dump server parameters when updated +// integer [0..1], default 0, 0:disabled, 1:enabled +dumpparameters:0 + + + diff --git a/source/src/protos.h b/source/src/protos.h index 490dedad..ae343830 100644 --- a/source/src/protos.h +++ b/source/src/protos.h @@ -1132,7 +1132,7 @@ struct serverconfigfile virtual void clear() {} virtual void read() {} void init(const char *name, bool tracking = true); - bool load(); + virtual bool load(); bool isbusy(); void trypreload(); void process(); @@ -1142,18 +1142,18 @@ struct serverconfigfile struct servercommandline { int uprate, serverport, syslogfacility, filethres, syslogthres, maxdemos, maxclients, kickthreshold, banthreshold, verbose, incoming_limit, afk_limit, ban_time, demotimelocal; - const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprot, *pwdfile, *blfile, *geoipfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service, *logfilepath; + const char *ip, *master, *logident, *serverpassword, *adminpasswd, *demopath, *maprot, *pwdfile, *blfile, *geoipfile, *nbfile, *infopath, *motdpath, *forbidden, *demofilenameformat, *demotimestampformat, *service, *logfilepath, *parfilepath; uchar *ssk; bool logtimestamp, demo_interm, loggamestatus; string motd, servdesc_full, servdesc_pre, servdesc_suf, voteperm, mapperm; int clfilenesting; - vector adminonlymaps; + vector adminonlymaps; // FIXME: remove this servercommandline() : uprate(0), serverport(CUBE_DEFAULT_SERVER_PORT), syslogfacility(6), filethres(-1), syslogthres(-1), maxdemos(5), maxclients(DEFAULTCLIENTS), kickthreshold(-5), banthreshold(-6), verbose(0), incoming_limit(10), afk_limit(45000), ban_time(20*60*1000), demotimelocal(0), ip(""), master(NULL), logident(""), serverpassword(""), adminpasswd(""), demopath(""), maprot("config/maprot.cfg"), pwdfile("config/serverpwd.cfg"), blfile("config/serverblacklist.cfg"), geoipfile("config/geoip.cfg"), nbfile("config/nicknameblacklist.cfg"), - infopath("config/serverinfo"), motdpath("config/motd"), forbidden("config/forbidden.cfg"), service(NULL), logfilepath("logs/"), + infopath("config/serverinfo"), motdpath("config/motd"), forbidden("config/forbidden.cfg"), service(NULL), logfilepath("logs/"), parfilepath("config/serverparameters.cfg"), ssk(NULL), logtimestamp(false), demo_interm(false), loggamestatus(true), clfilenesting(0) @@ -1220,7 +1220,7 @@ struct servercommandline } break; case 'H': logfilepath = a; break; - case 'A': if(*a) adminonlymaps.add(a); break; + case 'A': parfilepath = a; break; case 'c': if(ai > 0) maxclients = min(ai, MAXCLIENTS); break; case 'k': { diff --git a/source/src/server.cpp b/source/src/server.cpp index 4b26721b..162abaa3 100644 --- a/source/src/server.cpp +++ b/source/src/server.cpp @@ -76,6 +76,7 @@ struct servergame vector servermaps; // all available maps kept in memory servercontroller *svcctrl = NULL; +serverparameter serverparameters; servercommandline scl; servermaprot maprot; serveripblacklist ipblacklist; @@ -105,6 +106,8 @@ int totalclients = 0; int cn2boot; // synchronising the worker threads... +const char *endis[] = { "disabled", "enabled", "" }; +SERVPARLIST(dumpparameters, 0, 0, 0, endis, "ddump server parameters when updated"); void poll_serverthreads() // called once per mainloop-timeslice { @@ -180,7 +183,23 @@ void poll_serverthreads() // called once per mainloop-timeslice } break; } - case 4: // pause worker threads for a while (restart once a minute) + case 4: // check for fresh config files + { + if(serverparameters.updated) + { + serverparameters.updated = false; + if(dumpparameters) + { + defformatstring(fname)("%sdebug/serverparameter_dump_%s.txt", scl.logfilepath, numtime()); + stream *f = openfile(path(fname), "w"); + f->printf("// server parameter dump, %s\n\n", asctimestr()); + serverparameters.dump(f); + delete f; + } + } + else stage++; + } + case 5: // pause worker threads for a while (restart once a minute) { if(servmillis - lastworkerthreadstart > 60 * 1000) stage = 0; else if(numclients() == 0) @@ -4330,6 +4349,8 @@ void initserver(bool dedicated) if((isdedicated = dedicated)) { + serverparameters.init(scl.parfilepath); + if(serverparameters.load()) serverparameters.read(); // do this early, so the parameters are properly set during initialisation ENetAddress address = { ENET_HOST_ANY, (enet_uint16)scl.serverport }; if(scl.ip[0] && enet_address_set_host(&address, scl.ip)<0) logline(ACLOG_WARNING, "server ip not resolved!"); serverhost = enet_host_create(&address, scl.maxclients+1, 3, 0, scl.uprate); @@ -4433,7 +4454,10 @@ int main(int argc, char **argv) else printf("WARNING: unknown commandline argument \"%s\"\n", argv[i]); } } - +#ifdef _DEBUG + defformatstring(spdoc)("%s/docs/serverparameters.cfg", scl.logfilepath); + serverparameters_dumpdocu(spdoc); +#endif if(scl.service && !svcctrl) { #ifdef WIN32 diff --git a/source/src/serverfiles.h b/source/src/serverfiles.h index 661b2847..202d8555 100644 --- a/source/src/serverfiles.h +++ b/source/src/serverfiles.h @@ -1,5 +1,18 @@ // serverfiles.h +// abuse globals to register server parameters +#ifdef _DEBUG +#define SERVPAR(name, min, cur, max, desc) int last_##name, name = addservparint(#name, min, cur, max, NULL, &name, &last_##name, NULL, false, true, desc) +#define SERVPARLIST(name, min, cur, max, list, desc) int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, NULL, false, true, desc) +#define SERVPARF(name, min, cur, max, body, desc) extern int last_##name, name; void var_##name() { body; } int last_##name, name = addservparint(#name, min, cur, max, NULL,&name, &last_##name, var_##name, false, true, desc) +#define SERVSTR(name, cur, min, max, filt, desc) char last_##name[max+1], name[max+1]; bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, NULL, false, true, desc) +#else +#define SERVPAR(name, min, cur, max, desc) int last_##name, name = addservparint(#name, min, cur, max, NULL, &name, &last_##name, NULL, false, true, NULL) +#define SERVPARLIST(name, min, cur, max, list, desc) int last_##name, name = addservparint(#name, min, cur, max, list, &name, &last_##name, NULL, false, true, NULL) +#define SERVPARF(name, min, cur, max, body, desc) extern int last_##name, name; void var_##name() { body; } int last_##name, name = addservparint(#name, min, cur, max, NULL, &name, &last_##name, var_##name, false, true, NULL) +#define SERVSTR(name, cur, min, max, filt, desc) char last_##name[max+1], name[max+1]; bool __sdummy_##name = addservparstr(#name, min, max, filt, cur, name, last_##name, NULL, false, true, NULL) +#endif + // map management #define SERVERMAXMAPFACTOR 10 // 10 is huge... if you're running low on RAM, consider 9 as maximum (don't go higher than 10, or players will name their ugly dog after you) @@ -1382,3 +1395,223 @@ struct serverinfofile : serverconfigfile // plaintext info file, used for serve } }; +// realtime server parameters + +enum { SID_INT, SID_STR }; + +struct servpar +{ + int type; // one of SPAR_* above + const char *name; + union + { + int minval; // SID_INT + int minlen; // SID_STR + }; + union + { + int maxval; // SID_INT + int maxlen; // SID_STR + }; + union + { + int *i; // SID_INT + char *s; // SID_STR + }; + union + { + int *shadow_i; // SID_INT + char *shadow_s; // SID_STR + }; + void (*fun)(); + union + { + const char **list; // SID_INT + const char *defaultstr; // SID_STR + }; + union + { + int defaultint; // SID_INT + int filter; // SID_STR + }; +#ifdef _DEBUG + const char *desc; + int chapter; +#endif + bool log; // log value changes (careful!) + bool fromfile; // true: value can be set by config file, false: value is set by server + + servpar() {} + + // SID_INT + servpar(int type, const char *name, int minval, int maxval, const char *list[], int *i, int *shadow_i, int defval, void (*fun)(), bool log, bool fromfile, const char *_desc) + : type(type), name(name), minval(minval), maxval(maxval), i(i), shadow_i(shadow_i), fun(fun), list(list), defaultint(defval), log(log), fromfile(fromfile) + { DEBUGCODE(desc = _desc); } + + // SID_STR + servpar(int type, const char *name, int minlen, int maxlen, int filter, char *s, char *shadow_s, const char *defaultstr, void (*fun)(), bool log, int fromfile, const char *_desc) + : type(type), name(name), minlen(minlen), maxlen(maxlen), s(s), shadow_s(shadow_s), fun(fun), defaultstr(defaultstr), filter(filter), log(log), fromfile(fromfile) + { DEBUGCODE(desc = _desc;) } +}; + +hashtable *servpars = NULL; + +int addservparint(const char *name, int minval, int cur, int maxval, const char *list[], int *storage, int *shadowstorage, void (*fun)(), bool log, bool fromfile, const char *desc) +{ + if(!servpars) servpars = new hashtable; + if(list && minval == maxval) while(list[maxval - minval + 1][0]) maxval++; // get list length + ASSERT(cur >= minval && cur <= maxval); + servpar v(SID_INT, name, minval, maxval, list, storage, shadowstorage, cur, fun, log, fromfile, desc); + servpars->access(name, v); + *shadowstorage = cur; + return cur; +} + +bool addservparstr(const char *name, int minlen, int maxlen, int filt, const char *cur, char *storage, char *shadowstorage, void (*fun)(), bool log, bool fromfile, const char *desc) +{ + if(!servpars) servpars = new hashtable; + ASSERT(maxlen > minlen && minlen >= 0 && int(strlen(cur)) >= minlen && int(strlen(cur)) <= maxlen); + servpar v(SID_STR, name, minlen, maxlen, filt, storage, shadowstorage, cur, fun, log, fromfile, desc); + servpars->access(name, v); + strncpy(storage, cur, maxlen + 1); + strncpy(shadowstorage, cur, maxlen + 1); + storage[maxlen] = shadowstorage[maxlen] = '\0'; + return true; +} + +struct serverparameter : serverconfigfile +{ + bool load() + { + if(serverconfigfile::load()) + { + int line = 0, idx; + char *b, *l, *p = buf, *kb, *k, *v; + servpar *id; + while(p < buf + filelen) + { + l = p; p += strlen(p) + 1; line++; + l = strtok_r(l, " ", &b); + if(l && (k = strtok_r(l, ":", &kb)) && (v = strtok_r(NULL, ":", &kb)) && (id = servpars->access(k)) && id->fromfile) switch(id->type) + { + case SID_INT: + { + int i = ATOI(v); + if(id->list && (idx = getlistindex(v, id->list, false, -1)) >= 0) i = idx + id->minval; // multiple choice + if(i >= id->minval && i <= id->maxval) *(id->shadow_i) = i; + break; + } + case SID_STR: + { + if((v - k) - strlen(k) > 1) filterrichtext(v, v); + filtertext(v, v, id->filter); + int i = (int)strlen(v); + if(i >= id->minlen && i <= id->maxlen) strncpy(id->shadow_s, v, id->maxlen + 1); + break; + } + } + } + return true; + } + return false; + } + + void read() + { // in mainloop: update values from shadow values + enumerate(*servpars, servpar, id, + switch(id.type) + { + case SID_INT: + { + if(*id.i != *id.shadow_i) + { + if(id.fun) ((void (__cdecl *)())id.fun)(); + *id.i = *id.shadow_i; + } + break; + } + case SID_STR: + { + if(strcmp(id.s, id.shadow_s)) + { + if(id.fun) ((void (__cdecl *)())id.fun)(); + strncpy(id.s, id.shadow_s, id.maxlen + 1); + } + break; + } + } + ); + } + + void dump(stream *f) + { + enumerate(*servpars, servpar, id, + switch(id.type) + { + case SID_INT: f->printf("%s:%d // default %d\n", id.name, *id.i, id.defaultint); break; + case SID_STR: f->printf("%s:%s // default %s\n", id.name, id.s, id.defaultstr); break; + } + ); + } +}; + +#ifdef _DEBUG +int siddocsort(servpar **a, servpar **b) { return (*a)->chapter == (*b)->chapter ? strcmp((*a)->name, (*b)->name) : (*a)->chapter - (*b)->chapter; } + +const char *siddocchapters[] = { "dDebug switches", "mMisc settings", "vVote settings", "" }; +const char *siddocchaptersorting = "vmd"; + +void serverparameters_dumpdocu(char *fname) +{ + vector sp; + enumerate(*servpars, servpar, id, if(id.fromfile && id.desc && id.desc[0]) sp.add(&id)); + loopv(sp) + { + const char *e = strchr(siddocchaptersorting, sp[i]->desc[0]); + sp[i]->chapter = e ? (int) (e - siddocchaptersorting) : sp[i]->desc[0]; + } + sp.sort(siddocsort); + stream *f = openfile(path(fname), "w"); + int lastchap = 0; + if(f) + { + f->printf("// serverparameters.cfg\n//\n// parameters from this file are read once per minute by the server\n// and are effective immediately\n\n" + "// each setting consists of a keyword and a value, written like this: keyword:value\n// there are no spaces allowed between keyword and value\n" + "// using two colons (keyword::value) activates rich-text filtering for the value\n\n\n"); + loopv(sp) + { + servpar &id = *sp[i]; + if(lastchap != id.desc[0]) + { + int ch = -1; + for(int k = 0; siddocchapters[k][0]; k++) if(siddocchapters[k][0] == id.desc[0]) ch = k; + lastchap = id.desc[0]; + if(ch < 0) f->printf("\n// **** untitled chapter %c ****\n\n", lastchap); + else f->printf("\n// **** %s ****\n\n", siddocchapters[ch] + 1); + } + switch(id.type) + { + case SID_INT: + { + f->printf("// %s %s\n// integer [%d..%d], default %d", id.name, id.desc + 1, id.minval, id.maxval, id.defaultint); + if(id.list) + { + for(int k = 0; id.list[k] && id.list[k][0]; k++) f->printf(", %d:%s", id.minval + k, id.list[k]); + } + f->printf("\n%s:%d\n\n", id.name, id.defaultint); + break; + } + case SID_STR: + { + f->printf("// %s %s\n// string [%d..%d chars], default \"%s\"\n%s:%s\n\n", id.name, id.desc + 1, id.minlen, id.maxlen, id.defaultstr, id.name, id.defaultstr); + break; + } + } + } + f->printf("\n\n"); + delete f; + } +} +#endif + + diff --git a/source/src/tools.cpp b/source/src/tools.cpp index 99362ea8..f78c8a15 100644 --- a/source/src/tools.cpp +++ b/source/src/tools.cpp @@ -561,7 +561,7 @@ const char *hiddenpwd(const char *pwd, int showchars) int getlistindex(const char *key, const char *list[], bool acceptnumeric, int deflt) { int max = 0; - while(list[max][0]) if(!strcasecmp(key, list[max])) return max; else max++; + while(list[max] && list[max][0]) if(!strcasecmp(key, list[max])) return max; else max++; if(acceptnumeric && isdigit(key[0])) { int i = (int)strtol(key, NULL, 0); @@ -569,7 +569,8 @@ int getlistindex(const char *key, const char *list[], bool acceptnumeric, int de } #if !defined(STANDALONE) && defined(_DEBUG) char *opts = conc(list, -1, true); - if(*key) clientlogf("warning: unknown token \"%s\" (not in list [%s])", key, opts); + extern bool isdedicated; + if(!isdedicated && *key) clientlogf("warning: unknown token \"%s\" (not in list [%s])", key, opts); delstring(opts); #endif return deflt;