Out Of Bounds Exception!

Mi lugar de irreflexión y algo de programación

Archivos en la Categoría: Python

Codeando un servidor web en python para administrar programas remotamente (y III)

Bueno, últimamente he andado algo liado con trabajos, terminar la carrera, PFC y demás, así que no he podido dedicar nada de tiempo a mi querido servidor en python.

No obstante he subido el proyecto a Google Code para manejarlo desde ahí, así que ya que estoy pongo la url para los interesados. También lo añado a la sección de descargas

http://code.google.com/p/remote-process-management/

Si alguien está interesado en participar que me envíe un mail y tan ricamente le doy permisos.

PD: Perdón por las chapucillas, un día de estos lo estructuro todo bien y todo 😀

Un saludo.

Codeando un servidor web en python para administrar programas remotamente (II)

Por si te perdiste la parte 1.

Bueno, voy a continuar con el programa que empezamos en el post pasado. Lo que habría que hacer a continuación es crear una clase para el manejo de procesos y añadir diversos métodos a nuestra clase principal para controlarlos. Empecemos con la nueva clase:

En mi caso se va a llamar ProcessManager (muy original, ¿eh?) y va a tener una clase interna para manejar la entrada/salida del processo con un thread:

class ProcessManager:
    class ProcessReaderThread ( threading.Thread ):
    
        def __init__ ( self, subprocess):
            self.proc = subprocess
            self.alive = True
            self.buffer = list()
            threading.Thread.__init__ ( self )
    
        def run ( self ):
            while self.alive:
                output = self.proc.stdout.readline().strip()                
                self.buffer.append(output)
                
        def stop (self):
            self.alive = False
            
        def tell (self, cmd):
            self.proc.stdin.write('%s\n' % cmd)
            self.buffer.append(cmd)            

El constructor acepta un objeto subprocess (que veremos a continuación), inicializa el buffer de salida y crea la variable alive que determinará cuándo un proceso está o no en ejecución.
El método run se ejecuta para arrancar nuestro thread y lee la salida estándar del proceso mientras que alive sea true.
El método stop hace que alive sea false por lo que ya no se leerá más de la salida del proceso.
El método tell pasa un comando a la entrada estándar del proceso.

Además de esta clase interna, ProcessManager tendrá los siguientes métodos:

    def __init__ (self, name, path, processName):
        self.name = name
        self.path = path
        self.processName = processName
        self.process = None
        self.thread = None

Método constructor de la clase que toma como parámetros el nombre, ruta y proceso que sacamos de nuestro fichero de programas. Las variables process y thread se (auto)explican ahora.

El método startProcess iniciará el proceso en cuestión:

    def startProcess(self):
        self.process = subprocess.Popen(self.path,
                                stdin=subprocess.PIPE,
                                stdout=subprocess.PIPE,
                                )
        
        #Start a different thread to read process's output
        self.thread = self.ProcessReaderThread(self.process, self.log)
        self.thread.start()

Básicamente utilizamos la clase subprocess para abrir el proceso indicándole que tanto la entrada como la salida estándar las administraremos nosotros. Posteriormente creamos un nuevo thread y le pasamos este descriptor de proceso para que se encargue de él. Por tanto las variables process y thread servirán para controlar tanto el thread “lector/escritor” como el proceso en sí.

Obviamente, como “todo lo que tiene un principio, tiene un final Neo“:

    def killProcess(self):
        self.thread.stop()
        try:
            self.process.terminate()
        except:
            pass

Este método símplemente dirá al thread que deje de leer/escribir en el proceso y después terminará éste.

Nuestro método tell servirá para que la clase principal del programa lance los comandos que quiera que reciba el proceso

    def tellProcess(self, cmd):
        try:            
            self.thread.tell(cmd)
        except:
            pass

Básicamente llamará a la función ya vista de la clase interna. Nota: aquí hay un pequeño bug (solucionado en la versión que pondré para descarga) por el cual, si el proceso es cerrado sin llamar al método de esta clase (por ej: haciendo un exit si el programa es un terminal) al intentar ejecutar un comando, saltará una excepción, por lo tanto habría que comprobar si el proceso de verdad está corriendo y, si no, terminarlo.

Finalmente nos quedan un par de métodos básicos:

    def isRunning(self):
        if self.thread:
            return self.thread.alive
        
        return False
    
    def getOutputBuffer(self):
            return self.thread.buffer

Y con esto ya tendríamos nuestro manejador de procesos. A continuación pongo los nuevos métodos que tendrá nuestra clase principal para lidiar con todo esto y también haremos que nuestro servidor acepte peticiones POST para que se pueda manejar desde fuera.

