Hard to maintain that linux_server under UNIX? Process manager
#1
Hi all,

I guess all those nasty shell scripts and screen`ing and other unclear stuff just get your time wasted if all you want is just to run some servers, stop and restart them... and if you're not there to restart it, sheesh...

I spent this evening coding this script up:
#!/usr/bin/env python2.6
"""
Process manager by Drakas
Under zlib license.
Revision 1
Create a file nmpx.cfg.py:
config = {
    "curses":    True,
    "procs":    [
        {'name':'Public Server 1','dir':'/home/user/AssaultCube_v1.0.4/','autorestart':True,
        'args':['bash','-c','./bin_unix/linux_server -mlocalhost -f1234 -nPS1 -Nps1']},
        {'name':'Public Server 2','dir':'/home/user/AssaultCube_v1.0.4/','autorestart':False,
        'args':['bash','-c','./bin_unix/linux_server -mlocalhost -f1236 -nPS2 -Nps2']},
        {'name':'Private Server 1','dir':'/home/user/AssaultCube_v1.0.4/','autorestart':False,
        'args':['bash','-c','./bin_unix/linux_server -mlocalhost -f1238 -nPS3 -Npr1']},
        {'name':'Private Server 2','dir':'/home/user/AssaultCube_v1.0.4/','autorestart':True,
        'args':['./bin_unix/linux_server','-mlocalhost','-f1240','-nPS4 xyz','-Npr2']}
    ]
}

name: the service name, for your own reference
dir: the working directory of the service
autorestart: automatically restart if it died, or if the configuration was changed (!)
args: the command concerned. Every argument written separately,
        but you can use bash -c 'command' to make it easier on yourself.
        
The configuration file is reloaded every 5 seconds, and new processes are started if necessary.

"""
import time
import datetime
import subprocess
import curses
import os
procs = {}
ll = []
ml = []
myscreen = None
mcwd = None
# Not implemented yet:
# This function would permit us to read all our logs live, and perhaps dump them as well.
# Need help here
def mlog(txt,service_name):
    global myscreen
    if myscreen is not None:
        global ml
        if len(ml) > 20:
            ml.pop(0)
        ml.append("[%s] %s" % service_name.rjust(25," "), txt)
# This function just takes any logmessages and outputs them
def alog(txt):
    global myscreen
    if myscreen is not None:
        global ll
        if len(ll) > 5:
            ll.pop(0)
        ll.append("[%s] %s"% (time.strftime('%Y%m%d %H:%M:%S'), txt))
    else:
        print(txt)
# This function loads the configuration.
def lconfig():
    global procs
    global config
    global mcwd
    g = {}
    os.chdir(mcwd)
    execfile("nmpx.cfg.py",g)
    config = g["config"]
    for pro in config["procs"]:
        if not pro["name"] in procs:
            print "Adding new process, name %s" % pro["name"]
            procs[pro["name"]] = {"ran":False,"process":None,"name":None,"kill":False,"dir":None,"args":None,"modified":True}
        p = procs[pro["name"]]
        if p["name"] != pro["name"]:
            p["name"] = pro["name"]
            p["modified"] = True
        if p["dir"] != pro["dir"]:
            p["dir"] = pro["dir"]
        p["autorestart"] = pro["autorestart"]
        if p["args"] != pro["args"]:
            p["args"] = pro["args"]
            p["modified"] = True
# This function outputs the status
def status():
    global myscreen
    n = 0
    if myscreen is not None:
        myscreen.clear
        myscreen.border(0)
        myscreen.addstr(2, 2, "Process manager")
    for k in procs.keys():
        p = procs[k]
        if myscreen is None:
            #print ...
            alog("True")
        else:
            n=n+1    
            sstring = "(%s) %s        " % (['started','stopped'][p["process"] is None],p["name"].rjust(25," "))
            myscreen.addstr(3+n,2,sstring)
    if myscreen is not None:
        ni = 0
        global ll
        for az in ll:
            ni = ni + 1
            myscreen.addstr (4+n+ni,2, az)
    myscreen.refresh()
# This function checks the processes
def checkProcesses():
    global procs
    for k in procs.keys():
        p = procs[k]
        if p["process"] is not None and p["process"].poll() is not None:
            alog("Process %s died" % p["name"])
            p["process"] = None
        if p["modified"] and p["autorestart"]:
            if p["process"] is not None:
                alog("Process %s was modified, so stopping" % p["name"])
                p["process"].kill()
            p["process"] = None
        if p["process"] is not None and p["kill"]:
            alog("Killing process %s" % p["name"])
            p["process"].kill()
            p["process"] = None
        if p["process"] is None and (p["autorestart"] or p["ran"] is False) and not p["kill"]:
            alog("Process %s starting" % p["name"])
            os.chdir(p["dir"])
            p["process"] = subprocess.Popen(p["args"],stdout=subprocess.PIPE)
            p["modified"] = False
            p["ran"] = True
def gquit():
    global procs
    cquit()
    for k in procs.keys():
        if procs[k]["process"] is not None:
            alog("Killing %s "%procs[k]["name"])
            procs[k]["process"].kill()
    alog("Quitting")
    quit()
def cquit():
    global myscreen
    myscreen.keypad(0)
    curses.echo()
    curses.nocbreak()
    curses.endwin()
    myscreen = None
def main():
    global mcwd
    mcwd = os.getcwd()
    alog("Loading config")
    lconfig()
    
    global myscreen
    #
    
    if config["curses"]:
        myscreen = curses.initscr()
        myscreen.nodelay(1)
    else:
        myscreen = None
    alog("Checking processes")
    checkProcesses()
    timer = 0
    while True:
        timer = timer + 1
        time.sleep(0.1)
        # Reload the configuration every 5 seconds
        if timer % 50 is 0:
            timer = 0
            lconfig()
        checkProcesses()
        if myscreen is not None:
            ch = myscreen.getch()
            if ch > 0 and ch < 256:
                cc = chr(ch)
                # Allow only 1 - 10, so only 10 processes can be managed for now
                if cc.isdigit():
                    k = procs.keys()
                    cc = int(cc)
                    if cc == 0:
                        cc = 10
                    if cc <= len(k):
                        p = procs[k[cc-1]]
                        if p["process"] is not None:
                            # We tell it to get killed and stop
                            procs[k[cc-1]]["kill"] = True
                        else:
                            # We tell it to start like before
                            procs[k[cc-1]]["ran"] = False
                elif cc == "q":
                    gquit()
            status()
    alog("Quitting")
if __name__ == "__main__":
    try:
        main()
        if myscreen is not None:
            cquit()
    except:
        if myscreen is not None:
            cquit()
            # Bring back the error into a reset terminal, of course
            raise
Name this file as nmpx.py, and then run it with
screen python nmpx.py

Now you have an ncurses interface inside screen to start/stop your servers. Simply press 1/2/3/4/5/... to stop or start that server.

I've never written a Python script before (this is my first time...) so don't expect my code to be ueber-great. Yes, there are a lot of dodgy things (obviously), so I hope that I can get help from people who know what they're doing, and will submit any updates and improvements here.

What I'd like to have in the end is an interface where:
* You can select more than 10 servers and control them with a nice ncurses interface
* Read all logs, live, at the same time, and also dump them into one single file
* Interact with this application by other means, especially for remote control

Hope this helps :)

Thank you :D
Thanks given by:
#2
nice!!
Thanks given by:
#3
Dude wat, you beast O.o
Thanks given by:
#4
[Image: python.png]
Thanks given by:
#5
hihi :)

Anyone tried this? I haven't yet applied this to any of my servers.
Thanks given by:
#6
Thanks, I'll try when I have time
Thanks given by: