วิธีทำให้สคริปต์ Python ทำงานได้เหมือนบริการหรือดีมอนใน Linux


175

ฉันได้เขียนสคริปต์ Python ที่ตรวจสอบที่อยู่อีเมลที่แน่นอนและส่งอีเมลใหม่ไปยังโปรแกรมภายนอก ฉันจะทำให้สคริปต์นี้ทำงาน 24 ชั่วโมงเช่นเปลี่ยนเป็น daemon หรือบริการใน Linux ได้อย่างไร ฉันจะต้องวนซ้ำที่ไม่สิ้นสุดในโปรแกรมหรือสามารถทำได้โดยเพียงแค่มีรหัสดำเนินการหลายครั้ง?


1
ดูคำถาม SO: stackoverflow.com/questions/1423345/…
mjv

3
"ตรวจสอบที่อยู่อีเมลที่แน่นอนและส่งอีเมลใหม่ไปยังโปรแกรมภายนอก" นั่นไม่ใช่ sendmail อะไร คุณสามารถกำหนดชื่อแทนอีเมลเพื่อกำหนดเส้นทางกล่องจดหมายไปยังสคริปต์ เหตุใดคุณจึงไม่ใช้นามแฝงอีเมลเพื่อทำสิ่งนี้
S.Lott

2
บนลินุกซ์ที่ทันสมัยซึ่งมีsystemdคุณสามารถสร้างบริการ systemd ในdaemonโหมดตามที่อธิบายไว้ที่นี่ ดูเพิ่มเติมที่: freedesktop.org/software/systemd/man/systemd.service.html
ccpizza

หากระบบ Linux สนับสนุน systemd ใช้วิธีการที่แสดงไว้ที่นี่
gerardw

คำตอบ:


96

คุณมีสองตัวเลือกที่นี่

  1. สร้างงาน cronที่เหมาะสมซึ่งเรียกสคริปต์ของคุณ Cron เป็นชื่อสามัญสำหรับ GNU / Linux daemon ที่เปิดตัวสคริปต์เป็นระยะตามกำหนดการที่คุณตั้งไว้ คุณเพิ่มสคริปต์ของคุณลงใน crontab หรือวาง symlink ลงในไดเรกทอรีพิเศษและ daemon จะจัดการงานในการเปิดตัวในพื้นหลัง คุณสามารถอ่านเพิ่มเติมได้ที่ Wikipedia มี cron daemons ที่แตกต่างหลากหลาย แต่ระบบ GNU / Linux ของคุณควรติดตั้งไว้แล้ว

  2. ใช้วิธีไพ ธ อนบางอย่าง(เช่นไลบรารี) เพื่อให้สคริปต์ของคุณสามารถ daemonize เองได้ ใช่มันจะต้องมีวงเหตุการณ์ที่เรียบง่าย (ที่เหตุการณ์ของคุณมีการเรียกใช้ตัวจับเวลาอาจมีให้โดยฟังก์ชั่นการนอนหลับ)

ฉันจะไม่แนะนำให้คุณเลือก 2. เพราะในความเป็นจริงแล้วคุณจะใช้ฟังก์ชัน cron ซ้ำ กระบวนทัศน์ของระบบ Linux คือการให้เครื่องมือง่ายๆหลายอย่างโต้ตอบและแก้ไขปัญหาของคุณ หากไม่มีเหตุผลเพิ่มเติมว่าทำไมคุณควรสร้างภูต (นอกเหนือจากทริกเกอร์เป็นระยะ) ให้เลือกวิธีอื่น

นอกจากนี้หากคุณใช้ daemonize ด้วยการวนซ้ำและเกิดความผิดพลาดจะไม่มีใครตรวจสอบอีเมลหลังจากนั้น (ดังที่Ivan Nevostruevชี้ให้เห็นในการแสดงความคิดเห็นต่อคำตอบนี้ ) ในขณะที่ถ้าสคริปต์ถูกเพิ่มเป็นงาน cron มันจะทริกเกอร์อีกครั้ง


7
+1 กับ cronjob ฉันไม่คิดว่าคำถามจะระบุว่ากำลังตรวจสอบบัญชีอีเมลในเครื่องดังนั้นตัวกรองจดหมายจึงไม่สามารถใช้งานได้
John La Rooy

จะเกิดอะไรขึ้นใช้การวนซ้ำโดยไม่มีการยกเลิกในโปรแกรม Python แล้วลงทะเบียนในcrontabรายการจะเป็นอย่างไร ถ้าฉันตั้งค่าดังกล่าว.pyทุกชั่วโมงมันจะสร้างกระบวนการมากมายที่จะไม่สิ้นสุดหรือไม่ ถ้าเป็นเช่นนั้นฉันคิดว่านี่จะค่อนข้างเป็นภูต
Veck Hsiao