Empezamos con las peticiones POST en la clase ServerHandler

    def do_POST(self):
        try:            
            form = cgi.FieldStorage(
            fp = self.rfile, 
            headers = self.headers,
            environ = {'REQUEST_METHOD':'POST',
                       'CONTENT_TYPE':self.headers['Content-Type'],
                      })
                    
            if len(form.keys()) > 0:
                field = form.keys()[0]
                action = field.split("_")[0]
                name = field.split("_")[1]
                        
                if action == 'cmd':
                    if len(form[field].value) > MAX_CMD_LENGTH:
                        self.log_alert("%s" % OVERFLOW_ATTEMPT, len(form[field].value))
                    executeCommand(name, form[field].value[:MAX_CMD_LENGTH], self.client_address[0])
                elif action == 'switch':
                    switchProgram(name)
                            
                self.send_response(200)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
                self.wfile.write(makeHtml())
                return
            
        except Exception, err :
            if DEBUG: print 'Exception in POST: ' + str(err)
            self.send_error(500)        
            return 

Dado que nuestro html es muy listo y en el valor de los botones se incluye el nombre del proceso, nos bastará con coger los parámetros POST suministrados (con el cgi.FieldStorage) y hacer un split para tener la acción a realizar y el nombre del proceso sobre el cual hacerlo.
Una vez tengamos esto diferenciaremos las dos opciones actuales (switch* para cambiar de ON a OFF y viceversa; cmd para ejecutar un comando) y llamaremos a uno u otro método.
*Nótese que cometí el error de poner dos botones y hacer un switch, lo cual no tiene sentido, pero lo arreglaré en la siguiente versión.

Vamos ahora con el método de switch (que más tarde tendrá que aceptar un parámetro para ver si queremos encenderlo o apagarlo):

def switchProgram(name):
    
    global programs
    for program in programs:
        if program['name'] == name:
            #Create a new manager if there's noone
            if not program['manager']:
                program['manager'] = ProcessManager(program['name'], program['path'], program['process'])
                
            if not program['manager'].isRunning():
                if DEBUG: print "Turning ON " + name 
                program['manager'].startProcess()
            else:
                if DEBUG: print "Turning OFF " + name 
                program['manager'].killProcess()
                
            break

Básicamente cogemos nuestra lista de programas y buscamos el que nos piden por parámetro (quizá se podría agilizar esto usando un diccionario, así que lo apuntaré para futuras revisiones). Una vez encontrado miraremos a ver si alguna vez durante la ejecución hemos creado un manager para él (si no, lo creamos), y llamaremos a startProcess o killProcess dependiendo de si está apagado o encendido respectivamente.

Finalmente nuestro método executeCommand hará lo siguiente:

def executeCommand(name, command):
    
    global programs
    
    for program in programs:
        if program['name'] == name and program['manager']:
            program['manager'].tellProcess(command)

Buscamos el programa que nos piden y, si tiene un manager, le pasamos el comando.

Y ya está, más o menos por encima he explicado como crear un programa de unas 400 loc para administrar nuestros procesos. He obviado algunas partes porque se meten más con html que con el programa en sí, pero realmente son muy sencillas (quizá tediosas) de hacer.

Esto sería una versión alpha si acaso, aún necesitaríamos añadir logs, autenticación, protección contra XSS, lista de baneados, un nombre decente y algunas mejoras más que ya iré viendo y posteando según las vaya incluyendo. El programa lo pondré para descarga cuando considere que está medianamente presentable (está hecho en dos tardes, así que imaginad).

Un saludo.

Codeando un servidor web en python para administrar programas remotamente (I)

Tras una temporadilla considerable sin pasarme por estos lares a actualizar con alguna aventura de las mías, hoy, por fin, vuelvo a las andadas.

Y lo hago nada más y nada menos que con un programa que se me ocurrió en una de esas tardes ociosas típicas: un proceso que actúe como servidor web, mostrando una página html pseudodinámica (luego explico por qué el ‘pseudo’) que permita a un usuario administrar (iniciar, detener y comunicarse) una serie de programas configurables.

La idea me vino debido a que mis compañeros de clase y yo solemos jugar a diversos juegos según la temporada, y muchas veces a uno le toca hacer de servidor para alguno en concreto, por lo que se hace algo pesado estar pendiente de cuando los otros quieren jugar y tú no quieres o no estás. Por lo tanto, creé este programa para evitarme eso, símplemente me basta con tener el pc encendido conectado a internet y el servidorcillo web corriendo.

