Jak sprawić, by skrypt w Pythonie działał jak usługa lub demon w systemie Linux

175

Napisałem skrypt w Pythonie, który sprawdza określony adres e-mail i przekazuje nowe wiadomości e-mail do zewnętrznego programu. Jak mogę sprawić, aby ten skrypt działał 24 godziny na dobę, 7 dni w tygodniu, na przykład zamieniając go w demona lub usługę w systemie Linux. Czy potrzebowałbym również pętli, która nigdy nie kończy się w programie, czy można to zrobić, po prostu wykonując kod wielokrotnie ponownie?

adhanlon
źródło
1
Zobacz pytanie SO: stackoverflow.com/questions/1423345/ ...
mjv
3
„sprawdza określony adres e-mail i przekazuje nowe e-maile do zewnętrznego programu” Czy to nie to, co robi sendmail? Możesz zdefiniować alias poczty, aby kierować skrzynkę pocztową do skryptu. Dlaczego nie używasz do tego aliasów pocztowych?
S.Lott
2
Na nowoczesnym Linuksie, który ma systemd, możesz utworzyć usługę systemową w daemontrybie opisanym tutaj . Zobacz też: freedesktop.org/software/systemd/man/systemd.service.html
ccpizza
Jeśli system linux obsługuje systemd, zastosuj podejście opisane tutaj .
gerardw

Odpowiedzi:

96

Masz dwie możliwości.

  1. Wykonaj odpowiednie zadanie cron, które wywoła twój skrypt. Cron to popularna nazwa demona GNU / Linux, który okresowo uruchamia skrypty zgodnie z ustalonym harmonogramem. Dodajesz swój skrypt do crontab lub umieszczasz do niego dowiązanie symboliczne do specjalnego katalogu, a demon wykonuje zadanie uruchomienia go w tle. Możesz przeczytać więcej na Wikipedii. Istnieje wiele różnych demonów cron, ale Twój system GNU / Linux powinien mieć je już zainstalowane.

  2. Użyj jakiegoś podejścia w Pythonie (na przykład biblioteki), aby twój skrypt mógł się zdemonizować. Tak, będzie to wymagało prostej pętli zdarzeń (gdzie zdarzenia są wyzwalane przez timer, być może zapewnione przez funkcję uśpienia).

Nie radziłbym ci wybierać 2., ponieważ w rzeczywistości powtarzałbyś funkcjonalność crona. Paradygmat systemu Linux polega na umożliwieniu współdziałania wielu prostych narzędzi i rozwiązywania problemów. Jeśli nie ma dodatkowych powodów, dla których powinieneś tworzyć demona (oprócz okresowego uruchamiania), wybierz inne podejście.

Ponadto, jeśli użyjesz demonizacji z pętlą i nastąpi awaria, nikt nie będzie sprawdzał poczty później (jak wskazał Ivan Nevostruev w komentarzach do tej odpowiedzi). Jeśli skrypt zostanie dodany jako zadanie cron, po prostu uruchomi się ponownie.

P Shved
źródło
7
+1 do cronjob. Nie wydaje mi się, że pytanie określa, że ​​chodzi o sprawdzenie lokalnego konta pocztowego, więc filtry poczty nie mają zastosowania
John La Rooy
Co się stanie, jeśli użyjesz pętli bez zakończenia w programie w Pythonie, a następnie zarejestrujesz ją na crontabliście? Jeśli skonfiguruję taki .pyna godzinę, czy utworzy wiele procesów, które nigdy nie zostaną zakończone? Jeśli tak, myślę, że to całkiem spodobałoby się demonowi.
Veck Hsiao
Widzę, że cron jest oczywistym rozwiązaniem, jeśli sprawdzasz e-maile raz na minutę (co jest najniższą rozdzielczością czasową dla Crona). Ale co, jeśli chcę sprawdzać e-maile co 10 sekund? Czy powinienem napisać skrypt w języku Python, aby uruchamiał zapytanie 60 razy, co oznacza, że ​​kończy się ono po 50 sekundach, a następnie pozwolić cronowi ponownie uruchomić skrypt 10 sekund później?
Mads Skjern
Nie pracowałem z demonami / usługami, ale miałem wrażenie, że to (OS / init / init.d / upstart lub jak to się nazywa) dba o ponowne uruchomienie demona, gdy / jeśli się zakończy / ulegnie awarii.
Mads Skjern,
@VeckHsiao tak, crontab wywołuje skrypt, więc wiele instancji twojego skryptu Pythona będzie wywoływanych z każdą jego pętlą ....
Pipo
71