ฉันเห็นว่า cron เป็นทางออกที่ชัดเจนหากคุณตรวจสอบอีเมลทุกครั้ง (ซึ่งเป็นวิธีแก้ไขเวลาที่ต่ำที่สุดสำหรับ Cron) แต่ถ้าฉันต้องการตรวจสอบอีเมลทุกๆ 10 วินาทีล่ะ ฉันควรเขียนสคริปต์ Python เพื่อเรียกใช้แบบสอบถาม 60 ครั้งซึ่งหมายความว่าจะสิ้นสุดหลังจาก 50 วินาทีจากนั้นให้ cron เริ่มสคริปต์อีกครั้ง 10 วินาทีในภายหลัง
Mads Skjern

ฉันไม่ได้ทำงานกับ daemons / services แต่ฉันรู้สึกว่ามัน (OS / init / init.d / upstart หรือสิ่งที่เรียกว่า) ดูแลการเริ่มต้น daemon เมื่อ / ถ้ามันจบลง / ล่ม
Mads Skjern

@VeckHsiao ใช่ crontab เรียกสคริปต์จำนวนมากดังนั้นกรณีของสคริปต์หลามของคุณจะถูกเรียกว่ามีทุกคนห่วงของมัน ....
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

คุณควรใช้ไลบรารีpython-daemonดูแลทุกอย่าง

จาก PyPI: Library เพื่อใช้ Unix daemon


3
ความคิดเห็นของ Ditto Jorge Vargas หลังจากดูรหัสแล้วดูเหมือนว่าจะเป็นโค้ดที่ดี แต่การขาดเอกสารและตัวอย่างที่สมบูรณ์ทำให้ยากต่อการใช้งานมากซึ่งหมายความว่านักพัฒนาส่วนใหญ่จะไม่สนใจมันเพื่อเป็นทางเลือกที่ดีกว่า
Cerin

1
ดูเหมือนว่าจะไม่ทำงานอย่างถูกต้องใน Python 3.5: gist.github.com/MartinThoma/fa4deb2b4c71ffcd726b24b7b5ab581ae2
Martin Thoma

ไม่ทำงานตามที่คาดไว้ มันจะดีถ้ามันทำ
Harlin

Unix! = Linux - อาจเป็นปัญหาหรือไม่
Dana

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 สคริปต์ก็จะหยุดทำงานด้วย !!
David Okwii

@DavidOkwii nohup/ disownคำสั่งจะแยกกระบวนการจากคอนโซลและมันจะไม่ตาย หรือคุณสามารถเริ่มด้วย init.d
pholat

14

คุณสามารถทำให้สคริปต์ 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 เริ่มหรือหยุด


ฉันเพิ่งลองทำสิ่งนี้และปรากฎว่าสิ่งนี้จะเริ่มต้นกระบวนการ แต่มันจะไม่ถูก daemonized (เช่นยังคงเชื่อมต่อกับเทอร์มินัล) มันอาจจะทำงานได้ดีถ้าคุณรัน update-rc.d และทำให้มันทำงานในการบูต (ฉันคิดว่าไม่มีเทอร์มินัลต่ออยู่เมื่อสคริปต์เหล่านี้ทำงาน) แต่มันไม่ทำงานถ้าคุณเรียกใช้ด้วยตนเอง ดูเหมือนว่า supervisord อาจเป็นทางออกที่ดีกว่า
ryuusenshi

13

ที่ง่ายและได้รับการสนับสนุนรุ่นDaemonizeคือ