Tras esta pequeña introducción, vamos al lío. Primero hay que elegir el lenguaje de programación, el cual tiene que cumplir, al menos, dos requisitos: mínimo consumo de memoria y facilidad para lo que queremos hacer. Expongo a continuación mis 3 lenguajes preferidos y vamos descartando:

C++ no es precisamente trivial para abrir sockets ni en el manejo de procesos sobre Windows, por no hablar de que la programación sobre esta plataforma, para mí, es horrible, así que ni me lo planteo.
Java y su consumo de memoria no es precisamente óptimo y, aunque probablemente se pueda configurar la máquina virtual para que no ocupe esos odiosos 50 MB de ram para hacer una suma, quiero algo simple y lo quiero ya, así que descartado.
Python… qué puedo decir de mi amado Python, es más simple que el mecanismo de un chupete, consume poco más que un programa en C y además es posible que el resultado sea portable a Linux y todo. No se hable más.

¡Y empieza el codeo! Empecemos con la clase principal, que incluye el sevidor (nótese que soy algo nuevo en Python, así que es probable que muchas cosas se puedan hacer mejor o más rápido; si es así decídmelo y lo actualizo con mucho gusto).

Función main que es la que arranca todo el tema (llamada cuando se arranca el script):

def main():
    try:
        print 'Programs loaded...'
        loadProgramList()
        server = HTTPServer(('', 8080), ServerHandler)
        print 'Started httpserver...'
        server.serve_forever()
    except KeyboardInterrupt:
        print '^C received, shutting down server'
        server.socket.close()

if __name__ == '__main__':
    main()

Carga la lista de programas y arranca el servidor, simple, ¿no?

La función loadProgramList símplemente va a abrir un archivo de programas que tiene el siguiente formato (nótese que cada campo está separado por una tabulación, no por un espacio):

#NOMBRE RUTA PROCESO DESC
TaskManager V:\Window$\System23\taskmgr.exe taskmgr Programa bonito

La función es la que sigue:

def loadProgramList():
    file = open('programs')
    
    global programs
    programs = list()
        
    for line in file:
        if line.find('#') != 0 and line != '':
            line = line.replace('\n','').split("\t")
            programs.append({'name':line[0], 'path':line[1], 'process':line[2], 'manager':None})
            
    return programs

Programs es una variable global en la que guardamos una lista donde cada programa es un diccionario clave-valor con las propiedades extraídas del fichero y un extra, manager, que luego explicaré.

Vamos a la clase del ServerHandler:

class ServerHandler(BaseHTTPRequestHandler):
    
    def do_GET(self):
        try:
            if not self.path.endswith('favicon.ico'):                
                                   
                self.send_response(200)
                self.send_header('Content-type', 'text/html')
                self.end_headers()
                self.wfile.write(makeHtml())                    
                return
                
        except IOError:
            self.send_error(404,'File Not Found: %s' % self.path)
            return     

Algo simplificado, pero igualmente válido para explicar las bases. Cuando nos soliciten una página por get, contestamos con el resultado de la función makeHtml

Para terminar con la primera parte, veamos cómo es esta función:

def makeHtml():
    html = ''
    
    file = open('index.html')
    
    #load the html
    for line in file:
        html += line
            
    #find server's tags
    regex = re.compile(r' .*? ')
    tags = regex.findall(html)
    
    #replace server's tags
    for tag in tags:
        keyword = tag.replace('', '').replace('', '').strip()
        if keyword == ...
            content = ...
            html = html.replace(tag, content)
    return html

Vamos por partes, primero abrimos la página en html que hemos creado (con su css, javascript y demás ya hecho) y buscamos nuestras etiquetas personalizadas, que tendrán la forma <py>tag</py> donde ‘tag’ estará definido en el código (o, en caso de que en un futuro se quiera expandir esto, en un fichero). Ahora se sustituye cada una de las etiquetas por un contenido concreto, en mi caso haré que por cada programa se cree una etiqueta <li> para mostrar una serie de pestañas hechas con JQuery. Por otro lado debajo de esto añadiré, también para cada programa, un contenido concreto (varios botones, una caja de texto, etc) para el manejo de cada programa. En resumen, queda algo así:

El estilo ya es cosa de cada uno, yo como soy algo cutre lo he hecho lo más simple posible =P

En la parte 2 añadiré algunas mejoras y veremos cómo arrancar procesos y comunicarnos con ellos.

¡Un saludo!