시행착오/[python]

[python] 파이썬 daemon 데몬 만들기

bled 2021. 5. 6. 15:07

파이썬으로 백그라운드에서 계속 동작하게 하는 프로그램을 실행시키거나 끌때

nohup 과 pkill 을 써서 해도 되나 너무 번거롭고 기존에 데몬이 돌아가고 있는지 체크되지 않으므로

인터넷에서 조사하다가 Sander Marechal 란 사람이 만든 샘플코드가 도움이 되서 기록으로 남김

stackoverflow.com/questions/473620/how-do-you-create-a-daemon-in-python

www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/


아래는 위 샘플코드로 간단하게 +1 증가하는 로그 남기는 데몬 프로그램 (리눅스)

python main.py start => 시작

python main.py stop => 정지


[daemon.py]

"""Generic linux daemon base class for python 3.x."""

import sys, os, time, atexit, signal

class Daemon:
	"""A generic daemon class.

	Usage: subclass the daemon class and override the run() method."""

	def __init__(self, pidfile): self.pidfile = pidfile
	
	def daemonize(self):
		"""Deamonize class. UNIX double fork mechanism."""

		try: 
			pid = os.fork() 
			if pid > 0:
				# exit first parent
				sys.exit(0) 
		except OSError as err: 
			sys.stderr.write('fork #1 failed: {0}\n'.format(err))
			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 as err: 
			sys.stderr.write('fork #2 failed: {0}\n'.format(err))
			sys.exit(1) 
	
		# redirect standard file descriptors
		sys.stdout.flush()
		sys.stderr.flush()
		si = open(os.devnull, 'r')
		so = open(os.devnull, 'a+')
		se = open(os.devnull, 'a+')

		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())
		with open(self.pidfile,'w+') as f:
			f.write(pid + '\n')
	
	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:
			with open(self.pidfile,'r') as pf:

				pid = int(pf.read().strip())
		except IOError:
			pid = None
	
		if pid:
			message = "pidfile {0} already exist. " + \
					"Daemon already running?\n"
			sys.stderr.write(message.format(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:
			with open(self.pidfile,'r') as pf:
				pid = int(pf.read().strip())
		except IOError:
			pid = None
	
		if not pid:
			message = "pidfile {0} does not exist. " + \
					"Daemon not running?\n"
			sys.stderr.write(message.format(self.pidfile))
			return # not an error in a restart

		# Try killing the daemon process	
		try:
			while 1:
				os.kill(pid, signal.SIGTERM)
				time.sleep(0.1)
		except OSError as err:
			e = str(err.args)
			if e.find("No such process") > 0:
				if os.path.exists(self.pidfile):
					os.remove(self.pidfile)
			else:
				print (str(err.args))
				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()."""

 

[logger.py]

참고 bled214.tistory.com/270

# 로그

import gzip
import os
import logging.handlers

# 로그파일 압축 설정
class GZipRotator:
    def __call__(self, source, dest):
        os.rename(source, dest)
        with open(dest, 'rb') as f_in:
            with gzip.open("%s.gz" % dest, 'wb') as f_out:
                f_out.writelines(f_in)
        os.remove(dest)

# 현재 파일 경로
current_dir = os.path.dirname(os.path.realpath(__file__))
# 로그 저장할 폴더 logs 생성
log_dir = '{}/logs'.format(current_dir)
if not os.path.exists(log_dir):
    os.makedirs(log_dir)

myLogger = logging.getLogger('test_log')
myLogger.setLevel(logging.DEBUG)  # 로깅 수준 지정


file_handler = logging.handlers.TimedRotatingFileHandler(
    filename='./logs/test_log', when='midnight', interval=1, encoding='utf-8'
)
file_handler.suffix = '%Y-%m-%d'  # 파일명 끝에 붙여줌; ex. log-20190811

myLogger.addHandler(file_handler)

formatter = logging.Formatter(
    '%(asctime)s|%(levelname)s|%(filename)s|%(funcName)s()|line:%(lineno)d|%(message)s'
    # '%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] %(message)s'
)
file_handler.setFormatter(formatter)
file_handler.rotator = GZipRotator()

 

[main.py]

#!/usr/bin/env python

import sys, time
from daemon import Daemon
import logger

def do_something(i):
    logger.myLogger.info(i)

class MyDaemon(Daemon):

    def run(self):
        i = 1
        while True:
            do_something(i)
            time.sleep(1)

if __name__ == "__main__":
    daemon = MyDaemon('/tmp/daemon-example.pid')
    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            daemon.start()
        elif 'stop' == sys.argv[1]:
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            daemon.restart()
        else:
            print ("Unknown command")
            sys.exit(2)
        sys.exit(0)
    else:
        print ("usage: %s start|stop|restart" % sys.argv[0])
        sys.exit(2)