Як зробити так, щоб сценарій Python запускався як служба чи демон у Linux


175

Я написав сценарій Python, який перевіряє певну адресу електронної пошти та передає нові електронні листи зовнішній програмі. Як я можу змусити цей сценарій виконувати 24/7, наприклад перетворення його на демон або службу в Linux. Чи мені також знадобиться цикл, який ніколи не закінчується в програмі, чи це можна зробити, просто повторивши код кілька разів?


1
Див SO питання: stackoverflow.com/questions/1423345 / ...
MJV

3
"перевіряє певну адресу електронної пошти та передає нові електронні листи зовнішній програмі". Це не те, що робить sendmail? Ви можете визначити псевдонім пошти для перенаправлення поштової скриньки до сценарію. Чому для цього ви не використовуєте псевдоніми пошти?
S.Lott

2
На сучасному Linux, який має, systemdви можете створити системну службу в daemonрежимі, як описано тут . Дивіться також: freedesktop.org/software/systemd/man/systemd.service.html
ccpizza

Якщо система Linux підтримує systemd, скористайтеся описаним тут підходом .
gerardw

Відповіді:


96

Тут у вас є два варіанти.

  1. Зробіть належну роботу cron, яка викликає ваш сценарій. Cron - це загальна назва для демона GNU / Linux, який періодично запускає сценарії відповідно до встановленого вами розкладу. Ви додаєте свій скрипт в crontab або поміщаєте його до спеціального каталогу в спеціальний каталог, і демон виконує завдання запуску його у фоновому режимі. Більше ви можете прочитати у Вікіпедії. Існує безліч різних демонів cron, але ваша система GNU / Linux повинна мати її вже встановленою.

  2. Використовуйте певний підхід python (наприклад, бібліотека) для свого сценарію, щоб мати змогу демонструвати себе. Так, для цього знадобиться простий цикл подій (де ваші події запускаються таймером, можливо, передбачено функцією сну).

Я б не рекомендував вам обирати 2., оскільки ви б насправді повторювали функціональність cron. Системна парадигма Linux полягає в тому, щоб дозволити взаємодію декількох простих інструментів і вирішити ваші проблеми. Якщо немає додаткових причин, чому вам слід зробити демона (крім періодичного запуску), виберіть інший підхід.

Крім того, якщо ви використовуєте демонізування з циклом і трапиться збій, ніхто не перевірятиме пошту після цього (на що вказував Іван Невоструєв у коментарях до цієї відповіді). Хоча якщо сценарій буде доданий як робота з cron, він просто запуститься знову.


7
+1 до кроні. Я не думаю, що питання вказує на те, що він перевіряє локальний обліковий запис пошти, тому фільтри пошти не застосовуються
John La Rooy

Що відбувається, якщо цикл без завершення використовується в програмі Python, а потім буде зареєструвати його у crontabсписку? Якщо я налаштую таке .pyна щогодини, чи це створить багато процесів, які ніколи не припиняться? Якщо так, я думаю, що це цілком сподобається демону.
Век Сяо

Я можу побачити, що cron - це очевидне рішення, якщо ви перевіряєте повідомлення електронної пошти раз на хвилину (що є найнижчою роздільною здатністю для Cron). Але що робити, якщо я хочу перевіряти електронні листи кожні 10 секунд? Чи потрібно написати сценарій Python для запуску запиту 60 разів, це означає, що він закінчується через 50 секунд, а потім дозволити cron запустити сценарій ще через 10 секунд?
Mads Skjern

Я не працював з демонами / службами, але в мене склалося враження, що він (OS / init / init.d / upstart або, як його називають) піклується про перезапуск демона, коли / якщо він закінчиться / вийде з ладу.
Mads Skjern

@VeckHsiao так, crontab викликає скрипт, тому багато екземплярів вашого сценарію python буде викликано всіма його петлями ....
Pipo

71

Ось хороший клас, який взято звідси :

