#!/usr/bin/python3 # -*- coding: utf-8 -*- # # script to start/stop/status/send command/read log for khaganat process # # Copyright (C) 2017 AleaJactaEst # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Configuration File ------------------ This script need configuration file (see below for model):: [config:server] # Define port listen (default 8000) port = 8000 # Example to generate all key : see pycertificate # key keyfile = /home/gameserver/ca/appli/private/serverkey.pem # certificate certfile = /home/gameserver/ca/appli/certs/servercert.pem # certification to check signature ca_cert = /home/gameserver/ca/appli/certs/cachaincert.pem # address listen (default all port) address = # method : http or https method = https # Admin Executor Service [command:aes] # command to launch the program command = ryzom_admin_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/log/khanat --nobreak --fulladminname=admin_executor_service --shortadminname=AES # Path : where this program is launched path = /home/gameserver/khanat/server/ # size buffer log for each program launched (number line stdout) logsize = 1000 # buffer size (define value bufsize on subprocess.Popen, this buffer is use before read by manager) bufsize = 100 # bms_master : backup_service [command:bms_master] # command to launch the program command = ryzom_backup_service -A/home/gameserver/khanat/server -C/home/gameserver/khanat/server -L/home/gameserver/khanat/server/log --nobreak --writepid -P49990 # Path : where this program is launched path = /home/gameserver/khanat/server/ # we keep [logsize] last number line stdout logsize = 1000 # buffer size (define value bufsize on subprocess.Popen) bufsize = 100 # It's possible to collect some message on output (example player conected) with regex command # keep some data on array/dict state activate_filter = yes # size array/dict state size_max_filter = 1000 # search regex to add state (python regex) add_filter = "^((.*)(setActiveCharForPlayer).*(: set active char )[0-9]+( for )(?P.*)|(.*)(disconnectPlayer)(.+:.+<.+>){0,1}[ ]+(?P.*)[ ]+(is disconnected))" del_filter = "^((.*)(setActiveCharForPlayer).*(: set active char )[0-9]+( for )(?P.*)|(.*)(disconnectPlayer)(.+:.+<.+>){0,1}[ ]+(?P.*)[ ]+(is disconnected))" # autostart (when start OpenNelManager, launch this program) autostart = no # restart after crash restart_after_crash = yes # Delay after each restart (second) restart_delay = 10 # Enable special filter EGS (account connection / command admin) egs_filter = yes Manager ------- Manage all process khaganat Launch this prorgam in background and use clientManager to manipulate process Design .. graphviz:: digraph Manager { "Manager" -> "ManageCommand (command 1)"; "ManageCommand (command 1)" -> "read_output (thread1)"; "Manager" -> "ManageCommand (command 2)"; "ManageCommand (command 2)" -> "read_output (thread2)"; "Manager" -> "ServerHttp"; "ServerHttp" -> "khaganatHTTPServer"; "khaganatHTTPServer" -> "ManageHttpRequest"; "ManageHttpRequest" -> "ManageCommand (command 1)" [style=dashed]; "ManageHttpRequest" -> "ManageCommand (command 2)" [style=dashed]; } http(s) command : ----------------- +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **Html command** | **Path** | **Argument** {json format} | **Comment** | **Return*** | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /SHUTDOWN | | Stop all process and stop pymanager | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /STARTALL | | Start all processes | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /STATUSALL | | Get status all processes | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /STOPALL | | Stop all processes | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /START | {'name': program} | Start for one program | {'state': ''} | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /STDIN | {'name': program, 'action': action} | Send action for one program (send to input) | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /STATUS | {'name': program} | Get status for one program | {'state': ''} | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **POST** | /STOP | {'name': program} | Stop for one program | {'state': ''} | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /STDOUT | {'name': program, 'first-line': firstline } | Get log for one program | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /FILTER | {'name': program } | Get all state (key find in stdout add/remove) | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /CONFIG | {'name': program } | Get configuration | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /INFO | {'name': program } | Get Information (number player, ...) | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /PLAYER | {'name': program } | Get active player | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ | **GET** | /ADMINCOMMAND | {'name': program } | Get admin commmand | | +------------------+------------------+---------------------------------------------+-----------------------------------------------+------------------------------+ Example :: nohup pymanager --log info --filelog /home/gameserver/log/manager.log -c /home/gameserver/cfg/khaganat.cfg 2>/dev/null 1>/dev/null 0 %s" % (command, name, outjson)) self._set_headers() self.wfile.write(bytes(json.dumps(outjson), "utf-8")) def check_authentication(self): if not self.server.authentification: return True if __DISABLE_BCRYPT__: logging.error("Error module python bcrypt not installed") return False try: auth_header = self.headers['Authorization'].split() if auth_header[0] != 'Basic': logging.error("Authentification with Bad method (%s)" % auth_header[0]) return False decode = base64.b64decode(auth_header[1]).decode('UTF-8') account, password = decode.split(':', maxsplit=1) if account not in self.server.users: logging.error("Authentification with unknown user (%s)" % account) return False hashed_password = self.server.users[account] if bcrypt.checkpw(password.encode('utf-8'), hashed_password): return True else: logging.error("Authentification with wrong password for user (%s)" % account) return False except (ValueError, IndexError, AttributeError) as e: logging.error("Error detected %s" % e) return False def do_GET(self): """ Manage request READ we can execute LOG, STATUS, LIST & STATUSALL """ logging.debug('get recieved : %s' % self.path) if not self.check_authentication(): self.send_error(403, 'Wrong authentication') logging.error("Wrong authentication") elif self.path == '/STDOUT': self._command_log() elif self.path == "/FILTER": self._send_command("FILTER") elif self.path == "/INFO": self._send_command("INFO") elif self.path == "/PLAYER": self._send_command("PLAYER") elif self.path == "/ADMINCOMMAND": self._send_command("ADMINCOMMAND") elif self.path == "/CONFIG": self._send_command("CONFIG") elif self.path == '/STATUS': self._send_command("STATUS") elif self.path == '/LIST': self._send_list() elif self.path == '/STATUSALL': self._send_command_all("STATUS") else: self.send_error(400, 'Path unknown') logging.error("Path unknwon '%s'" % self.path) def do_POST(self): """ Manage request POST (CREATE) currently, we execute START, STOP, ACTION, SHUTDOWN, STARTALL & STOPALL """ logging.debug('post recieved : %s' % self.path) if not self.check_authentication(): self.send_error(403, 'Wrong authentication') logging.error("Wrong authentication") elif self.path == '/START': self._send_command("START") elif self.path == '/STOP': self._send_command("STOP") elif self.path == '/STDIN': self._send_action() elif self.path == '/SHUTDOWN': self._send_shutdown() elif self.path == '/STARTALL': self._send_command_all("START") elif self.path == '/STOPALL': self._send_command_all("STOP") else: self.send_error(400, 'Path unknown') logging.error("Path unknwon '%s'" % self.path) def do_HEAD(self): """ request HEAD received """ logging.debug('head recieved : %s' % self.path) self.send_error(404, 'File Not Found: %s' % self.path) def do_PUT(self): """ request PUT (UPDATE/REPLACE) received """ logging.debug('put recieved!') self.send_error(404, 'File Not Found: %s' % self.path) def do_PATCH(self): """ request PATCH (UPDATE/MODIFY) received """ logging.debug('patch recieved!') self.send_error(404, 'File Not Found: %s' % self.path) def do_DELETE(self): """ request DELETE received """ logging.debug('delete recieved!') self.send_error(404, 'File Not Found: %s' % self.path) def do_OPTIONS(self): """ request OPTIONS received """ self.send_response(200, "ok") self.send_header('Access-Control-Allow-Origin', '*') self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') self.send_header("Access-Control-Allow-Headers", "Content-Type, *") self.end_headers() class khaganatHTTPServer(ThreadingMixIn, http.server.HTTPServer): """ Class khaganatHTTPServer Redefine HTTPServer (adding queue input & queue output, use by ManageHttpRequest) """ def __init__(self, listQueueIn, listQueueOut, listSemaphore, server_address, RequestHandlerClass, authentification, users, bind_and_activate=True): http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass, bind_and_activate) self.listQueueIn = listQueueIn self.listQueueOut = listQueueOut self.listSemaphore = listSemaphore self.authentification = authentification self.users = users class ServerHttp(multiprocessing.Process): """ Initialize server HTTPS * define Dictionnary queueIn & queueOut (with key as section's name in configuration) """ def __init__(self, keyfile, certfile, ca_cert, address='', port=8000, authentification=True, method='http', users={}): multiprocessing.Process.__init__(self) self.listQueueIn = {} self.listQueueOut = {} self.listSemaphore = {} self.port = port self.key_file = keyfile self.cert_file = certfile self.ca_cert = ca_cert self.address = address self.authentification = authentification self.users = users self.method = method def run(self): server_address = (self.address, self.port) httpd = khaganatHTTPServer(self.listQueueIn, self.listQueueOut, self.listSemaphore, server_address, ManageHttpRequest, self.authentification, self.users) if self.method == 'http': logging.info('http listen') elif self.method == 'https': if self.ca_cert: httpd.socket = ssl.wrap_socket(httpd.socket, keyfile=self.key_file, certfile=self.cert_file, ca_certs=self.ca_cert, cert_reqs=ssl.CERT_REQUIRED, ssl_version=ssl.PROTOCOL_TLSv1_2, server_side=True) else: httpd.socket = ssl.wrap_socket(httpd.socket, keyfile=self.key_file, certfile=self.cert_file, server_side=True) logging.info('https listen') else: logging.error("Bad value 'method' (%s)" % str(self.method)) raise ValueError httpd.serve_forever() logging.info("End") def appendchild(self, name, queueIn, queueOut, semaphore): self.listQueueIn.setdefault(name, queueIn) self.listQueueOut.setdefault(name, queueOut) self.listSemaphore.setdefault(name, semaphore) class ManageCommand(): """ Manage Command (only one) * start/stop/status/get log/send an action [stdin] for command (receive order with queueIn) * read output [in other thread] * communicate with ManageHttpRequest (with queueOut) """ def __init__(self, name, command, path, logsize, bufsize, queueIn, queueOut, semaphore, activate_filter, size_max_filter, add_filter, del_filter, autostart, restart_after_crash, restart_delay, egs_filter, maxWaitEnd=10, waitDelay=1): self.process = None self.queueIn = queueIn self.queueOut = queueOut self.name = name self.command = command self.path = path self.log = [] self.poslastlog = 0 self.maxlog = logsize self.semaphore = semaphore self.bufsize = bufsize self.threadRead = None self.running = False # self.state = multiprocessing.Queue() self.pipeIn, self.pipeOut = multiprocessing.Pipe() self.eventRunningReader = threading.Event() self.eventRunningRestart = threading.Event() self.maxWaitEnd = maxWaitEnd self.waitDelay = waitDelay self.activate_filter = activate_filter self.size_max_filter = size_max_filter self.add_filter_cmd = add_filter[1:-1] self.del_filter_cmd = del_filter[1:-1] self.filter_add_filter = re.compile(self.add_filter_cmd) self.filter_del_filter = re.compile(self.del_filter_cmd) self.filter = {} self.autostart = autostart self.restart_after_crash = restart_after_crash self.restart_delay = restart_delay self.threadRestart = None self.egs_filter = egs_filter self.egs_filter_load_character = re.compile(r".*(egs_plinfo).*(: LOADED User )'(?P[\d]+)' Character '(?P[^']+)' from BS stream file 'characters/([\d]+)/account_(?P[\d]+)_(?P[\d]+)_pdr.bin") self.egs_filter_active_character = re.compile(r".*(setActiveCharForPlayer).*(: set active char )(?P[\d]+)( for player )(?P[\d]+)") self.egs_filter_sid = re.compile(r".*(Mapping UID )(?P[\d]+)( => Sid )[(](?P.*)[)]") self.egs_filter_client_ready = re.compile(r".*(Updating IS_NEWBIE flag for character: )[(](?P.*)[)]") self.egs_filter_disconnected = re.compile(r".*(disconnectPlayer).+[\s]+(player )(?P[\d]+)[\s]+(is disconnected)") self.egs_filter_admin = re.compile(r"(.*)(cbClientAdmin).*(: ADMIN)(: Player )[(](?P.*)[)](?P.+)") # cbClientAdmin EGS-133 : ADMIN: Player (0x0000000021:00:00:86) tried to execute a no valid client admin command 'info' self.filter_load_character = {} self.filter_active_character = {} self.filter_admin = {} self.number_start = 0 self.first_line = 0 self.last_line = 0 self.pos_admin = 0 def _analyze_line(self, msg): now = time.strftime('%Y/%m/%d %H:%M:%S %Z') self.poslastlog += 1 while len(self.log) >= self.maxlog: self.log.pop(0) self.first_line = self.first_line + 1 self.log.append(now + ' ' + msg) self.last_line = self.last_line + 1 # If option sate is defined, analyze message and keep state (example , all player connected) logging.debug("recu: '%s'" % (msg)) if self.activate_filter: res = self.filter_add_filter.match(msg) if res: logging.debug("add_filter found") if len(self.filter) < self.size_max_filter: logging.debug("include add_filter found") dico = res.groupdict() for key in dico: logging.debug("set add_filter found [%s]" % (str(key))) if dico[key]: logging.debug("set1 add_filter found [%s][%s]" % (str(key), str(dico[key]))) self.filter.setdefault(key, {}) self.filter[key].setdefault(dico[key], now) res = self.filter_del_filter.match(msg) if res: logging.debug("del_filter found") dico = res.groupdict() for key in dico: logging.debug("prepare del_filter found %s" % str(key)) if dico[key]: self.filter.setdefault(key, {}) if dico[key] in self.filter[key]: logging.debug("del1 del_filter found [%s][%s][%s]" % (str(key), str(dico[key]), str(self.filter[key]))) del self.filter[key][dico[key]] if self.egs_filter: res = self.egs_filter_load_character.match(msg) if res: logging.debug("egs_filter_load_character found") if len(self.filter_load_character) < self.size_max_filter: logging.debug("include add_filter found") dico = res.groupdict() try: self.filter_load_character.setdefault(dico['UID'], {}) self.filter_load_character[dico['UID']].setdefault(dico['IDCHAR'], {'NameDomain': dico['NameDomain'], 'UID': dico['UIDBIS'], 'when': now}) except KeyError as e: logging.error('Missing key when read "load_character" (%s)' % e) else: logging.warning("impossible to add param 'load_character' (size too high)") return res = self.egs_filter_active_character.match(msg) if res: logging.debug("egs_filter_active_character found") if len(self.filter_active_character) < self.size_max_filter: dico = res.groupdict() try: self.filter_active_character.setdefault(dico['UID'], {}) self.filter_active_character[dico['UID']] = self.filter_load_character[dico['UID']][dico['IDCHAR']] del self.filter_load_character[dico['UID']] except KeyError as e: logging.error('Missing key when read "active_character" (%s)' % e) else: logging.warning("impossible to add param 'active_character' (size too high)") return res = self.egs_filter_sid.match(msg) if res: logging.debug("egs_filter_sid found") dico = res.groupdict() try: if dico['UID'] in self.filter_active_character: self.filter_active_character[dico['UID']].setdefault("SID", dico['SID']) else: logging.error('Impossible to add SID on player %s (Player not found)' % dico['UID']) except KeyError as e: logging.error('Missing key when read "sid" (%s)' % e) return res = self.egs_filter_disconnected.match(msg) if res: logging.debug("egs_filter_sid found") dico = res.groupdict() try: if dico['UID'] in self.filter_active_character: del self.filter_active_character[dico['UID']] else: logging.error('Impossible to remove player %s (Player not found)' % dico['UID']) except KeyError as e: logging.error('Missing key when remove player (%s)' % e) return res = self.egs_filter_admin.match(msg) if res: logging.debug("egs_filter_admin found") while len(self.filter_admin) >= self.maxlog: print(self.pos_admin, self.pos_admin - self.maxlog) del self.filter_admin[self.pos_admin - self.maxlog] try: dico = res.groupdict() username = '' try: for key in self.filter_active_character: if self.filter_active_character[key]['SID'] == dico['SID']: username = self.filter_active_character[key]['NameDomain'] break except KeyError: pass self.filter_admin.setdefault(self.pos_admin, {'when': now, 'SID': dico['SID'], 'ACTION': dico['ACTION'], 'USER': username}) except KeyError as e: logging.error('Missing key when admin player (%s)' % e) self.pos_admin = self.pos_admin + 1 return def _readline_stdout(self): try: line = self.process.stdout.readline() except AttributeError: logging.error("process %s down (not detected)" % self.name) return True, False except ValueError: logging.error("process %s down (not detected)" % self.name) return True, False if not line: time.sleep(self.waitDelay) return False, True logging.debug("line %s " % line) self._analyze_line(line.decode().strip()) return False, False def read_output(self): """ Thread to read output (stdout) """ fl = fcntl.fcntl(self.process.stdout, fcntl.F_GETFL) fcntl.fcntl(self.process.stdout, fcntl.F_SETFL, fl | os.O_NONBLOCK) logging.debug("Start reader %s" % self.name) crashed = False while self.eventRunningReader.is_set(): try: logging.debug("ping") code = self.process.poll() if code is not None: logging.error("process %s down" % self.name) crashed = True except AttributeError as e: logging.warning("process %s down (%s)" % (self.name, e)) break crashedbis, end = self._readline_stdout() if end and (crashed or crashedbis): break # Send to thread manage process if crashed: logging.debug("Process stopped : '%s'" % self.name) wait_semaphore = self.semaphore.acquire(False) while self.eventRunningReader.is_set() and not wait_semaphore: time.sleep(1) wait_semaphore = self.semaphore.acquire(False) if wait_semaphore: self.queueIn.put("STOPPED") self.semaphore.release() if self.activate_filter: self.filter_load_character = {} self.filter_active_character = {} logging.debug("End reader: '%s'" % self.name) def restart(self): """ Thread to restart after crash """ logging.debug('initialize process restart %s (wait %ds)' % (self.name, self.restart_delay)) time.sleep(self.restart_delay) logging.debug('Prepare restart service %s' % (self.name)) wait_semaphore = self.semaphore.acquire(False) while self.eventRunningRestart.is_set() and not wait_semaphore: logging.debug('Ping - restart service %s' % (self.name)) time.sleep(1) wait_semaphore = self.semaphore.acquire(False) logging.debug('Prepare restart service %s (step 2)' % (self.name)) if wait_semaphore: logging.debug('Restart service %s' % (self.name)) self.queueIn.put("START") self.queueOut.get() self.semaphore.release() logging.debug('Prepare restart service %s (step 3)' % (self.name)) def receive_signal(self, signum, frame): """ Managed signal (not used) """ logging.info("Received signal %s (%d)" % (self.name, signum)) self.queueIn.put("SHUTDOWN") self.queueIn.put("SHUTDOWN") def start(self): """ Start program """ logging.debug("start %s" % (self.name)) if self.process: logging.debug("%s already exist" % self.name) code = self.process.poll() if code is None: logging.debug("%s already exist" % self.name) return 0 else: logging.debug("%s crashed" % self.name) code = self.process.wait() self.process.stdin.close() self.process.stdout.close() logging.error("%s crashed (return code:%d) - restart program" % (self.name, code)) try: self.process = subprocess.Popen(self.command.split(), cwd=self.path, shell=False, bufsize=self.bufsize, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True) except FileNotFoundError as e: logging.error("Impossible to start %s (%s)" % (self.name, e)) return 2 except PermissionError as e: logging.error("Impossible to start %s (%s)" % (self.name, e)) return 2 if self.threadRead: self.eventRunningReader.clear() self.threadRead.join() self.threadRead = None self.running = True self.eventRunningReader.set() self.threadRead = threading.Thread(target=self.read_output) self.threadRead.start() tmp = self.number_start tmp = tmp + 1 if tmp > self.number_start: self.number_start = tmp return 0 def status(self): """ Get status of program """ logging.debug("status %s" % (self.name)) if self.process: logging.debug("status %s - check" % (self.name)) code = self.process.poll() if code is None: logging.debug("%s status [started]" % (self.name)) return 0 else: logging.error("%s crashed (return code:%d)" % (self.name, code)) # self.queueIn.put("STOPPED") return 2 else: logging.debug("%s status [stopped]" % (self.name)) return 1 def list_thread(self): """ List number thrad (not used) """ logging.debug('list thread') # main_thread = threading.currentThread() for t in threading.enumerate(): logging.debug('thread %s', t.getName()) logging.debug("id %d" % t.ident) def stop(self): """ Stop program """ logging.debug("stop %s" % (self.name)) if not self.process: return 1 else: try: code = self.process.poll() loop = self.maxWaitEnd while (code is None) and (loop > 0): logging.debug("stop process %s", self.name) self.process.send_signal(15) time.sleep(1) code = self.process.poll() loop -= 1 except ProcessLookupError as e: logging.warning("Stop process (%s)" % str(e)) try: loop = self.maxWaitEnd while (code is None) and (loop > 0): logging.debug("terminate process %s", self.name) self.process.terminate() time.sleep(1) code = self.process.poll() loop -= 1 except ProcessLookupError as e: logging.warning("Stop process (%s)" % str(e)) try: loop = self.maxWaitEnd while (code is None) and (loop > 0): logging.debug("kill process %s", self.name) self.process.send_signal(9) time.sleep(1) code = self.process.poll() loop -= 1 except ProcessLookupError as e: logging.warning("Stop process (%s)" % str(e)) try: code = self.process.wait() self.process.stdin.close() self.process.stdout.close() self.process = None if self.threadRead: self.eventRunningReader.clear() self.threadRead.join() self.threadRead = None logging.info("%s stopped (return code:%d)" % (self.name, code)) except ProcessLookupError as e: logging.warning("Stop process (%s)" % str(e)) return 1 def getlog(self, firstline): """ Get log """ logging.debug("read log %d " % firstline) outjson = {} pos = self.poslastlog - len(self.log) + 1 firstlinefound = 0 for line in self.log: if pos >= firstline: outjson.setdefault(pos, line) if not firstlinefound: firstlinefound = pos pos += 1 outjson.setdefault('first-line', firstlinefound) outjson.setdefault('last-line', pos - 1) return outjson def getfilter(self): """ Get filter """ return self.filter def getconfig(self): outjson = {'activate_filter': str(self.activate_filter), 'bufsize': str(self.bufsize), 'size_max_filter': str(self.size_max_filter), 'path': str(self.path), 'add_filter': str(self.add_filter_cmd), 'del_filter': str(self.del_filter_cmd), 'command': str(self.command), 'maxWaitEnd': str(self.maxWaitEnd), 'waitDelay': str(self.waitDelay), 'maxlog': str(self.maxlog), 'filter': str(self.activate_filter), 'egs': str(self.egs_filter)} return outjson def getinfo(self): outjson = {'number_launch': str(self.number_start), 'first_line': str(self.first_line), 'last_line': str(self.last_line), 'number_filter': len(self.filter), 'player_connected': len(self.filter_active_character)} return outjson def getplayer(self): return self.filter_active_character def getadmincommand(self): return self.filter_admin def action(self, action): """ Send action to program (send input to stdin) """ logging.debug("STDIN '%s'" % action) if self.process: code = self.process.poll() if code is None: if action: self.process.stdin.write(bytes(action+'\n', 'UTF-8')) self.process.stdin.flush() return "ok" return "ko" def run(self): """ loop, run child (wait command) """ signal.signal(signal.SIGABRT, self.receive_signal) signal.signal(signal.SIGTERM, self.receive_signal) statuscmd = {0: 'started', 1: 'stopped', 2: 'crashed'} loop = True if self.autostart: savedstate = self.start() else: savedstate = 1 while loop: logging.debug('wait event %s' % self.name) msg = self.queueIn.get() logging.debug("command : '%s'" % msg) command = msg.split()[0] if command == "SHUTDOWN": loop = False continue elif command == "START": savedstate = self.start() self.queueOut.put({'state': statuscmd[savedstate]}) elif command == "STATUS": currentstate = self.status() if currentstate != 1 or savedstate != 2: savedstate = currentstate self.queueOut.put({'state': statuscmd[savedstate], 'last_line': str(self.last_line), 'number_launch': str(self.number_start), 'filter': str(self.activate_filter), 'egs': str(self.egs_filter)}) elif command == "STOP": savedstate = self.stop() self.queueOut.put({'state': statuscmd[savedstate]}) elif command == "STDIN": data = msg.split(maxsplit=1)[1] self.queueOut.put({'state': self.action(data)}) elif command == "STDOUT": try: firstline = int(msg.split(maxsplit=1)[1]) except ValueError: logging.warning("Bad value for param first-line (need integer)") firstline = 0 except IndexError: firstline = 0 self.queueOut.put(self.getlog(firstline)) elif command == "FILTER": self.queueOut.put(self.getfilter()) elif command == "CONFIG": self.queueOut.put(self.getconfig()) elif command == "INFO": self.queueOut.put(self.getinfo()) elif command == "PLAYER": self.queueOut.put(self.getplayer()) elif command == "ADMINCOMMAND": self.queueOut.put(self.getadmincommand()) elif command == "STOPPED": currentstate = self.status() logging.debug('Received event process stopped (current state:%d, saved state:%d)' % (currentstate, savedstate)) if currentstate == 2 and savedstate != 1 and self.restart_after_crash: logging.debug('Prepare restart') self.stop() savedstate = 2 self.eventRunningRestart.clear() try: self.threadRestart.terminate() self.threadRestart.join() except AttributeError: pass self.eventRunningRestart.set() self.threadRestart = threading.Thread(target=self.restart) self.threadRestart.start() else: logging.warning("Bad command (%s)" % command) self.queueOut.put({"error": "command unknown"}) logging.debug('Stop %s' % self.name) self.stop() logging.debug('prepare end') self.eventRunningReader.clear() if self.threadRead: try: self.threadRead.join() except AttributeError: pass self.eventRunningRestart.clear() if self.threadRestart: try: self.threadRestart.terminate() self.threadRestart.join() except AttributeError: pass logging.debug('end') class Manager(): """ Manage all services (read configuration, launch ManageCommand & launch ServerHttp & wait the end) * https service * all child to manage (it start ManageCommand by command define in configuration) """ def __init__(self, launch_program): self.threadCommand = [] self.info = {} self.command = [] self.launch_program = launch_program self.param = {} self.users = {} self.passwordfile = None self.serverHttp = None self.port = 8000 self.address = '' self.keyfile = 'crt/key.pem' self.certfile = 'crt/cert.pem' self.ca_cert = 'crt/ca_cert.crt' self.authentification = False self.method = 'http' def load_config(self, filecfg): if filecfg is None: raise ValueError config = configparser.ConfigParser() config.read_file(filecfg) self._load_config(config) def load_password(self): if self.passwordfile: with open(self.passwordfile, 'rt') as fp: for line in fp: line = line.strip() if not line: continue username, password = line.split(':', maxsplit=1) self.users.setdefault(username, password) def _load_config(self, config): """ Read configuration object param: config: configuration object """ logging.debug("Sections :%s" % config.sections()) for name in config.sections(): if name == 'config:client': continue if name == 'config:user': continue elif name == 'config:server': logging.debug("read config '%s'" % name) try: self.port = int(config[name]['port']) except (TypeError, KeyError, ValueError): pass try: self.address = config[name]['address'] except (TypeError, KeyError): pass try: self.keyfile = config[name]['keyfile'] except (TypeError, KeyError): pass try: self.certfile = config[name]['certfile'] except (TypeError, KeyError): pass try: self.ca_cert = config[name]['ca_cert'] except (TypeError, KeyError): pass try: tmp = config[name]['authentification'] if tmp.upper().strip() == 'YES': self.authentification = True else: self.authentification = False except (TypeError, KeyError): pass try: self.passwordfile = config[name]['passwordfile'] except (TypeError, KeyError): pass try: self.method = config[name]['method'] except (TypeError, KeyError): pass else: try: head, value = name.split(':', maxsplit=1) except ValueError: logging.warning("ignore bad parameter '%s'" % (name)) continue if head == 'command' and 'command' in config[name]: logging.debug("read command '%s'" % name) if 'path' in config[name]: path = config[name]['path'] else: path = None if 'logsize' in config[name]: try: logsize = int(config[name]['logsize']) except (TypeError, KeyError, ValueError): logging.error("Impossible to read param logsize (command:%s)", name) raise ValueError else: logsize = 100 if 'bufsize' in config[name]: try: bufsize = int(config[name]['bufsize']) except (TypeError, KeyError, ValueError): logging.error("Impossible to read param bufsize (command:%s)", name) raise ValueError else: bufsize = 100 if 'activate_filter' in config[name]: try: tmp = config[name]['activate_filter'] if tmp.upper().strip() == 'YES': activate_filter = True else: activate_filter = False except (TypeError, KeyError, ValueError): logging.error("Impossible to read param activate_filter (command:%s)", name) raise ValueError else: activate_filter = False if 'size_max_filter' in config[name]: try: size_max_filter = int(config[name]['size_max_filter']) except (TypeError, KeyError, ValueError): logging.error("Impossible to read param size_max_filter (command:%s)", name) raise ValueError else: size_max_filter = 100 if 'add_filter' in config[name]: try: add_filter = config[name]['add_filter'] except (TypeError, KeyError, ValueError): logging.error("Impossible to read param add_filter (command:%s)", name) raise ValueError else: add_filter = '' if 'del_filter' in config[name]: try: del_filter = config[name]['del_filter'] except (TypeError, KeyError, ValueError): logging.error("Impossible to read param del_filter (command:%s)", name) raise ValueError else: del_filter = '' if 'autostart' in config[name]: try: tmp = config[name]['autostart'] if tmp.upper().strip() == 'YES': autostart = True else: autostart = False except (TypeError, KeyError, ValueError): logging.error("Impossible to read param autostart (command:%s)", name) raise ValueError else: autostart = False if 'restart_after_crash' in config[name]: try: tmp = config[name]['restart_after_crash'] if tmp.upper().strip() == 'YES': restart_after_crash = True else: restart_after_crash = False except (TypeError, KeyError, ValueError): logging.error("Impossible to read param restart_after_crash (command:%s)", name) raise ValueError else: restart_after_crash = False if 'restart_delay' in config[name]: try: restart_delay = int(config[name]['restart_delay']) except (TypeError, KeyError, ValueError): logging.error("Impossible to read param restart_delay (command:%s)", name) raise ValueError else: restart_delay = 10 if 'egs_filter' in config[name]: try: tmp = config[name]['egs_filter'] if tmp.upper().strip() == 'YES': egs_filter = True else: egs_filter = False except (TypeError, KeyError, ValueError): logging.error("Impossible to read param autostart (command:%s)", name) raise ValueError else: egs_filter = False self.param.setdefault(name, {'command': config[name]['command'], 'path': path, 'logsize': logsize, 'bufsize': bufsize, 'activate_filter': activate_filter, 'size_max_filter': size_max_filter, 'add_filter': add_filter, 'del_filter': del_filter, 'autostart': autostart, 'restart_after_crash': restart_after_crash, 'restart_delay': restart_delay, 'egs_filter': egs_filter}) def initialize_http(self): """ Initialize object serverHttp """ logging.debug("Initialize server http(s)") self.serverHttp = ServerHttp(self.keyfile, self.certfile, self.ca_cert, self.address, self.port, authentification=self.authentification, method=self.method, users=self.users) def runCommand(self, name, command, path, logsize, bufsize, queueIn, queueOut, semaphore, activate_filter, size_max_filter, add_filter, del_filter, autostart, restart_after_crash, restart_delay, egs_filter): """ Thread to manage khaganat program """ logging.debug("Initialize '%s'" % name) manageCommand = ManageCommand(name=name, command=command, path=path, logsize=logsize, bufsize=bufsize, queueIn=queueIn, queueOut=queueOut, semaphore=semaphore, activate_filter=activate_filter, size_max_filter=size_max_filter, add_filter=add_filter, del_filter=del_filter, autostart=autostart, restart_after_crash=restart_after_crash, restart_delay=restart_delay, egs_filter=egs_filter) manageCommand.run() def launch_server_http(self): """ Launch server https """ self.serverHttp.daemon = True self.serverHttp.start() def launch_command(self): """ Launch child to manage each program """ for name in self.param: logging.debug("Initialize '%s'" % name) queueIn = multiprocessing.Queue() queueOut = multiprocessing.Queue() # semaphore = multiprocessing.Semaphore() semaphore = multiprocessing.BoundedSemaphore() self.serverHttp.appendchild(name, queueIn, queueOut, semaphore) if self.launch_program: autostart = True else: autostart = self.param[name]['autostart'] threadCommand = multiprocessing.Process(target=self.runCommand, args=(name, self.param[name]['command'], self.param[name]['path'], self.param[name]['logsize'], self.param[name]['bufsize'], queueIn, queueOut, semaphore, self.param[name]['activate_filter'], self.param[name]['size_max_filter'], self.param[name]['add_filter'], self.param[name]['del_filter'], autostart, self.param[name]['restart_after_crash'], self.param[name]['restart_delay'], self.param[name]['egs_filter'])) threadCommand.start() self.threadCommand.append(threadCommand) self.info.setdefault(name, {'queueIn': queueIn, 'queueOut': queueOut, 'semaphore': semaphore, 'threadCommand': threadCommand, 'command': self.param[name]['command'], 'path': self.param[name]['path'], 'logsize': self.param[name]['logsize'], 'bufsize': self.param[name]['bufsize'], 'activate_filter': self.param[name]['activate_filter'], 'size_max_filter': self.param[name]['size_max_filter'], 'add_filter': self.param[name]['add_filter'], 'del_filter': self.param[name]['del_filter'], 'autostart': autostart, 'restart_after_crash': self.param[name]['restart_after_crash'], 'restart_delay': self.param[name]['restart_delay'], 'egs_filter': self.param[name]['egs_filter']}) def receive_signal(self, signum, frame): """ Managed signal """ logging.info("Received signal (%d)" % (signum)) for child in self.threadCommand: logging.info("send signal to child %s" % (child.name)) try: child.terminate() child.join() except AttributeError: logging.info("child not started") pass if self.serverHttp: logging.info("send signal to server http") try: self.serverHttp.terminate() except AttributeError: pass logging.info("Finalize signal (%d)" % (signum)) os._exit(0) def wait_children_commands(self): for child in self.threadCommand: child.join() def wait_child_server_http(self): self.serverHttp.terminate() self.serverHttp.join() def run(self): """ launch all """ signal.signal(signal.SIGABRT, self.receive_signal) signal.signal(signal.SIGTERM, self.receive_signal) self.launch_command() self.launch_server_http() logging.info('started') self.wait_children_commands() logging.info('execute shutdown') signal.alarm(0) logging.info('wait thread http') time.sleep(1) self.wait_child_server_http() logging.info('shutdown completed') def root(filecfg, fileLog, logLevel, launch_program, show_log_console): """ Main function :param str filecfg: configuration file :param str fileLog: log file :param bool launch_program: do you launch program when you start manager (auto start) :param bool show_log_console: do you need show log on console """ # Manage log logging.getLogger('logging') numeric_level = getattr(logging, logLevel.upper(), None) if not isinstance(numeric_level, int): raise ValueError('Invalid log level: %s' % logLevel) handlers = [] if show_log_console: handlers.append(logging.StreamHandler()) if fileLog: handlers.append(logging.FileHandler(fileLog.name)) logging.basicConfig(handlers=handlers, level=numeric_level, format='%(asctime)s %(levelname)s [pid:%(process)d] [%(funcName)s:%(lineno)d] %(message)s') if filecfg is None: logging.error("Missing configuration file") raise ValueError manager = Manager(launch_program) manager.load_config(filecfg) manager.load_password() manager.initialize_http() manager.run() def main(args=sys.argv[1:]): """ Main function :param list args: all arguments ('--help, '--version', ...) """ parser = argparse.ArgumentParser(description='Manage khaganat process') parser.add_argument('--version', action='version', version='%(prog)s ' + __VERSION__) parser.add_argument('-c', '--conf', type=argparse.FileType('r'), default='khaganat.cfg', help='configuration file') parser.add_argument('--show-log-console', action='store_true', help='show message in console', default=False) parser.add_argument('--filelog', type=argparse.FileType('wt'), default=None, help='log file') parser.add_argument('--log', default='INFO', help='log level [DEBUG, INFO, WARNING, ERROR') parser.add_argument('--launch-program', action='store_true', help='launch program when start manager', default=False) param = parser.parse_args(args) root(filecfg=param.conf, fileLog=param.filelog, logLevel=param.log, launch_program=param.launch_program, show_log_console=param.show_log_console) logging.debug("End") if __name__ == '__main__': main()