15 de marzo de 2017

Socket server como servicio

En la entrada http://codigo-python.blogspot.com.es/2017/02/socket-server-tcp-multi-thread-ii.html construíamos un socket server basado en hilos con control de número hilos en ejecución y de forma que lo que que recibíamos desde los diferentes clientes era almacenado a un fichero de log, el cual además rotaba de forma periódica.

A continuación vamos a ver como ejecutar la aplicación para que se comporte como un servicio en sistemas unix, en concreto en debian y para que corra con un usuario del sistema con los permisos adecuados.

Vamos a suponer que el código de nuestro socket server lo tenemos en el fichero socket-server.py dentro del directorio /opt/socket-server/ y que queremos ejecutar el código como usuario socket-user, el cual pertenece al grupo socket-group.

Tendremos algo como lo siguiente:
root@debian:/opt# pwd
/opt

root@debian:/opt# ls -l
total 4
drwxr-xr-x  2 socket-user socket-group 4096 mar 15 19:15 socket-server

root@debian:/opt/socket-server# ls -l
total 4
-rwxr-xr-x 1 socket-user socket-group 3231 mar 15 19:38 socket-server.py

Los logs de la aplicación vamos a guardarlos en el directorio /var/log/socket-server/, con lo cual nos aseguraremos que el directorio tiene los permisos adecuados:
root@debian:/var/log# pwd
/var/log
root@debian:/var/log# ls -l | grep socket-server
drwxr-xr-x  2 socket-user socket-group       4096 mar 15 19:25 socket-server

Con estas premisas vamos a montar un script de bash que llamaremos socket-server que hará las veces de manejador del servicio y que guardaremos en el directorio /etc/init.d/. El contenido del fichero es el siguiente:
#!/bin/bash

# /etc/init.d/scripts
# Description: Script for manage socket-server
# ————————————————–
#
### BEGIN INIT INFO
# Provides: Scripts for socket-server
# Required-Start: $network $local_fs $syslog
# Required-Stop: $local_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Start Python scripts to provide socket-server service
### END INIT INFO

PIDFILE=/tmp/socket-server
DAEMONLOG=/var/log/socket-server/daemon.log

case "$1" in
 start)
   if [ ! -f $PIDFILE ] ; then
                echo "Starting socket-server..."
                su socket-user -c "nohup /usr/bin/python -u /opt/socket-server/socket-server.py > $DAEMONLOG 2>&1 &"
   else
         for pid in $(cat $PIDFILE) ; do
                if ! ps --no-headers p "$pid" | grep socket-server > /dev/null ; then
                        echo "Starting socket-server..."
                        su socket-user -c "nohup /usr/bin/python -u /opt/socket-server/socket-server.py > $DAEMONLOG 2>&1 &"
                else
                        echo "The socket-server is already running!!"
        fi
   done
  fi
  ;;
  stop)
  if [ ! -f $PIDFILE ] ; then
                echo "The socket-server is not running"
  else
        for pid in $(cat $PIDFILE) ; do
                if ! ps --no-headers p "$pid" | grep socket-server > /dev/null ; then
                        echo "The socket-server is not running"
                else
                        echo "Stopping socket-server..."
                        kill -9 $pid
                fi
        done
  fi
  ;;

 restart)
   $0 stop
   sleep 1
   $0 start
   ;;
 *)
   echo "usage: $0 {start|stop|restart}"
esac

Un par de detalles a tener en cuenta: el script define un fichero de logs DAEMONLOG=/var/log/socket-server/daemon.log, y os preguntaréis para qué? si el propio socket server ya escribe su fichero de logs. En este caso en el fichero daemon.log va a almacenar los logs del propio script de bash y además las excepciones no capturadas del fichero socket-server.py

También podéis observar que definimos un fichero PIDFILE donde se va a almacenar el PID del proceso. Si echamos la vista atrás sobre el código socket-server.py vemos que no se maneja en ningún sitio el PID del proceso. Debemos por tanto incluirlo. Para ello simplemente añadimos un trocito de código a nuestro fichero socket-server.py, de forma que tendremos:

#!/usr/bin/env python

import threading
import SocketServer, socket
import sys
import os
import logging, logging.handlers

TIMEOUT = 10
HOST = '0.0.0.0'
PORT = 3456

MAX_THREADS = 50

LOG_FOLDER = '/var/log/socket-server/'
LOG_FILE = 'socket-server.log'
ROTATE_TIME = 'midnight'
LOG_COUNT = 10
PID = "/tmp/socket-server"