Oto fajna klasa, która została wzięta stąd :

#!/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().
                """
the_drow
źródło
1
czy jest restartowany po ponownym uruchomieniu systemu? ponieważ po ponownym uruchomieniu systemu proces zostanie zabity, prawda?
ShivaPrasad
58

Powinieneś użyć biblioteki python-daemon , ona dba o wszystko.

Z PyPI: Library, aby zaimplementować dobrze działający proces demona Unix.

Prody
źródło
3
Komentarz Ditto Jorge Vargasa. Po przyjrzeniu się kodowi wygląda na całkiem niezły fragment kodu, ale całkowity brak dokumentacji i przykładów sprawia, że ​​jest bardzo trudny w użyciu, co oznacza, że ​​większość programistów słusznie zignoruje go, aby uzyskać lepiej udokumentowane alternatywy.
Cerin
1
Wydaje się, że nie działa poprawnie w Pythonie 3.5: gist.github.com/MartinThoma/fa4deb2b4c71ffcd726b24b7ab581ae2
Martin Thoma
Nie działa zgodnie z oczekiwaniami. Byłoby miło, gdyby tak się stało.
Harlin
Unix! = Linux - czy to może być problem?
Dana
39

Możesz użyć fork (), aby odłączyć swój skrypt od terminala i kontynuować jego działanie, na przykład:

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

Oczywiście musisz również zaimplementować nieskończoną pętlę, na przykład

while 1:
  do_your_check()
  sleep(5)

Mam nadzieję, że to się zaczęło.

jhwist
źródło
Witam, próbowałem tego i działa na mnie. Ale kiedy zamykam terminal lub wychodzę z sesji ssh, skrypt również przestaje działać !!
David Okwii
@DavidOkwii nohup/ disowncommands odłączy proces od konsoli i nie umrze. Lub możesz zacząć od
init.d
14

Możesz również uruchomić skrypt Pythona jako usługę za pomocą skryptu powłoki. Najpierw utwórz skrypt powłoki, aby uruchomić skrypt Pythona w ten sposób (dowolna nazwa skryptu)

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

teraz utwórz plik w /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

Teraz możesz uruchomić i zatrzymać skrypt Pythona za pomocą polecenia /etc/init.d/scriptname start lub stop.

Kishore K.
źródło
Właśnie tego spróbowałem i okazuje się, że to rozpocznie proces, ale nie zostanie zdemonizowany (tj. Nadal jest podłączony do terminala). Prawdopodobnie zadziałałoby dobrze, gdybyś uruchomił update-rc.d i uruchomił go podczas rozruchu (zakładam, że nie ma podłączonego terminala, gdy te skrypty są uruchamiane), ale nie działa, jeśli wywołasz go ręcznie. Wydaje się, że lepszym rozwiązaniem może być przełożony.
ryuusenshi
13

Prosta i obsługiwana wersja to Daemonize.

Zainstaluj go z Python Package Index (PyPI):

$ pip install daemonize

a następnie użyj:

...
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()
fcm
źródło
1
czy jest restartowany po ponownym uruchomieniu systemu? ponieważ po ponownym uruchomieniu systemu proces zostanie zabity, prawda?
ShivaPrasad
@ShivaPrasad u znalazłeś odpowiedź?
1UC1F3R616
restart po restarcie systemu nie jest funkcją demona. używaj cron, systemctl, haków startowych lub innych narzędzi do uruchamiania aplikacji podczas uruchamiania.
fcm
1
@Kush tak, chciałem zrestartować po ponownym uruchomieniu systemu lub użyć podobnych poleceń Użyłem funkcji systemd.Jeśli chcesz spróbować, sprawdź to access.redhat.com/documentation/en-us/red_hat_enterprise_linux/ ...
ShivaPrasad
@ShivaPrasad Dzięki bracie
1UC1F3R616
12

cronjest z pewnością doskonałym wyborem do wielu celów. Jednak nie tworzy usługi ani demona, jak zażądałeś w OP. cronpo prostu uruchamia zadania okresowo (co oznacza, że ​​zadanie jest uruchamiane i zatrzymywane) i nie częściej niż raz na minutę. Występują problemy cron- na przykład, jeśli poprzednia instancja skryptu nadal działa, gdy następnym razem cronpojawi się harmonogram i uruchomi nową instancję, czy to w porządku? cronnie obsługuje zależności; po prostu próbuje rozpocząć pracę, gdy nakazuje to harmonogram.

Jeśli znajdziesz sytuację, w której naprawdę potrzebujesz demona (procesu, który nigdy nie przestaje działać), spójrz na supervisord. Zapewnia prosty sposób na opakowanie normalnego, nie demonizowanego przez demona skryptu lub programu i sprawienie, aby działał jak demon. To znacznie lepszy sposób niż tworzenie natywnego demona Pythona.

Chris Johnson
źródło
9

co powiesz na użycie $nohuppolecenia w systemie Linux?

Używam go do uruchamiania poleceń na moim serwerze Bluehost.

Proszę o poradę, jeśli się mylę.

faisal00813
źródło
Używam tego również, działa jak urok. „Proszę o radę, jeśli się mylę”.
Alexandre Mazel
5

Jeśli używasz terminala (ssh lub czegoś podobnego) i chcesz, aby skrypt działał przez długi czas po wylogowaniu się z terminala, możesz spróbować tego:

screen

apt-get install screen

utwórz wirtualny terminal wewnątrz (a mianowicie abc): screen -dmS abc

teraz łączymy się z abc: screen -r abc

Więc teraz możemy uruchomić skrypt w Pythonie: python keep_sending_mails.py

od teraz możesz bezpośrednio zamknąć terminal, jednak skrypt Pythona będzie działał, a nie zostanie zamknięty

Ponieważ ten keep_sending_mails.pyPID jest procesem potomnym wirtualnego ekranu, a nie terminala (ssh)

Jeśli chcesz wrócić, sprawdź stan działania skryptu, możesz użyć screen -r abcponownie

Microos
źródło
2
chociaż to działa, jest bardzo szybkie i brudne i należy go unikać w produkcji
pcnate
3

Najpierw przeczytaj aliasy poczty. Alias ​​pocztowy zrobi to wewnątrz systemu pocztowego bez konieczności wygłupywania się z demonami, usługami lub czymkolwiek w tym rodzaju.

Możesz napisać prosty skrypt, który będzie wykonywany przez sendmail za każdym razem, gdy wiadomość zostanie wysłana do określonej skrzynki pocztowej.

Zobacz http://www.feep.net/sendmail/tutorial/intro/aliases.html

Jeśli naprawdę chcesz napisać niepotrzebnie złożony serwer, możesz to zrobić.

nohup python myscript.py &

To wszystko, czego potrzeba. Twój skrypt po prostu zapętla się i śpi.

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
S.Lott
źródło
6
Problem polega na tym, że do_the_work()może zawiesić skrypt i nikt go nie uruchomi
Ivan Nevostruev
jeśli funkcja do_the_work () ulegnie awarii, zostanie wywołana ponownie po 10 minutach, ponieważ tylko jedno wywołanie funkcji powoduje błąd. Ale zamiast przerywać pętlę, tylko tryczęść zawodzi i except:część zostanie wywołana zamiast tego (w tym przypadku nic), ale pętla będzie kontynuowana i będzie próbowała wywołać funkcję.
sarbot
3

Zakładając, że naprawdę chciałbyś, aby Twoja pętla działała 24/7 jako usługa w tle

W przypadku rozwiązania, które nie obejmuje wstrzykiwania kodu za pomocą bibliotek, możesz po prostu utworzyć szablon usługi, ponieważ używasz systemu Linux:

wprowadź opis obrazu tutaj

Umieść ten plik w folderze usługi demona (zwykle /etc/systemd/system/) i zainstaluj go za pomocą następujących poleceń systemctl (prawdopodobnie będzie wymagać uprawnień sudo):

systemctl enable <service file name without extension>

systemctl daemon-reload

systemctl start <service file name without extension>

Następnie możesz sprawdzić, czy Twoja usługa działa, używając polecenia:

systemctl | grep running
Heitor Castro
źródło
2

Poleciłbym to rozwiązanie. Musisz dziedziczyć i przesłonić metodę 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()
Fomalhaut
źródło
2

aby stworzyć coś, co działa jak usługa, możesz użyć tego:

Pierwszą rzeczą, którą musisz zrobić, jest zainstalowanie frameworka Cement : szkielet Cement to szkielet interfejsu CLI, na którym można wdrożyć aplikację.

interfejs wiersza poleceń aplikacji:

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()

Klasa 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

Pamiętaj, że aby aplikacja była demonem, musi działać w wątku

Aby uruchomić aplikację, zrób to w linii poleceń

python interface.py --help

Manouchehr Rasouli
źródło
1

Użyj dowolnego menedżera usług, który oferuje twój system - na przykład w systemie Ubuntu użyj upstart . Spowoduje to obsługę wszystkich szczegółów, takich jak uruchamianie podczas rozruchu, ponowne uruchamianie w przypadku awarii itp.

Richard
źródło