Linux run python script as daemon

How to make a Python script run like a service or daemon in Linux

I wouldn’t recommend you to choose 2., because you would be, in fact, repeating cron functionality. The Linux system paradigm is to let multiple simple tools interact and solve your problems. Unless there are additional reasons why you should make a daemon (in addition to trigger periodically), choose the other approach.

Also, if you use daemonize with a loop and a crash happens, no one will check the mail after that (as pointed out by Ivan Nevostruev in comments to this answer). While if the script is added as a cron job, it will just trigger again.

Here’s a nice class that is taken from here:

#!/usr/bin/env python import sys, os, time, atexit from signal import SIGTERM class Daemon: """ A generic daemon class. Usage: subclass the Daemon class and override the run() method """ def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'): self.stdin = stdin self.stdout = stdout self.stderr = stderr self.pidfile = pidfile def daemonize(self): """ do the UNIX double-fork magic, see Stevens' "Advanced Programming in the UNIX Environment" for details (ISBN 0201563177) http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 """ try: pid = os.fork() if pid > 0: # exit first parent sys.exit(0) except OSError, e: sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) sys.exit(1) # decouple from parent environment os.chdir("/") os.setsid() os.umask(0) # do second fork try: pid = os.fork() if pid > 0: # exit from second parent sys.exit(0) except OSError, e: sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) sys.exit(1) # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() si = file(self.stdin, 'r') so = file(self.stdout, 'a+') se = file(self.stderr, 'a+', 0) os.dup2(si.fileno(), sys.stdin.fileno()) os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) # write pidfile atexit.register(self.delpid) pid = str(os.getpid()) file(self.pidfile,'w+').write("%s\n" % pid) def delpid(self): os.remove(self.pidfile) def start(self): """ Start the daemon """ # Check for a pidfile to see if the daemon already runs try: pf = file(self.pidfile,'r') pid = int(pf.read().strip()) pf.close() except IOError: pid = None if pid: message = "pidfile %s already exist. Daemon already running?\n" sys.stderr.write(message % self.pidfile) sys.exit(1) # Start the daemon self.daemonize() self.run() def stop(self): """ Stop the daemon """ # Get the pid from the pidfile try: pf = file(self.pidfile,'r') pid = int(pf.read().strip()) pf.close() except IOError: pid = None if not pid: message = "pidfile %s does not exist. Daemon not running?\n" sys.stderr.write(message % self.pidfile) return # not an error in a restart # Try killing the daemon process try: while 1: os.kill(pid, SIGTERM) time.sleep(0.1) except OSError, err: err = str(err) if err.find("No such process") > 0: if os.path.exists(self.pidfile): os.remove(self.pidfile) else: print str(err) sys.exit(1) def restart(self): """ Restart the daemon """ self.stop() self.start() def run(self): """ You should override this method when you subclass Daemon. It will be called after the process has been daemonized by start() or restart(). """ 

Источник

Читайте также:  Сменить регистр символа питон

Создание демона Python с использованием Systemd

Создание демона Python с использованием Systemd

Недавно у меня возникла задача создать демон (фоновое приложение) реализованный на Python в системе Linux использующей Systemd . В поисках современного решения и родилась данная статья. Ранее для реализации демона выполнялась «демонизация» приложения Python, зачастую с помощью библиотеки python-daemon

(opens new window) . Даже была создана спецификация pep-3143

(opens new window) для реализации демонов. Но на текущий момент времени с использованием Systemd нет необходимости демонизировать наше Python приложение, достаточно корректно описать его запуск в юните. Для начала рассмотрим как работает Systemd .

Все операции указанные в данной статье выполнялись в окружении Linux Ubuntu 20, с установленным Python 3.8.

# Systemd Unit

  • /usr/lib/systemd/system/ — юниты из установленных пакетов, такие как nginx, postgreee и др.
  • /run/systemd/system — юниты созданные в runtime
  • /etc/systemd/system — юиниты, созданные администратором, в основном пользовательские юниты должны храниться здесь.

Для создания юнита нам необходимо описать 3 секции: [Unit], [Service], [Install] Основные переменные блока [Unit]:

[Unit] Descripiton=Unit Descripion After=syslog.target After=network.target After=nginx.service After=mysql.service Requires=mysql.service Wants=redis.service 
  • Description — описание юнита
  • After — указывает, что юнит должен быть запущен после группы указанных сервисов
  • Requires — узказывает, что для запуска юнита требуется запущенный сервис mysql , запуск нашего сервиса выполняется паралллельно с требуемым ( mysql ), если требуемый не указан в After
  • Wants — описательная переменная, показывающая, что для запуска сервиса желателен запущенный сервис redis
[Service] Type=simple PIDFile=/var/lib/service.pid WorkingDirectory=/var/www/myapp User=user Group=user Environment=STAGE_ENV=production OOMScoreAdjust=-100 ExecStart=/my_venv/bin/python my_app.py --start ExecStop=/my_venv/bin/python my_app.py --stop ExecReload=/my_venv/bin/python my_app.py --restart TimeoutSec=300 Restart=always 
  • Type — тип запуска: simple (по умолчанию) запускает службу незамедлительно при этом процесс не должен разветвляться, не подходит если другие службы зависят от очередности при запуске данной службы; forking — служба запускается однократно и процесс разветвляется с завершением родительского процесса; другие типы можно рассмотреть по ссылке ниже.
  • PIDFile — позволяет задать место нахождения pid файла
  • WorkingDirectory — указывает рабочий каталог приложения, если указан то ExecStart|Stop|Reload запускаются из этого каталога, т.е. my_app.py станет /var/www/myapp/my_app.py
  • User, Group — соответственно пользователь и группа под которыми будет запущен сервис.
  • Environment — переменные окружения
  • OOOMSCoreAdjust — запрет на kill сервиса вследствие нехватки памяти и срабатывания механизма ООМ: -1000 полный запрет, -100 понижает вероятность.
  • ExecStart|Stop|Reload — команды запуска, останова, перезагрузки сервиса, команда должна использовать абсолютный путь к исполняемому файлу.
  • Timeout — время ожидания systemd отработки команд Start|Stop в сек.
  • Restart — перезапуск сервиса если он упадет.
