Mostrando entradas con la etiqueta service. Mostrar todas las entradas
Mostrando entradas con la etiqueta service. Mostrar todas las entradas

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!!