ติดตั้งจาก Python Package Index (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, hooks เริ่มต้นหรือเครื่องมืออื่น ๆ เพื่อเรียกใช้แอปของคุณเมื่อเริ่มต้น
fcm

1
@Kush ใช่ฉันต้องการที่จะเริ่มต้นใหม่หลังจากรีสตาร์ทระบบหรือการใช้คำสั่งเช่นผมใช้ฟังก์ชั่น systemd ถ้าต้องการที่จะลองตรวจสอบเรื่องนี้access.redhat.com/documentation/en-us/red_hat_enterprise_linux/...
ShivaPrasad

@ShivaPrasad Thanks bro
1UC1F3R616

12

cronเป็นตัวเลือกที่ดีอย่างชัดเจนสำหรับวัตถุประสงค์หลายประการ อย่างไรก็ตามมันไม่ได้สร้างบริการหรือภูตตามที่คุณร้องขอใน OP cronเพียงแค่เรียกใช้งานเป็นระยะ (หมายถึงงานเริ่มต้นและหยุด) และไม่บ่อยกว่าหนึ่งครั้ง / นาที มีปัญหากับcron- ตัวอย่างเช่นหากอินสแตนซ์ก่อนหน้าของสคริปต์ของคุณยังคงทำงานในครั้งต่อไปที่cronกำหนดมาและเปิดตัวอินสแตนซ์ใหม่นั่นคือตกลงหรือไม่ cronไม่ได้จัดการการพึ่งพา มันแค่พยายามเริ่มงานเมื่อถึงเวลาที่กำหนด

หากคุณพบว่าสถานการณ์ที่คุณต้องการอย่างแท้จริงภูต (กระบวนการที่ไม่เคยหยุดทำงาน) supervisordจะดูที่ มันมีวิธีง่ายๆในการหุ้มสคริปต์หรือโปรแกรมปกติที่ไม่ใช่ daemonized และทำให้มันทำงานเหมือน daemon นี่เป็นวิธีที่ดีกว่าการสร้าง Python ดั้งเดิม


9

วิธีการเกี่ยวกับการใช้$nohupคำสั่งบน linux

ฉันใช้เพื่อเรียกใช้คำสั่งของฉันบนเซิร์ฟเวอร์ Bluehost

กรุณาแนะนำถ้าฉันผิด


ฉันใช้มันทำงานเหมือนจับใจ "กรุณาให้คำแนะนำหากฉันผิด"
Alexandre Mazel

5

หากคุณกำลังใช้เทอร์มินัล (ssh หรืออะไรบางอย่าง) และคุณต้องการให้สคริปต์ทำงานเป็นเวลานานหลังจากที่คุณออกจากระบบคุณสามารถลอง:

screen

apt-get install screen

สร้าง terminal เสมือนภายใน (คือ abc): screen -dmS abc

ตอนนี้เราเชื่อมต่อกับ abc: screen -r abc

ดังนั้นตอนนี้เราสามารถเรียกใช้สคริปต์หลาม: python keep_sending_mails.py

จากนี้ไปคุณสามารถปิดเทอร์มินัลของคุณได้โดยตรงอย่างไรก็ตามสคริปต์ python จะทำงานต่อไปแทนที่จะปิดตัวลง

เนื่องจากkeep_sending_mails.pyPID นี้เป็นกระบวนการลูกของหน้าจอเสมือนจริงมากกว่าเทอร์มินัล (ssh)

หากคุณต้องการกลับไปตรวจสอบสถานะการทำงานของสคริปต์คุณสามารถใช้screen -r abcอีกครั้ง


2
ในขณะที่ใช้งานได้อย่างรวดเร็วและสกปรกและควรหลีกเลี่ยงในการผลิต
pcnate

3

ก่อนอื่นให้อ่านชื่อแทนจดหมาย นามแฝงอีเมลจะทำสิ่งนี้ภายในระบบอีเมลโดยที่คุณไม่ต้องไปไหนกับ daemons หรือบริการหรืออะไรก็ตาม

คุณสามารถเขียนสคริปต์ง่าย ๆ ที่จะถูกดำเนินการโดย 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()สามารถผิดพลาดสคริปต์และไม่มีใครวิ่งอีกครั้ง
อีวาน Nevostruev

หากฟังก์ชัน do_the_work () ขัดข้องระบบจะเรียกใช้อีกครั้งหลังจากผ่านไป 10 นาทีเนื่องจากการเรียกใช้ฟังก์ชันเดียวจะทำให้เกิดข้อผิดพลาด แต่แทนที่จะหยุดการวนซ้ำเพียงtryส่วนที่ล้มเหลวและexcept:ส่วนจะถูกเรียกแทน (ในกรณีนี้ไม่มีอะไร) แต่วงจะดำเนินการต่อและพยายามเรียกใช้ฟังก์ชันต่อไป
sarbot

3

สมมติว่าคุณต้องการให้วงของคุณทำงานตลอด 24 ชั่วโมงทุกวันเป็นบริการพื้นหลัง

สำหรับโซลูชันที่ไม่เกี่ยวข้องกับการฉีดโค้ดของคุณกับไลบรารี่คุณสามารถสร้างเทมเพลตบริการเนื่องจากคุณใช้ linux:

ป้อนคำอธิบายรูปภาพที่นี่

วางไฟล์นั้นในโฟลเดอร์บริการ daemon ของคุณ (โดยปกติ/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

เพื่อสร้างบางสิ่งที่ทำงานเหมือนบริการคุณสามารถใช้สิ่งนี้:

สิ่งแรกที่คุณต้องทำคือติดตั้งซีเมนต์เฟรมเวิร์กของ : โครงเฟรมซีเมนต์คือโครงเฟรม 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

โปรดทราบว่าแอปของคุณต้องทำงานบนเธรดเพื่อให้เป็นดีมอน

หากต้องการเรียกใช้แอปให้ทำในบรรทัดคำสั่ง

หลาม interface.py - ช่วย


1

ใช้สิ่งที่ผู้จัดการบริการข้อเสนอระบบของคุณ - ตัวอย่างเช่นภายใต้ Ubuntu ใช้พุ่งพรวด การดำเนินการนี้จะจัดการรายละเอียดทั้งหมดสำหรับคุณเช่นเริ่มการบูทรีสตาร์ทเมื่อเกิดข้อผิดพลาดเป็นต้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.