[Install] WantedBy=multi-user.target 
  • WantedBy — уровень запуска нашего сервиса, mulit-user.target или runlevel3.target соответствует runlevel=3 «Многопользовательский режим без графики»

Размещаем данный файл с указанными секциями в директории /etc/systemd/sysmtem/.service

systemctl -l status test_unit 
systemctl start test_unit 

При внесении изменений в наш сервис перегружаем его:

# Python приложение

Разобрав как работает Systemd можем приступить к созданию нашего сервиса. Для ознакомления создадим приложение Python которое будет выводить сообщения в log файл test_daemon.py :

import time import argparse import logging logger = logging.getLogger('test_daemon') logger.setLevel(logging.INFO) formatstr = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' formatter = logging.Formatter(formatstr) def do_something(): """ Здесь мы только лишь пишем сообщение в Log, но можем реализовать абсолютно любые задачи выполняемые в фоне. """ while True: logger.info("this is an INFO message") time.sleep(5) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Example daemon in Python") # Мы можем заменить default или запускать приложение с указанием нахождения # log файла, через параметр -l /путь_к_файлу/файл.log parser.add_argument('-l', '--log-file', default='/home/user/test_daemon.log') args = parser.parse_args() fh = logging.FileHandler(args.log_file) fh.setLevel(logging.INFO) fh.setFormatter(formatter) logger.addHandler(fh) do_something() 

Проверим работоспособность нашего скрипта:

Проверим что log-файл создан и в него пишутся сообщения:

tail -f /home/user/test_daemon.log 

Теперь создадим юнит Systemd для запуска демона. В директории /etc/systemd/system файл test_daemon.service с содержанием:

[Unit] Description=Test daemon After=syslog.target [Service] Type=simple User=user Group=user WorkingDirectory=/home/user/ ExecStart=/usr/bin/python3 test_daemon.py [Install] WantedBy=multi-user.target 

В данном юните следует изменить пользователя и группу User , Group на вашего пользователя. Также указать в качестве рабочего каталога WorkingDirectory абсолютный путь к директории где находится файл test_daemon.py.

В случае использования venv в параметре ExecStart следует указать путь к python внутри venv , т.е. заменть /usr/bin/python3 на /venv/bin/python

Проверяем статус нашего сервиса:

systemctl -l status test_daemon 
systemd enable test_daemon 
systemctl start test_daemon 

Проверим отображение данных в логе:

tail -f /home/user/test_daemon.log 

Наш демон заработал, но осталась не решенной еще одна задача. Зачастую при остановке нашего демона требуется выполнить каие либо задачи (сохранить состояние приложения, отправить уведомление, выполнить очистку данных и т.п.), но на текущий момент при останове наш демон просто закрывается.

# Обработка сигналов завершения

В стандартной библиотеке Pyhton реализован модуль sygnal позволяющий обрабатывать сигналы UNIX-based операционной системы. Полный перечень сигналов можно посмотреть по команде:

Демон Systemd при выполнении операции останова демона отправляет изначально сигнал 15 (SIGTERM) — нормальный останов процесса. По умолчанию если в течение 30 сек. не будет получен сигнал выхода приложения будет отправлен сигнал жесткого завершения процесса 9 (SIGKILL) .

Таким образом в нашем приложении необходимо предусмотреть обработчик сигнала 15 (SIGTERM) , как результат наш скрипт test_daemon.py примет следующий вид:

import time import argparse import logging import sys import signal logger = logging.getLogger('test_daemon') logger.setLevel(logging.INFO) formatstr = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' formatter = logging.Formatter(formatstr) def terminate(signalNumber, frame): """ Здесь мы можем обработать завершение нашего приложения Главное не забыть в конце выполнить выход sys.exit() """ logger.info(f'Recieved signalNumber>') sys.exit() def do_something(): """ Здесь мы только лишь пишем сообщение в Log, но можем реализовать абсолютно любые задачи выполняемые в фоне. """ while True: logger.info("this is an INFO message") time.sleep(5) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Example daemon in Python") # Мы можем заменить default или запускать приложение с указанием нахождения # log файла, через параметр -l /путь_к_файлу/файл.log parser.add_argument('-l', '--log-file', default='/home/user/test_daemon.log') args = parser.parse_args() signal.signal(signal.SIGTERM, terminate) fh = logging.FileHandler(args.log_file) fh.setLevel(logging.INFO) fh.setFormatter(formatter) logger.addHandler(fh) do_something() 

Теперь при останове нашего демона:

systemctl start test_daemon 

Мы увидим в лог файле сообщение Recieved 15

2021-06-27 15:06:48,024 - test_daemon - INFO - this is an INFO message 2021-06-27 15:06:53,029 - test_daemon - INFO - this is an INFO message 2021-06-27 15:06:56,971 - test_daemon - INFO - Recieved 15 

Таким образом мы создали шаблон демона который в дальнейшем может быть использован для решения реальных фоновых задач.

Источник

Оцените статью