if os.access(os.path.expanduser(PID), os.F_OK):
    logger.info('Checking if socket-server is already running...')
    pidfile = open(os.path.expanduser(PID), "r")
    pidfile.seek(0)
    old_pd = pidfile.readline()
    # process PID
    if os.path.exists("/proc/%s" % old_pd) and old_pd!="":
        logger.info('You already have an instance of the socket-server running')
        logger.info('It is running as process %s' , old_pd)
        sys.exit(1)
    else:
        logger.info('socket-server is not running. Trying to start socket-server...')
        os.remove(os.path.expanduser(PID))
pidfile = open(os.path.expanduser(PID), 'a')
logger.info('socket-server started with PID: %s' , os.getpid())
pidfile.write(str(os.getpid()))
pidfile.close()

log_folder = os.path.dirname(LOG_FOLDER)

if not os.path.exists(log_folder):
 try:
  os.makedirs(log_folder)
 except Exception as error:
  print 'Error creating the log folder: %s' % error
        exit()

try:
 logger = logging.getLogger('socket-server')
 loggerHandler = logging.handlers.TimedRotatingFileHandler(LOG_FOLDER + LOG_FILE , ROTATE_TIME, 1, backupCount=LOG_COUNT)
 formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
 loggerHandler.setFormatter(formatter)
 logger.addHandler(loggerHandler)
 logger.setLevel(logging.DEBUG)
except Exception as error:
 print '------------------------------------------------------------------'
 print '[ERROR] Error writing log at %s: %s' % (LOG_FOLDER, error)
 print '[ERROR] Please verify path folder exits and write permissions'
 print '------------------------------------------------------------------'
 exit()


class RequestHandler(SocketServer.BaseRequestHandler):
    def handle(self):
        try:
            # chequeamos el numero de threads activos. Si es mayor que el limite establecido cerramos la conexion y no atendemos al cliente. Lo trazamos
            if threading.activeCount() > MAX_THREADS:
                logger.warn('%s -- Execution threads number: %d', threading.currentThread().getName(),
                            threading.activeCount() - 1)
                logger.warn('Max threads number as been reached.')
                self.closed()
            # si no hemos alcanzado el limite lo atendemos
            else:
                threadName = threading.currentThread().getName()
                activeThreads = threading.activeCount() - 1
                clientIP = self.client_address[0]
                logger.info('[%s] -- New connection from %s -- Active threads: %d' , threadName, clientIP, activeThreads)
                data = self.request.recv(1024)
                logger.info('[%s] -- %s -- Received: %s' , threadName, clientIP, data)
                response = 'Thanks %s, message received!!' % clientIP
                self.request.send(response)
        except Exception, error:
            if str(error) == "timed out":
                logger.error ('[%s] -- %s -- Timeout on data transmission ocurred after %d seconds.' ,threadName, clientIP, TIMEOUT)

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    def server_bind(self):
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)

    def finish_request(self, request, client_address):
        request.settimeout(TIMEOUT)
        SocketServer.TCPServer.finish_request(self, request, client_address)
        SocketServer.TCPServer.close_request(self, request)

try:
    print "Starting server TCP at IP %s and port %d..." % (HOST,PORT)
    server = ThreadedTCPServer((HOST, PORT), RequestHandler)
    server.serve_forever()
except KeyboardInterrupt:
    server.socket.close()


Con todo lo anterior ya estamos en disposición de manejar nuestro socket-server como un servicio:

root@debian:~# /etc/init.d/socket-server
usage: /etc/init.d/socket-server {start|stop|restart}
Probamos a arrancarlo:

root@debian:~# /etc/init.d/socket-server start
Starting socket-server...
Si todo ha ido bien el proceso debería estar corriendo con el usuario que hemos definido. Si hubiera algún error podemos comprobar el fichero /var/log/socket-server/daemon.log Para detenerlo:

root@debian:~# /etc/init.d/socket-server stop
Stoping socket-server...
Si queremos que el proceso se arranque automáticamente al arrancar el sistema, en el caso de debian ejecutamos los siguientes comandos:

root@debian:~# cd /etc/init.d/
root@debian:/etc/init.d# update-rc.d socket-server defaults
El script que maneja el servicio se puede adaptar fácilmente a vuestro propio código python si queréis convertirlo en servicio. Animaos y probadlo!!