#!/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().
                """

1
це перезапуск при перезапуску системи? тому що при перезапуску системи процес буде вбито так?
ShivaPrasad

58

Вам слід використовувати бібліотеку пітона-демона , вона піклується про все.

Від PyPI: Бібліотека для впровадження добре керованого демонового процесу Unix.


3
Коментар Дітто Хорхе Варгаса. Переглянувши код, він насправді виглядає як непоганий фрагмент коду, однак повна відсутність документів і прикладів робить його дуже важким у використанні, а значить, більшість розробників справедливо ігнорує його за краще задокументовані альтернативи.
Серін

1
Здається, що в Python 3.5 не працює належним чином: gist.github.com/MartinThoma/fa4deb2b4c71ffcd726b24b7ab581ae2
Мартін Тома

Не працює, як очікувалося. Було б добре, якби це все-таки було.
Харлін

Unix! = Linux - це може бути проблемою?
Дана

39

Ви можете використовувати fork (), щоб від'єднати свій скрипт від tty і продовжити його виконання, як:

import os, sys
fpid = os.fork()
if fpid!=0:
  # Running as daemon now. PID is fpid
  sys.exit(0)

Звичайно, вам також потрібно здійснити нескінченну петлю, наприклад

while 1:
  do_your_check()
  sleep(5)

Сподіваюся, що це ви почали.


Привіт, я спробував це, і це працює на мене. Але коли я закриваю термінал або виходжу з сеансу ssh, сценарій також перестає працювати !!
Девід Оквії

@DavidOkwii nohup/ disownкоманди від'єднають процес від консолі, і він не загине. Або ви можете почати це з init.d
pholat

14

Ви також можете змусити сценарій python виконуватись як послуга, використовуючи скрипт оболонки. Спочатку створіть скрипт оболонки, щоб запустити сценарій python таким чином (довільне ім'я сценарію)

#!/bin/sh
script='/home/.. full path to script'
/usr/bin/python $script &

тепер зробіть файл у /etc/init.d/scriptname

#! /bin/sh

PATH=/bin:/usr/bin:/sbin:/usr/sbin
DAEMON=/home/.. path to shell script scriptname created to run python script
PIDFILE=/var/run/scriptname.pid

test -x $DAEMON || exit 0

. /lib/lsb/init-functions

case "$1" in
  start)
     log_daemon_msg "Starting feedparser"
     start_daemon -p $PIDFILE $DAEMON
     log_end_msg $?
   ;;
  stop)
     log_daemon_msg "Stopping feedparser"
     killproc -p $PIDFILE $DAEMON
     PID=`ps x |grep feed | head -1 | awk '{print $1}'`
     kill -9 $PID       
     log_end_msg $?
   ;;
  force-reload|restart)
     $0 stop
     $0 start
   ;;
  status)
     status_of_proc -p $PIDFILE $DAEMON atd && exit 0 || exit $?
   ;;
 *)
   echo "Usage: /etc/init.d/atd {start|stop|restart|force-reload|status}"
   exit 1
  ;;
esac

exit 0

Тепер ви можете запустити і зупинити свій скрипт python, використовуючи команду /etc/init.d/scriptname start або stop.


Я просто спробував це, і виявляється, це почне процес, але він не буде демонізований (тобто він все ще приєднаний до терміналу). Можливо, це буде добре, якщо ви запустили update-rc.d і змусили його працювати під час завантаження (я припускаю, що під час запуску цих скриптів термінал не додається), але він не працює, якщо викликати його вручну. Схоже, нагляд може бути кращим рішенням.
ryuusenshi

13

Проста і підтримуються версією є Daemonize.

Встановіть його з індексу пакета Python (PyPI):

$ pip install daemonize

а потім використовувати:

...
import os, sys
from daemonize import Daemonize
...
def main()
      # your code here

if __name__ == '__main__':
        myname=os.path.basename(sys.argv[0])
        pidfile='/tmp/%s' % myname       # any name
        daemon = Daemonize(app=myname,pid=pidfile, action=main)
        daemon.start()

1
це перезапуск при перезапуску системи? тому що при перезапуску системи процес буде вбито так?
ShivaPrasad

@ShivaPrasad ти знайшов відповідь на це?
1UC1F3R616

перезапуск після перезавантаження системи не є демоновою функцією. використовуйте cron, systemctl, гачки для запуску або інші інструменти для запуску програми при запуску.
fcm

1
@Kush Так, я хотів перезапустити систему після перезавантаження системи або використовувати як команди, я використовував системні функції. Якщо хочу спробувати перевірити цей access.redhat.com/documentation/en-us/red_hat_enterprise_linux/…
ShivaPrasad

@ShivaPrasad Подяки bro
1UC1F3R616

12

cronявно чудовий вибір для багатьох цілей. Однак це не створює службу чи демон, як ви просили в ОП. cronпросто виконує завдання періодично (тобто робота починається і зупиняється), і не частіше одного разу / хвилину. Існують проблеми з cron- наприклад, якщо попередній екземпляр вашого сценарію все ще працює наступного разу, коли cronграфік з’явиться та запустить новий екземпляр, це гаразд? cronне справляється із залежностями; він просто намагається розпочати роботу, коли графік каже.

Якщо ви виявили ситуацію, коли вам справді потрібен демон (процес, який ніколи не припиняється), погляньте на це supervisord. Він надає простий спосіб обернути звичайний недемонізований сценарій або програму і змусити його працювати як демон. Це набагато кращий спосіб, ніж створення рідного демона Python.


9

як щодо використання $nohupкоманди на Linux?

Я використовую його для запуску команд на моєму сервері Bluehost.

Прошу поради, якщо я помиляюся.


Я також використовую це, працює як шарм. "Будь ласка, поради, якщо я помиляюся."
Олександр Мазель

5

Якщо ви використовуєте термінал (ssh або щось подібне) і хочете, щоб тривалий сценарій працював після виходу з терміналу, ви можете спробувати це:

screen

apt-get install screen

створити віртуальний термінал всередині (а саме abc): screen -dmS abc

тепер ми підключаємось до abc: screen -r abc

Отже, тепер ми можемо запустити скрипт python: python keep_sending_mails.py

відтепер ви можете безпосередньо закрити свій термінал, проте сценарій python буде продовжувати працювати, а не закриватися

Оскільки цей keep_sending_mails.pyPID - це дочірній процес віртуального екрану, а не терміналу (ssh)

Якщо ви хочете повернутися назад, перевірте стан запущеного сценарію, ви можете використовувати його screen -r abcще раз


2
Хоча це працює, це дуже швидко і брудно, і його слід уникати у виробництві
pcnate

3

Спочатку читайте псевдоніми пошти. Псевдонім електронної пошти зробить це всередині поштової системи, без того, щоб вам довелося дурити з демонами чи послугами чи чим-небудь подібним.

Ви можете написати простий сценарій, який буде виконуватися sendmail щоразу, коли повідомлення електронної пошти надсилається на певну поштову скриньку.

Дивіться http://www.feep.net/sendmail/tutorial/intro/aliases.html

Якщо ви дійсно хочете написати зайвий складний сервер, ви можете це зробити.

nohup python myscript.py &

Це все, що потрібно. Ваш сценарій просто замикається і спить.

import time
def do_the_work():
    # one round of polling -- checking email, whatever.
while True:
    time.sleep( 600 ) # 10 min.
    try:
        do_the_work()
    except:
        pass

6
Проблема тут полягає в тому, do_the_work()що сценарій може
зірвати

якщо функція do_the_work () виходить з ладу, вона буде викликана знову через 10 хвилин, оскільки лише один виклик функції викликає помилку. Але замість збоїв циклу просто tryчастина виходить з ладу, і except:частина буде викликана замість цього (в цьому випадку нічого), але цикл продовжуватиметься і намагатиметься викликати функцію.
sarbot

3

Якщо припустити, що ви дійсно хочете, щоб ваш цикл працював 24/7 як фонова служба

Для рішення, яке не передбачає введення коду в бібліотеки, ви можете просто створити шаблон служби, оскільки ви використовуєте Linux:

введіть тут опис зображення

Розмістіть цей файл у папці служби демон (як правило /etc/systemd/system/) та встановіть його за допомогою наступних команд systemctl (швидше за все, потрібні будуть права доступу sudo):

systemctl enable <service file name without extension>

systemctl daemon-reload

systemctl start <service file name without extension>

Потім ви можете перевірити, чи працює ваша служба, скориставшись командою:

systemctl | grep running

2

Я б рекомендував це рішення. Вам потрібно успадкувати і переосмислити метод run.

import sys
import os
from signal import SIGTERM
from abc import ABCMeta, abstractmethod



class Daemon(object):
    __metaclass__ = ABCMeta


    def __init__(self, pidfile):
        self._pidfile = pidfile


    @abstractmethod
    def run(self):
        pass


    def _daemonize(self):
        # decouple threads
        pid = os.fork()

        # stop first thread
        if pid > 0:
            sys.exit(0)

        # write pid into a pidfile
        with open(self._pidfile, 'w') as f:
            print >> f, os.getpid()


    def start(self):
        # if daemon is started throw an error
        if os.path.exists(self._pidfile):
            raise Exception("Daemon is already started")

        # create and switch to daemon thread
        self._daemonize()

        # run the body of the daemon
        self.run()


    def stop(self):
        # check the pidfile existing
        if os.path.exists(self._pidfile):
            # read pid from the file
            with open(self._pidfile, 'r') as f:
                pid = int(f.read().strip())

            # remove the pidfile
            os.remove(self._pidfile)

            # kill daemon
            os.kill(pid, SIGTERM)

        else:
            raise Exception("Daemon is not started")


    def restart(self):
        self.stop()
        self.start()

2

щоб створити якусь річ, яка працює як служба, ви можете використовувати цю річ:

Перше, що вам потрібно зробити, це встановити рамку Cement : Робота цементного кадру - це робота з рамками CLI, на якій ви можете розгорнути свою програму.

Інтерфейс командного рядка програми:

interface.py

 from cement.core.foundation import CementApp
 from cement.core.controller import CementBaseController, expose
 from YourApp import yourApp

 class Meta:
    label = 'base'
    description = "your application description"
    arguments = [
        (['-r' , '--run'],
          dict(action='store_true', help='Run your application')),
        (['-v', '--version'],
          dict(action='version', version="Your app version")),
        ]
        (['-s', '--stop'],
          dict(action='store_true', help="Stop your application")),
        ]

    @expose(hide=True)
    def default(self):
        if self.app.pargs.run:
            #Start to running the your app from there !
            YourApp.yourApp()
        if self.app.pargs.stop:
            #Stop your application
            YourApp.yourApp.stop()

 class App(CementApp):
       class Meta:
       label = 'Uptime'
       base_controller = 'base'
       handlers = [MyBaseController]

 with App() as app:
       app.run()

Клас YourApp.py:

 import threading

 class yourApp:
     def __init__:
        self.loger = log_exception.exception_loger()
        thread = threading.Thread(target=self.start, args=())
        thread.daemon = True
        thread.start()

     def start(self):
        #Do every thing you want
        pass
     def stop(self):
        #Do some things to stop your application

Майте на увазі, що ваш додаток має працювати на потоці, щоб бути демоном

Щоб запустити додаток, просто зробіть це в командному рядку

python interface.py --help


1

Використовуйте будь-який диспетчер сервісів, який пропонує ваша система - наприклад, під Ubuntu використовуйте upstart . Це дозволить обробити всі деталі для вас, такі як запуск при завантаженні, перезапуск при збої тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.