From 6bb620cb803a6587dcbacc4a3360cbf3d75f3064 Mon Sep 17 00:00:00 2001 From: RblSb Date: Wed, 3 Jun 2020 14:32:02 +0300 Subject: Event logs and log replay --- build/server.js | 522 +++++++++++++++++++++++++++++++++++++++------ res/client.js | 2 + src/Types.hx | 1 + src/client/Main.hx | 1 + src/server/ConsoleInput.hx | 141 ++++++++++-- src/server/Logger.hx | 69 ++++++ src/server/Main.hx | 136 ++++++++---- src/server/ServerEvent.hx | 10 + src/server/Utils.hx | 5 + user/README.md | 4 +- 10 files changed, 758 insertions(+), 133 deletions(-) create mode 100644 src/server/Logger.hx create mode 100644 src/server/ServerEvent.hx diff --git a/build/server.js b/build/server.js index 09fa596..4789311 100644 --- a/build/server.js +++ b/build/server.js @@ -251,6 +251,35 @@ HxOverrides.dateStr = function(date) { var s = date.getSeconds(); return date.getFullYear() + "-" + (m < 10 ? "0" + m : "" + m) + "-" + (d < 10 ? "0" + d : "" + d) + " " + (h < 10 ? "0" + h : "" + h) + ":" + (mi < 10 ? "0" + mi : "" + mi) + ":" + (s < 10 ? "0" + s : "" + s); }; +HxOverrides.strDate = function(s) { + switch(s.length) { + case 8: + var k = s.split(":"); + var d = new Date(); + d["setTime"](0); + d["setUTCHours"](k[0]); + d["setUTCMinutes"](k[1]); + d["setUTCSeconds"](k[2]); + return d; + case 10: + var k = s.split("-"); + return new Date(k[0],k[1] - 1,k[2],0,0,0); + case 19: + var k = s.split(" "); + var y = k[0].split("-"); + var t = k[1].split(":"); + return new Date(y[0],y[1] - 1,y[2],t[0],t[1],t[2]); + default: + throw haxe_Exception.thrown("Invalid date format : " + s); + } +}; +HxOverrides.cca = function(s,index) { + var x = s.charCodeAt(index); + if(x != x) { + return undefined; + } + return x; +}; HxOverrides.substr = function(s,pos,len) { if(len == null) { len = s.length; @@ -914,7 +943,7 @@ JsonParser_$28.prototype = $extend(json2object_reader_BaseParser.prototype,{ this.value = null; } ,loadJsonString: function(s,pos,variable) { - this.value = this.loadString(s,pos,variable,["Connected","Login","PasswordRequest","LoginError","Logout","Message","ServerMessage","UpdateClients","AddVideo","RemoveVideo","SkipVideo","VideoLoaded","Pause","Play","GetTime","SetTime","SetRate","Rewind","SetLeader","PlayItem","SetNextItem","ToggleItemType","ClearChat","ClearPlaylist","ShufflePlaylist","UpdatePlaylist","TogglePlaylistLock"],"Connected"); + this.value = this.loadString(s,pos,variable,["Connected","Disconnected","Login","PasswordRequest","LoginError","Logout","Message","ServerMessage","UpdateClients","AddVideo","RemoveVideo","SkipVideo","VideoLoaded","Pause","Play","GetTime","SetTime","SetRate","Rewind","SetLeader","PlayItem","SetNextItem","ToggleItemType","ClearChat","ClearPlaylist","ShufflePlaylist","UpdatePlaylist","TogglePlaylistLock"],"Connected"); } ,__class__: JsonParser_$28 }); @@ -1876,9 +1905,28 @@ Lambda.exists = function(it,f) { } return false; }; +Lambda.count = function(it,pred) { + var n = 0; + if(pred == null) { + var _ = $getIterator(it); + while(_.hasNext()) { + _.next(); + ++n; + } + } else { + var x = $getIterator(it); + while(x.hasNext()) if(pred(x.next())) { + ++n; + } + } + return n; +}; var haxe_IMap = function() { }; haxe_IMap.__name__ = true; haxe_IMap.__isInterface__ = true; +haxe_IMap.prototype = { + __class__: haxe_IMap +}; var haxe_ds_StringMap = function() { this.h = Object.create(null); }; @@ -1895,8 +1943,32 @@ haxe_ds_StringMap.keysIterator = function(h) { return keys[idx - 1]; }}; }; +haxe_ds_StringMap.kvIterator = function(h) { + var keys = Object.keys(h); + var len = keys.length; + var idx = 0; + return { hasNext : function() { + return idx < len; + }, next : function() { + idx += 1; + var k = keys[idx - 1]; + return { key : k, value : h[k]}; + }}; +}; haxe_ds_StringMap.prototype = { - __class__: haxe_ds_StringMap + get: function(key) { + return this.h[key]; + } + ,set: function(key,value) { + this.h[key] = value; + } + ,keys: function() { + return haxe_ds_StringMap.keysIterator(this.h); + } + ,keyValueIterator: function() { + return haxe_ds_StringMap.kvIterator(this.h); + } + ,__class__: haxe_ds_StringMap }; var Lang = function() { }; Lang.__name__ = true; @@ -2013,6 +2085,46 @@ StringTools.startsWith = function(s,start) { return false; } }; +StringTools.endsWith = function(s,end) { + var elen = end.length; + var slen = s.length; + if(slen >= elen) { + return s.indexOf(end,slen - elen) == slen - elen; + } else { + return false; + } +}; +StringTools.isSpace = function(s,pos) { + var c = HxOverrides.cca(s,pos); + if(!(c > 8 && c < 14)) { + return c == 32; + } else { + return true; + } +}; +StringTools.ltrim = function(s) { + var l = s.length; + var r = 0; + while(r < l && StringTools.isSpace(s,r)) ++r; + if(r > 0) { + return HxOverrides.substr(s,r,l - r); + } else { + return s; + } +}; +StringTools.rtrim = function(s) { + var l = s.length; + var r = 0; + while(r < l && StringTools.isSpace(s,l - r - 1)) ++r; + if(r > 0) { + return HxOverrides.substr(s,0,l - r); + } else { + return s; + } +}; +StringTools.trim = function(s) { + return StringTools.ltrim(StringTools.rtrim(s)); +}; StringTools.lpad = function(s,c,l) { if(c.length <= 0) { return s; @@ -2023,6 +2135,15 @@ StringTools.lpad = function(s,c,l) { buf_b += s == null ? "null" : "" + s; return buf_b; }; +StringTools.rpad = function(s,c,l) { + if(c.length <= 0) { + return s; + } + var buf_b = ""; + buf_b = "" + (s == null ? "null" : "" + s); + while(buf_b.length < l) buf_b += c == null ? "null" : "" + c; + return buf_b; +}; StringTools.replace = function(s,sub,by) { return s.split(sub).join(by); }; @@ -2429,6 +2550,75 @@ haxe_io_Path.extension = function(path) { } return s.ext; }; +haxe_io_Path.normalize = function(path) { + var slash = "/"; + path = path.split("\\").join(slash); + if(path == slash) { + return slash; + } + var target = []; + var _g = 0; + var _g1 = path.split(slash); + while(_g < _g1.length) { + var token = _g1[_g]; + ++_g; + if(token == ".." && target.length > 0 && target[target.length - 1] != "..") { + target.pop(); + } else if(token == "") { + if(target.length > 0 || HxOverrides.cca(path,0) == 47) { + target.push(token); + } + } else if(token != ".") { + target.push(token); + } + } + var acc_b = ""; + var colon = false; + var slashes = false; + var _g2_offset = 0; + var _g2_s = target.join(slash); + while(_g2_offset < _g2_s.length) { + var s = _g2_s; + var index = _g2_offset++; + var c = s.charCodeAt(index); + if(c >= 55296 && c <= 56319) { + c = c - 55232 << 10 | s.charCodeAt(index + 1) & 1023; + } + var c1 = c; + if(c1 >= 65536) { + ++_g2_offset; + } + var c2 = c1; + switch(c2) { + case 47: + if(!colon) { + slashes = true; + } else { + var i = c2; + colon = false; + if(slashes) { + acc_b += "/"; + slashes = false; + } + acc_b += String.fromCodePoint(i); + } + break; + case 58: + acc_b += ":"; + colon = true; + break; + default: + var i1 = c2; + colon = false; + if(slashes) { + acc_b += "/"; + slashes = false; + } + acc_b += String.fromCodePoint(i1); + } + } + return acc_b; +}; haxe_io_Path.addTrailingSlash = function(path) { if(path.length == 0) { return "/"; @@ -3129,13 +3319,19 @@ json2object_PositionUtils.prototype = { ,__class__: json2object_PositionUtils }; var server_ConsoleInput = function(main) { + var _g = new haxe_ds_StringMap(); + _g.set("addAdmin",{ args : ["name","password"], desc : "Adds channel admin"}); + _g.set("replay",{ args : ["name"], desc : "Replay log file on server from user/logs/"}); + _g.set("logList",{ args : [], desc : "Show log list from user/logs/"}); + _g.set("exit",{ args : [], desc : "Exit process"}); + this.commands = _g; this.main = main; }; server_ConsoleInput.__name__ = true; server_ConsoleInput.prototype = { initConsoleInput: function() { var _gthis = this; - var rl = js_node_Readline.createInterface(process.stdin,process.stdout); + var rl = js_node_Readline.createInterface({ input : process.stdin, output : process.stdout, completer : $bind(this,this.onCompletion)}); haxe_Log.trace = function(msg,pos) { js_node_Readline.clearLine(process.stdout,0); js_node_Readline.cursorTo(process.stdout,0,null); @@ -3148,26 +3344,105 @@ server_ConsoleInput.prototype = { rl.prompt(); }); } - ,parseLine: function(line) { - if(StringTools.startsWith(line,"/addAdmin")) { - var args = line.split(" "); - if(args.length != 3) { - haxe_Log.trace("Wrong count of arguments",{ fileName : "src/server/ConsoleInput.hx", lineNumber : 36, className : "server.ConsoleInput", methodName : "parseLine"}); - return; + ,onCompletion: function(line) { + var _g = []; + var item = this.commands.keys(); + while(item.hasNext()) _g.push("/" + item.next() + " "); + var _g1 = []; + var _g11 = 0; + while(_g11 < _g.length) { + var v = _g[_g11]; + ++_g11; + if(StringTools.startsWith(v,line)) { + _g1.push(v); } - var name = args[1]; - var password = args[2]; + } + if(_g1.length > 0) { + return [_g1,line]; + } + return [_g,line]; + } + ,parseLine: function(line) { + if(line.charCodeAt(0) != 47 || line.length < 2) { + this.printHelp(line); + return; + } + var args = StringTools.trim(line).split(" "); + var command = HxOverrides.substr(args.shift(),1,null); + if(this.commands.get(command) == null) { + this.printHelp(line); + return; + } + if(!this.isValidArgs(command,args)) { + return; + } + switch(command) { + case "addAdmin": + var name = args[0]; + var password = args[1]; if(this.main.badNickName(name)) { - haxe_Log.trace(StringTools.replace(Lang.get("usernameError"),"$MAX","" + this.main.config.maxLoginLength),{ fileName : "src/server/ConsoleInput.hx", lineNumber : 44, className : "server.ConsoleInput", methodName : "parseLine"}); + haxe_Log.trace(StringTools.replace(Lang.get("usernameError"),"$MAX","" + this.main.config.maxLoginLength),{ fileName : "src/server/ConsoleInput.hx", lineNumber : 100, className : "server.ConsoleInput", methodName : "parseLine"}); return; } this.main.addAdmin(name,password); - } else if(line == "/exit") { + break; + case "exit": this.main.exit(); - return; - } else { - haxe_Log.trace("Unknown command \"" + line + "\". List:\n/addAdmin name password | Adds channel admin\n/exit | Exit process",{ fileName : "src/server/ConsoleInput.hx", lineNumber : 53, className : "server.ConsoleInput", methodName : "parseLine"}); + break; + case "logList": + server_Utils.ensureDir(this.main.logsDir); + var _this = js_node_Fs.readdirSync(this.main.logsDir); + var _g = []; + var _g1 = 0; + while(_g1 < _this.length) { + var v = _this[_g1]; + ++_g1; + if(StringTools.endsWith(v,".json")) { + _g.push(v); + } + } + var _g1 = 0; + while(_g1 < _g.length) haxe_Log.trace(haxe_io_Path.withoutExtension(_g[_g1++]),{ fileName : "src/server/ConsoleInput.hx", lineNumber : 121, className : "server.ConsoleInput", methodName : "parseLine"}); + break; + case "replay": + server_Utils.ensureDir(this.main.logsDir); + var path = haxe_io_Path.normalize("" + this.main.logsDir + "/" + args[0] + ".json"); + if(!sys_FileSystem.exists(path)) { + haxe_Log.trace("File \"" + path + "\" not found",{ fileName : "src/server/ConsoleInput.hx", lineNumber : 110, className : "server.ConsoleInput", methodName : "parseLine"}); + return; + } + var events = JSON.parse(js_node_Fs.readFileSync(path,{ encoding : "utf8"})); + this.main.replayLog(events); + break; + } + } + ,isValidArgs: function(command,args) { + var len = args.length; + var actual = this.commands.get(command).args.length; + if(len != actual) { + haxe_Log.trace("Wrong count of arguments for command \"" + command + "\" (" + len + " instead of " + actual + ")",{ fileName : "src/server/ConsoleInput.hx", lineNumber : 132, className : "server.ConsoleInput", methodName : "isValidArgs"}); + return false; + } + return true; + } + ,printHelp: function(line) { + var maxLength = 0; + var _g = this.commands.keyValueIterator(); + while(_g.hasNext()) { + var _g1 = _g.next(); + var len = ("/" + _g1.key + " " + _g1.value.args.join(" ")).length; + if(maxLength < len) { + maxLength = len; + } } + var list = []; + var _g = this.commands.keyValueIterator(); + while(_g.hasNext()) { + var _g1 = _g.next(); + var data = _g1.value; + list.push("" + StringTools.rpad("/" + _g1.key + " " + data.args.join(" ")," ",maxLength) + " | " + data.desc); + } + haxe_Log.trace("Unknown command \"" + line + "\". List:\n" + list.join("\n"),{ fileName : "src/server/ConsoleInput.hx", lineNumber : 151, className : "server.ConsoleInput", methodName : "printHelp"}); } ,__class__: server_ConsoleInput }; @@ -3327,6 +3602,72 @@ server_HttpServer.getMimeType = function(ext) { } return contentType; }; +var server_Logger = function(folder,maxCount,verbose) { + this.matchFileFormat = new EReg("[0-9_-]+\\.json$",""); + this.logs = []; + this.folder = folder; + this.maxCount = maxCount; + this.verbose = verbose; +}; +server_Logger.__name__ = true; +server_Logger.prototype = { + log: function(event) { + this.logs.push(event); + if(this.logs.length > 5000) { + this.logs.shift(); + } + } + ,saveLog: function() { + if(this.logs.length == 0) { + return; + } + server_Utils.ensureDir(this.folder); + this.removeOldestLog(this.folder); + var name = DateTools.format(new Date(),"%Y-%m-%d_%H_%M_%S"); + js_node_Fs.writeFileSync("" + this.folder + "/" + name + ".json",JSON.stringify(this.logs,$bind(this,this.filterNulls),"\t")); + } + ,filterNulls: function(key,value) { + if(value == null) { + return undefined; + } + return value; + } + ,removeOldestLog: function(folder) { + var _gthis = this; + var names = js_node_Fs.readdirSync(folder); + if(Lambda.count(names,function(item) { + return _gthis.matchFileFormat.match(item); + }) < this.maxCount) { + return; + } + var minDate = 0.0; + var fileName = null; + var _g = 0; + while(_g < names.length) { + var name = names[_g]; + ++_g; + var date = this.extractFileDate(name).getTime(); + if(minDate == 0 || minDate > date) { + minDate = date; + fileName = name; + } + } + if(fileName == null) { + return; + } + js_node_Fs.unlinkSync("" + folder + "/" + fileName); + } + ,extractFileDate: function(name) { + name = haxe_io_Path.withoutExtension(name); + var t = name.split("_"); + var d = t.shift().split("-"); + if(d.length != 3 && t.length != 3) { + return new Date(0); + } + return HxOverrides.strDate("" + d[0] + "-" + d[1] + "-" + d[2] + " " + t[0] + ":" + t[1] + ":" + t[2]); + } + ,__class__: server_Logger +}; var server_Main = function() { this.loadedClientsCount = 0; this.htmlChars = new EReg("[&^<>'\"]",""); @@ -3342,6 +3683,7 @@ var server_Main = function() { var _gthis = this; this.verbose = Lambda.has(process.argv.slice(2),"--verbose"); this.statePath = "" + this.rootDir + "/user/state.json"; + this.logsDir = "" + this.rootDir + "/user/logs"; process.on("SIGINT",$bind(this,this.exit)); process.on("SIGUSR1",$bind(this,this.exit)); process.on("SIGUSR2",$bind(this,this.exit)); @@ -3354,6 +3696,7 @@ var server_Main = function() { _gthis.logError("unhandledRejection",reason); _gthis.exit(); }); + this.logger = new server_Logger(this.logsDir,10,this.verbose); this.consoleInput = new server_ConsoleInput(this); this.consoleInput.initConsoleInput(); this.initIntergationHandlers(); @@ -3371,8 +3714,8 @@ var server_Main = function() { } server_Utils.getGlobalIp(function(ip) { _gthis.globalIp = ip; - haxe_Log.trace("Local: http://" + _gthis.localIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 86, className : "server.Main", methodName : "new"}); - haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 87, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Local: http://" + _gthis.localIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 90, className : "server.Main", methodName : "new"}); + haxe_Log.trace("Global: http://" + _gthis.globalIp + ":" + _gthis.port,{ fileName : "src/server/Main.hx", lineNumber : 91, className : "server.Main", methodName : "new"}); }); var dir = "" + this.rootDir + "/res"; server_HttpServer.init(dir,"" + this.rootDir + "/user/res",this.config.localAdmins); @@ -3391,6 +3734,7 @@ server_Main.main = function() { server_Main.prototype = { exit: function() { this.saveState(); + this.logger.saveLog(); if(this.wss == null) { process.exit(); return; @@ -3445,7 +3789,7 @@ server_Main.prototype = { var field = _g1[_g]; ++_g; if(Reflect.field(config,field) == null) { - haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 141, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: config field \"" + field + "\" is unknown",{ fileName : "src/server/Main.hx", lineNumber : 146, className : "server.Main", methodName : "getUserConfig"}); } config[field] = Reflect.field(customConfig,field); } @@ -3456,14 +3800,14 @@ server_Main.prototype = { var emote = _g1[_g]; ++_g; if(emoteCopies_h[emote.name]) { - haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 146, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 151, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.name] = true; if(!this.verbose) { continue; } if(emoteCopies_h[emote.image]) { - haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 149, className : "server.Main", methodName : "getUserConfig"}); + haxe_Log.trace("Warning: emote url of name \"" + emote.name + "\" has copy",{ fileName : "src/server/Main.hx", lineNumber : 154, className : "server.Main", methodName : "getUserConfig"}); } emoteCopies_h[emote.image] = true; } @@ -3478,13 +3822,11 @@ server_Main.prototype = { } ,writeUsers: function(users) { var folder = "" + this.rootDir + "/user"; - if(!sys_FileSystem.exists(folder)) { - sys_FileSystem.createDirectory(folder); - } + server_Utils.ensureDir(folder); js_node_Fs.writeFileSync("" + folder + "/users.json",JSON.stringify(users,null,"\t")); } ,saveState: function() { - haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 173, className : "server.Main", methodName : "saveState"}); + haxe_Log.trace("Saving state...",{ fileName : "src/server/Main.hx", lineNumber : 176, className : "server.Main", methodName : "saveState"}); var json = JSON.stringify({ videoList : this.videoList, isPlaylistOpen : this.isPlaylistOpen, itemPos : this.itemPos, messages : this.messages, timer : { time : this.videoTimer.getTime(), paused : this.videoTimer.isPaused()}},null,"\t"); js_node_Fs.writeFileSync(this.statePath,json); } @@ -3492,7 +3834,7 @@ server_Main.prototype = { if(!sys_FileSystem.exists(this.statePath)) { return; } - haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 190, className : "server.Main", methodName : "loadState"}); + haxe_Log.trace("Loading state...",{ fileName : "src/server/Main.hx", lineNumber : 193, className : "server.Main", methodName : "loadState"}); var data = JSON.parse(js_node_Fs.readFileSync(this.statePath,{ encoding : "utf8"})); this.videoList.length = 0; this.messages.length = 0; @@ -3509,11 +3851,9 @@ server_Main.prototype = { this.videoTimer.pause(); } ,logError: function(type,data) { - haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 204, className : "server.Main", methodName : "logError", customParams : [data]}); + haxe_Log.trace(type,{ fileName : "src/server/Main.hx", lineNumber : 207, className : "server.Main", methodName : "logError", customParams : [data]}); var crashesFolder = "" + this.rootDir + "/user/crashes"; - if(!sys_FileSystem.exists(crashesFolder)) { - sys_FileSystem.createDirectory(crashesFolder); - } + server_Utils.ensureDir(crashesFolder); js_node_Fs.writeFileSync("" + crashesFolder + "/" + (DateTools.format(new Date(),"%Y-%m-%d_%H_%M_%S") + "-" + type) + ".json",JSON.stringify(data,null,"\t")); } ,initIntergationHandlers: function() { @@ -3527,7 +3867,7 @@ server_Main.prototype = { if(_gthis.clients.length == 0) { return; } - haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 219, className : "server.Main", methodName : "initIntergationHandlers"}); + haxe_Log.trace("Ping " + url,{ fileName : "src/server/Main.hx", lineNumber : 222, className : "server.Main", methodName : "initIntergationHandlers"}); js_node_Http.get(url,null,function(r) { }); }; @@ -3541,62 +3881,68 @@ server_Main.prototype = { } this.userList.admins.push({ name : name, hash : hash}); this.writeUsers(this.userList); - haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 234, className : "server.Main", methodName : "addAdmin"}); + haxe_Log.trace("Admin " + name + " added.",{ fileName : "src/server/Main.hx", lineNumber : 237, className : "server.Main", methodName : "addAdmin"}); + } + ,replayLog: function(events) { + var _gthis = this; + var timer = new haxe_Timer(1000); + timer.run = function() { + if(events.length == 0) { + timer.stop(); + return; + } + var e = events.shift(); + switch(e.event.type) { + case "Connected": + if(ClientTools.getByName(_gthis.clients,e.clientName) == null) { + var id = _gthis.freeIds.length > 0 ? _gthis.freeIds.shift() : _gthis.clients.length; + _gthis.clients.push(new Client({ send : function() { + return; + }},null,id,e.clientName,e.clientGroup)); + } + _gthis.onMessage(ClientTools.getByName(_gthis.clients,e.clientName),e.event,true); + break; + case "Login": + var name = e.event.login.clientName; + if(e.event.login.passHash != null && !Lambda.exists(_gthis.userList.admins,function(a) { + return a.name == name; + })) { + e.event.login.passHash = null; + } + _gthis.onMessage(ClientTools.getByName(_gthis.clients,e.clientName),e.event,true); + break; + default: + _gthis.onMessage(ClientTools.getByName(_gthis.clients,e.clientName),e.event,true); + } + }; } ,onConnect: function(ws,req) { var _gthis = this; var ip = req.connection.remoteAddress; var id = this.freeIds.length > 0 ? this.freeIds.shift() : this.clients.length; var name = "Guest " + (id + 1); - haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 241, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace("" + name + " connected (" + ip + ")",{ fileName : "src/server/Main.hx", lineNumber : 274, className : "server.Main", methodName : "onConnect"}); var client = new Client(ws,req,id,name,0); client.setGroupFlag(ClientGroup.Admin,this.config.localAdmins && req.connection.localAddress == ip); this.clients.push(client); - if(this.clients.length == 1 && this.videoList.length > 0) { - if(this.videoTimer.isPaused()) { - this.videoTimer.play(); - } - } - var client1 = client; - var tmp = this.config; - var tmp1 = this.messages; - var client2 = client.name; - var _g = []; - var _g1 = 0; - var _g2 = this.clients; - while(_g1 < _g2.length) _g.push(_g2[_g1++].getData()); - this.send(client1,{ type : "Connected", connected : { config : tmp, history : tmp1, isUnknownClient : true, clientName : client2, clients : _g, videoList : this.videoList, isPlaylistOpen : this.isPlaylistOpen, itemPos : this.itemPos, globalIp : this.globalIp}}); - this.sendClientList(); + this.onMessage(client,{ type : "Connected"},true); ws.on("message",function(data) { var obj = _gthis.wsEventParser.fromJson(data); if(_gthis.wsEventParser.errors.length > 0) { var errors = "" + ("Wrong request for type \"" + obj.type + "\":") + "\n" + json2object_ErrorUtils.convertErrorArray(_gthis.wsEventParser.errors); - haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 273, className : "server.Main", methodName : "onConnect"}); + haxe_Log.trace(errors,{ fileName : "src/server/Main.hx", lineNumber : 289, className : "server.Main", methodName : "onConnect"}); _gthis.serverMessage(client,errors); return; } - _gthis.onMessage(client,obj); + _gthis.onMessage(client,obj,false); }); ws.on("close",function(err) { - haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 280, className : "server.Main", methodName : "onConnect"}); - server_Utils.sortedPush(_gthis.freeIds,client.id); - HxOverrides.remove(_gthis.clients,client); - _gthis.sendClientList(); - if((client.group & 2) != 0) { - if(_gthis.videoTimer.isPaused()) { - _gthis.videoTimer.play(); - } - } - if(_gthis.clients.length == 0) { - if(_gthis.waitVideoStart != null) { - _gthis.waitVideoStart.stop(); - } - _gthis.videoTimer.pause(); - } + _gthis.onMessage(client,{ type : "Disconnected"},true); }); } - ,onMessage: function(client,data) { + ,onMessage: function(client,data,internal) { var _gthis = this; + this.logger.log({ clientName : client.name, clientGroup : client.group, event : data, time : new Date().getTime()}); switch(data.type) { case "AddVideo": if(!this.checkPermission(client,"addVideo")) { @@ -3650,6 +3996,36 @@ server_Main.prototype = { this.broadcast(data); break; case "Connected": + if(!internal) { + return; + } + if(this.clients.length == 1 && this.videoList.length > 0) { + if(this.videoTimer.isPaused()) { + this.videoTimer.play(); + } + } + this.send(client,{ type : "Connected", connected : { config : this.config, history : this.messages, isUnknownClient : true, clientName : client.name, clients : this.clientList(), videoList : this.videoList, isPlaylistOpen : this.isPlaylistOpen, itemPos : this.itemPos, globalIp : this.globalIp}}); + this.sendClientListExcept(client); + break; + case "Disconnected": + if(!internal) { + return; + } + haxe_Log.trace("Client " + client.name + " disconnected",{ fileName : "src/server/Main.hx", lineNumber : 334, className : "server.Main", methodName : "onMessage"}); + server_Utils.sortedPush(this.freeIds,client.id); + HxOverrides.remove(this.clients,client); + this.sendClientList(); + if((client.group & 2) != 0) { + if(this.videoTimer.isPaused()) { + this.videoTimer.play(); + } + } + if(this.clients.length == 0) { + if(this.waitVideoStart != null) { + this.waitVideoStart.stop(); + } + this.videoTimer.pause(); + } break; case "GetTime": if(this.videoList.length == 0) { @@ -3712,7 +4088,7 @@ server_Main.prototype = { client.name = name; client.setGroupFlag(ClientGroup.User,true); this.send(client,{ type : data.type, login : { isUnknownClient : true, clientName : client.name, clients : this.clientList()}}); - this.sendClientList(); + this.sendClientListExcept(client); break; case "LoginError": break; @@ -3721,7 +4097,7 @@ server_Main.prototype = { client.name = "Guest " + (this.clients.indexOf(client) + 1); client.setGroupFlag(ClientGroup.User,false); this.send(client,{ type : data.type, logout : { oldClientName : oldName, clientName : client.name, clients : this.clientList()}}); - this.sendClientList(); + this.sendClientListExcept(client); break; case "Message": if(!this.checkPermission(client,"writeChat")) { @@ -3920,6 +4296,9 @@ server_Main.prototype = { ,sendClientList: function() { this.broadcast({ type : "UpdateClients", updateClients : { clients : this.clientList()}}); } + ,sendClientListExcept: function(skipped) { + this.broadcastExcept(skipped,{ type : "UpdateClients", updateClients : { clients : this.clientList()}}); + } ,serverMessage: function(client,textId) { this.send(client,{ type : "ServerMessage", serverMessage : { textId : textId}}); } @@ -4018,6 +4397,11 @@ server_Main.prototype = { }; var server_Utils = function() { }; server_Utils.__name__ = true; +server_Utils.ensureDir = function(path) { + if(!sys_FileSystem.exists(path)) { + sys_FileSystem.createDirectory(path); + } +}; server_Utils.getGlobalIp = function(callback) { js_node_Https.get("https://myexternalip.com/raw",function(r) { r.setEncoding("utf8"); @@ -4029,7 +4413,7 @@ server_Utils.getGlobalIp = function(callback) { callback(data_b); }); }).on("error",function(e) { - haxe_Log.trace("Warning: connection error, server is local.",{ fileName : "src/server/Utils.hx", lineNumber : 16, className : "server.Utils", methodName : "getGlobalIp"}); + haxe_Log.trace("Warning: connection error, server is local.",{ fileName : "src/server/Utils.hx", lineNumber : 21, className : "server.Utils", methodName : "getGlobalIp"}); callback("127.0.0.1"); }); }; diff --git a/res/client.js b/res/client.js index ad8506c..ab6760b 100644 --- a/res/client.js +++ b/res/client.js @@ -1169,6 +1169,8 @@ client_Main.prototype = { this.onConnected(data); this.onTimeGet.run(); break; + case "Disconnected": + break; case "GetTime": if(data.getTime.paused == null) { data.getTime.paused = false; diff --git a/src/Types.hx b/src/Types.hx index d16e58e..eb137da 100644 --- a/src/Types.hx +++ b/src/Types.hx @@ -171,6 +171,7 @@ typedef WsEvent = { enum abstract WsEventType(String) { var Connected; + var Disconnected; var Login; var PasswordRequest; var LoginError; diff --git a/src/client/Main.hx b/src/client/Main.hx index d0bc5eb..02d04ac 100644 --- a/src/client/Main.hx +++ b/src/client/Main.hx @@ -328,6 +328,7 @@ class Main { case Connected: onConnected(data); onTimeGet.run(); + case Disconnected: // server-only case Login: onLogin(data.login.clients, data.login.clientName); diff --git a/src/server/ConsoleInput.hx b/src/server/ConsoleInput.hx index 8b2463f..3c287af 100644 --- a/src/server/ConsoleInput.hx +++ b/src/server/ConsoleInput.hx @@ -1,20 +1,59 @@ package server; +import haxe.extern.EitherType as Or; +import haxe.io.Path; +import haxe.Json; +import sys.FileSystem; +import sys.io.File; import js.html.Console; import js.node.Readline; import js.Node.process; using StringTools; +private typedef CommandData = { + args:Array, + desc:String +} + +private enum abstract Command(String) from String { + var AddAdmin = "addAdmin"; + var Replay = "replay"; + var LogList = "logList"; + var Exit = "exit"; +} + class ConsoleInput { final main:Main; + final commands:Map = [ + AddAdmin => { + args: ["name", "password"], + desc: "Adds channel admin" + }, + Replay => { + args: ["name"], + desc: "Replay log file on server from user/logs/" + }, + LogList => { + args: [], + desc: "Show log list from user/logs/" + }, + Exit => { + args: [], + desc: "Exit process" + } + ]; public function new(main:Main) { this.main = main; } public function initConsoleInput():Void { - final rl = Readline.createInterface(process.stdin, process.stdout); + final rl = Readline.createInterface({ + input: process.stdin, + output: process.stdout, + completer: onCompletion + }); haxe.Log.trace = (msg, ?pos) -> { Readline.clearLine(process.stdout, 0); Readline.cursorTo(process.stdout, 0, null); @@ -29,31 +68,87 @@ class ConsoleInput { // rl.on("close", exit); } + function onCompletion(line:String):Array, String>> { + final commands:Array = [ + for (item in commands.keys()) '/$item ' + ]; + final matches = commands.filter(item -> item.startsWith(line)); + if (matches.length > 0) return [matches, line]; + return [commands, line]; + } + function parseLine(line:String):Void { - if (line.startsWith("/addAdmin")) { - final args = line.split(" "); - if (args.length != 3) { - trace("Wrong count of arguments"); - return; - } - final name = args[1]; - final password = args[2]; - if (main.badNickName(name)) { - final error = Lang.get("usernameError") - .replace("$MAX", '${main.config.maxLoginLength}'); - trace(error); - return; - } - main.addAdmin(name, password); - - } else if (line == "/exit") { - main.exit(); + if (line.fastCodeAt(0) != "/".code || line.length < 2) { + printHelp(line); return; - } else { - trace('Unknown command "$line". List: -/addAdmin name password | Adds channel admin -/exit | Exit process'); } + final args = line.trim().split(" "); + final command:Command = args.shift().substr(1); + if (commands[command] == null) { + printHelp(line); + return; + } + if (!isValidArgs(command, args)) return; + + switch (command) { + case AddAdmin: + final name = args[0]; + final password = args[1]; + if (main.badNickName(name)) { + final error = Lang.get("usernameError") + .replace("$MAX", '${main.config.maxLoginLength}'); + trace(error); + return; + } + main.addAdmin(name, password); + + case Replay: + Utils.ensureDir(main.logsDir); + final name = args[0]; + final path = Path.normalize('${main.logsDir}/$name.json'); + if (!FileSystem.exists(path)) { + trace('File "$path" not found'); + return; + } + final text = File.getContent(path); + final events:Array = Json.parse(text); + main.replayLog(events); + + case LogList: + Utils.ensureDir(main.logsDir); + final names = FileSystem.readDirectory(main.logsDir) + .filter(s -> s.endsWith(".json")); + for (name in names) trace(Path.withoutExtension(name)); + + case Exit: + main.exit(); + } + } + + function isValidArgs(command:Command, args:Array):Bool { + final len = args.length; + final actual = commands[command].args.length; + if (len != actual) { + trace('Wrong count of arguments for command "$command" ($len instead of $actual)'); + return false; + } + return true; + } + + function printHelp(line:String):Void { + var maxLength = 0; + for (name => data in commands) { + final len = '/$name ${data.args.join(" ")}'.length; + if (maxLength < len) maxLength = len; + } + final list:Array = []; + for (name => data in commands) { + final args = data.args.join(" "); + final item = '/$name $args'.rpad(" ", maxLength); + list.push('$item | ${data.desc}'); + } + final desc = list.join("\n"); + trace('Unknown command "$line". List:\n$desc'); } } diff --git a/src/server/Logger.hx b/src/server/Logger.hx new file mode 100644 index 0000000..9ff4c34 --- /dev/null +++ b/src/server/Logger.hx @@ -0,0 +1,69 @@ +package server; + +import haxe.io.Path; +import sys.io.File; +import haxe.Json; +import sys.FileSystem; +using StringTools; +using Lambda; + +class Logger { + + final folder:String; + final maxCount:Int; + final verbose:Bool; + final logs:Array = []; + final matchFileFormat = ~/[0-9_-]+\.json$/; + + public function new(folder:String, maxCount:Int, verbose:Bool):Void { + this.folder = folder; + this.maxCount = maxCount; + this.verbose = verbose; + } + + public function log(event:ServerEvent):Void { + logs.push(event); + if (logs.length > 5000) logs.shift(); + } + + public function saveLog():Void { + if (logs.length == 0) return; + Utils.ensureDir(folder); + removeOldestLog(folder); + final name = DateTools.format(Date.now(), "%Y-%m-%d_%H_%M_%S"); + File.saveContent('$folder/$name.json', Json.stringify(logs, filterNulls, "\t")); + } + + function filterNulls(key:Any, value:Any):Any { + #if js + if (value == null) return js.Lib.undefined; + #end + return value; + } + + function removeOldestLog(folder:String):Void { + final names = FileSystem.readDirectory(folder); + if (names.count(item -> matchFileFormat.match(item)) < maxCount) return; + var minDate = 0.0; + var fileName:String = null; + for (name in names) { + final date = extractFileDate(name).getTime(); + if (minDate == 0 || minDate > date) { + minDate = date; + fileName = name; + } + } + if (fileName == null) return; + FileSystem.deleteFile('$folder/$fileName'); + } + + function extractFileDate(name:String):Date { + name = Path.withoutExtension(name); + final t = name.split("_"); + final d = t.shift().split("-"); + if (d.length != 3 && t.length != 3) return Date.fromTime(0); + final s = '${d[0]}-${d[1]}-${d[2]} ${t[0]}:${t[1]}:${t[2]}'; + return Date.fromString(s); + } + +} diff --git a/src/server/Main.hx b/src/server/Main.hx index 3c85c3e..6c7753c 100644 --- a/src/server/Main.hx +++ b/src/server/Main.hx @@ -28,6 +28,7 @@ class Main { static inline var VIDEO_START_MAX_DELAY = 3000; static inline var VIDEO_SKIP_DELAY = 1000; final rootDir = '$__dirname/..'; + public final logsDir:String; final verbose:Bool; final statePath:String; final wss:WSServer; @@ -43,6 +44,7 @@ class Main { final videoList = new VideoList(); final videoTimer = new VideoTimer(); final messages:Array = []; + final logger:Logger; var isPlaylistOpen = true; var itemPos = 0; @@ -51,6 +53,7 @@ class Main { function new() { verbose = Sys.args().has("--verbose"); statePath = '$rootDir/user/state.json'; + logsDir = '$rootDir/user/logs'; // process.on("exit", exit); process.on("SIGINT", exit); // ctrl+c process.on("SIGUSR1", exit); // kill pid @@ -67,6 +70,7 @@ class Main { logError("unhandledRejection", reason); exit(); }); + logger = new Logger(logsDir, 10, verbose); consoleInput = new ConsoleInput(this); consoleInput.initConsoleInput(); initIntergationHandlers(); @@ -101,6 +105,7 @@ class Main { public function exit():Void { saveState(); + logger.saveLog(); if (wss == null) { process.exit(); return; @@ -162,9 +167,7 @@ class Main { function writeUsers(users:UserList):Void { final folder = '$rootDir/user'; - if (!FileSystem.exists(folder)) { - FileSystem.createDirectory(folder); - } + Utils.ensureDir(folder); final data = Json.stringify(users, "\t"); File.saveContent('$folder/users.json', data); } @@ -203,7 +206,7 @@ class Main { function logError(type:String, data:Dynamic):Void { trace(type, data); final crashesFolder = '$rootDir/user/crashes'; - if (!FileSystem.exists(crashesFolder)) FileSystem.createDirectory(crashesFolder); + Utils.ensureDir(crashesFolder); final name = DateTools.format(Date.now(), "%Y-%m-%d_%H_%M_%S") + "-" + type; File.saveContent('$crashesFolder/$name.json', Json.stringify(data, "\t")); } @@ -234,6 +237,36 @@ class Main { trace('Admin $name added.'); } + public function replayLog(events:Array):Void { + final timer = new Timer(1000); + timer.run = () -> { + if (events.length == 0) { + timer.stop(); + return; + } + final e = events.shift(); + switch (e.event.type) { + case Connected: + if (clients.getByName(e.clientName) == null) { + final ws:Any = {send: () -> {}}; + final id = freeIds.length > 0 ? freeIds.shift() : clients.length; + final client = new Client(ws, null, id, e.clientName, e.clientGroup); + clients.push(client); + } + onMessage(clients.getByName(e.clientName), e.event, true); + case Login: + final name = e.event.login.clientName; + final hash = e.event.login.passHash; + if (hash != null && !userList.admins.exists(a -> a.name == name)) { + e.event.login.passHash = null; + } + onMessage(clients.getByName(e.clientName), e.event, true); + default: + onMessage(clients.getByName(e.clientName), e.event, true); + } + } + } + function onConnect(ws:WebSocket, req:IncomingMessage):Void { final ip = req.connection.remoteAddress; final id = freeIds.length > 0 ? freeIds.shift() : clients.length; @@ -243,26 +276,9 @@ class Main { final client = new Client(ws, req, id, name, 0); client.isAdmin = isAdmin; clients.push(client); - if (clients.length == 1 && videoList.length > 0) - if (videoTimer.isPaused()) videoTimer.play(); - - send(client, { - type: Connected, - connected: { - config: config, - history: messages, - isUnknownClient: true, - clientName: client.name, - clients: [ - for (client in clients) client.getData() - ], - videoList: videoList, - isPlaylistOpen: isPlaylistOpen, - itemPos: itemPos, - globalIp: globalIp - } - }); - sendClientList(); + onMessage(client, { + type: Connected + }, true); ws.on("message", data -> { final obj = wsEventParser.fromJson(data); @@ -274,26 +290,59 @@ class Main { serverMessage(client, errors); return; } - onMessage(client, obj); + onMessage(client, obj, false); }); + ws.on("close", err -> { - trace('Client ${client.name} disconnected'); - Utils.sortedPush(freeIds, client.id); - clients.remove(client); - sendClientList(); - if (client.isLeader) { - if (videoTimer.isPaused()) videoTimer.play(); - } - if (clients.length == 0) { - if (waitVideoStart != null) waitVideoStart.stop(); - videoTimer.pause(); - } + onMessage(client, { + type: Disconnected + }, true); }); } - function onMessage(client:Client, data:WsEvent):Void { + function onMessage(client:Client, data:WsEvent, internal:Bool):Void { + logger.log({ + clientName: client.name, + clientGroup: client.group.toInt(), + event: data, + time: Date.now().getTime() + }); switch (data.type) { case Connected: + if (!internal) return; + if (clients.length == 1 && videoList.length > 0) + if (videoTimer.isPaused()) videoTimer.play(); + + send(client, { + type: Connected, + connected: { + config: config, + history: messages, + isUnknownClient: true, + clientName: client.name, + clients: clientList(), + videoList: videoList, + isPlaylistOpen: isPlaylistOpen, + itemPos: itemPos, + globalIp: globalIp + } + }); + sendClientListExcept(client); + + case Disconnected: + if (!internal) return; + trace('Client ${client.name} disconnected'); + Utils.sortedPush(freeIds, client.id); + clients.remove(client); + sendClientList(); + if (client.isLeader) { + if (videoTimer.isPaused()) videoTimer.play(); + } + if (clients.length == 0) { + if (waitVideoStart != null) waitVideoStart.stop(); + videoTimer.pause(); + } + case UpdateClients: sendClientList(); case Login: @@ -330,7 +379,7 @@ class Main { clients: clientList() } }); - sendClientList(); + sendClientListExcept(client); case PasswordRequest: case LoginError: @@ -347,7 +396,7 @@ class Main { clients: clientList() } }); - sendClientList(); + sendClientListExcept(client); case Message: if (!checkPermission(client, WriteChatPerm)) return; @@ -582,6 +631,15 @@ class Main { }); } + function sendClientListExcept(skipped:Client):Void { + broadcastExcept(skipped, { + type: UpdateClients, + updateClients: { + clients: clientList() + } + }); + } + function serverMessage(client:Client, textId:String):Void { send(client, { type: ServerMessage, serverMessage: { diff --git a/src/server/ServerEvent.hx b/src/server/ServerEvent.hx new file mode 100644 index 0000000..6b9080f --- /dev/null +++ b/src/server/ServerEvent.hx @@ -0,0 +1,10 @@ +package server; + +import Types.WsEvent; + +typedef ServerEvent = { + time:Float, + clientName:String, + clientGroup:Int, + event:WsEvent +} diff --git a/src/server/Utils.hx b/src/server/Utils.hx index abce1c0..40fa282 100644 --- a/src/server/Utils.hx +++ b/src/server/Utils.hx @@ -1,10 +1,15 @@ package server; +import sys.FileSystem; import js.node.Https; import js.node.Os; class Utils { + public static function ensureDir(path:String):Void { + if (!FileSystem.exists(path)) FileSystem.createDirectory(path); + } + public static function getGlobalIp(callback:(ip:String)->Void):Void { // untyped to skip second null argument for node < v10 Https.get(untyped "https://myexternalip.com/raw", r -> { diff --git a/user/README.md b/user/README.md index 1dff0d8..59ccb4b 100644 --- a/user/README.md +++ b/user/README.md @@ -19,5 +19,5 @@ Server has input commands, for example, to set admin users. Simple enter anythin ## Other files here - `state.json` - Saved state of latest server session (messages, videos, video time). - `users.json` - Admin names with password hashes and random channel-specific salt. Do not share this file! -- `crashes/` - Latest error logs, when the server had to restart itself. -- `logs/` - (TODO) Latest activity logs. +- `crashes/` - Latest error logs, when the server crashes. +- `logs/` - Latest activity logs, saved when the server shuts down. -- cgit v1.2.3