#!/usr/bin/env python3
#########################################################################
# Copyright 2016- Christian Strassburg c.strassburg@gmx.de
#########################################################################
# This file is part of SmartHomeNG.
# https://github.com/smarthomeNG/smarthome
#
# SmartHomeNG.py is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SmartHomeNG.py is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SmartHomeNG.py. If not, see <http://www.gnu.org/licenses/>.
#########################################################################
"""
This file contains the functions needed to run SmartHomeNG as a daemon
"""
import logging
import os
import sys
import psutil
import errno
import portalocker
logger = logging.getLogger(__name__)
[Doku]def daemonize(pidfile,stdin='/dev/null', stdout='/dev/null', stderr=None):
"""
This method daemonizes the sh.py process and redirects standard file descriptors.
:param pidfile: Path to pidfile
:param stdin: Path to new stdin, default value is "/dev/null"
:param stdout: Path to new stdout, default value is "/dev/null"
:param stderr: Path to new stderr, default value is None, but if stderr is None it is mapped to stdout
:type pidfile: string
:type stdin: string
:type stdout: string
:type stderr: string
"""
if os.name == 'nt':
return
# use stdout file if stderr is none
if (not stderr):
stderr = stdout
# do the UNIX double-fork magic, see Stevens' "Advanced
# Programming in the UNIX Environment" for details (ISBN 0201563177)
try:
pid = os.fork()
if pid > 0:
# exit first parent
sys.exit(0)
except OSError as e:
print("fork #1 failed: %d (%s)" % (e.errno, e.strerror) , file=sys.stderr)
sys.exit(1)
# decouple from parent environment
os.setsid()
os.umask(0)
# do second fork
try:
pid = os.fork()
if pid > 0:
# exit from second parent, print eventual PID before
print ("Daemon PID %d" % pid )
sys.exit(0)
else:
write_pidfile(os.getpid(), pidfile)
except OSError as e:
print("fork #2 failed: %d (%s)" % (e.errno, e.strerror) , file=sys.stderr)
sys.exit(1)
# Redirect standard file descriptors.
si = open(stdin, 'r')
so = open(stdout, 'a+')
se = open(stderr, 'a+')
os.close(sys.stdin.fileno())
os.close(sys.stdout.fileno())
os.close(sys.stderr.fileno())
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
[Doku]def remove_pidfile(pidfile):
"""
This method removes the pidfile.
:param pidfile: Name of the pidfile to write to
:type pidfile: str
"""
if os.path.exists(pidfile):
os.remove(pidfile)
[Doku]def write_pidfile(pid, pidfile):
"""
This method writes the PID to the pidfile and locks it while the process is running.
:param pid: PID of SmartHomeNG
:param pidfile: Name of the pidfile to write to
:type pid: int
:type pidfile: str
"""
if os.name == 'nt':
return
with open(pidfile, 'w+') as fh:
fh.write("%s" % pid)
global _pidfile_handle
try:
_pidfile_handle = open(pidfile, 'r')
#print(f"_pidfile_handle = '{_pidfile_handle}'")
# LOCK_EX - acquire an exclusive lock
# LOCK_NB - non blocking
portalocker.lock(_pidfile_handle, portalocker.LOCK_EX | portalocker.LOCK_NB)
# don't close _pidfile_handle or lock is gone!!!
except portalocker.AlreadyLocked as e:
print("Could not lock pid file: %d (%s)" % (e.errno, e.strerror) , file=sys.stderr)
[Doku]def read_pidfile(pidfile):
"""
This method reads the pidfile and returns the PID.
:param pidfile: Name of the pidfile to check
:type pidfile: str
:return: PID of SmartHomeNG or 0 if it is not running
:rtype: int
"""
try:
if os.path.isfile(pidfile):
fd = open(pidfile,'r')
line = fd.readline()
pid = int(line)
return pid
except ValueError:
logger.warning("PID could not be read, maybe a false write or a corrupt filesystem? Please check the file system ASAP!")
return 0
[Doku]def check_sh_is_running(pidfile):
"""
This method checks whether another smarthome process process is already running.
:param pidfile: Name of the pidfile to check
:type pidfile: str
:return: True: if SmartHomeNG is running, False: if SmartHome is not running
:rtype: bool
"""
pid = read_pidfile(pidfile)
#print("daemon.check_sh_is_running: pidfile={}, pid={}, psutil.pid_exists(pid)={}".format(pidfile, pid, psutil.pid_exists(pid)))
isRunning = False
if pid > 0 and psutil.pid_exists(pid):
#print("daemon.check_sh_is_running: pid={}, psutil.pid_exists(pid)={}".format(pid, psutil.pid_exists(pid)))
try:
fh = open(pidfile, 'r')
# LOCK_EX - acquire an exclusive lock
# LOCK_NB - non blocking
portalocker.lock(fh, portalocker.LOCK_EX | portalocker.LOCK_NB)
print("daemon.check_sh_is_running: portalocker.lock erfolgreich")
# pidfile not locked, so sh is terminated
except portalocker.LockException:
isRunning = True
finally:
if fh:
fh.close()
return isRunning
[Doku]def kill(pidfile, waittime=15, pid0_warning=True):
"""
This method kills the process identified by pidfile.
:param pidfile: Name of the pidfile identifying the process to kill
:param waittime: Number of seconds to wait before killing the process
:type pidfile: str
:type waittime: int
"""
pid = read_pidfile(pidfile)
if pid == 0 and pid0_warning:
logger.error("SmartHomeNG cannot run with a process ID of 0, probably no instance of SmartHomeNG running otherwise kill SmartHomeNG manually")
return
if psutil.pid_exists(pid):
logger.warning("Stopping SmartHomeNG, please wait...")
p = psutil.Process(pid)
if p is not None:
p.terminate()
try:
p.wait(timeout=waittime)
except Exception as e:
pass
if p.is_running():
logger.warning("Trying to terminate SmartHomeNG timed out, killing process")
p.kill()
try:
p.wait(timeout=5)
except Exception as e:
pass
elif pid != 0:
logger.warning("No instance of SmartHomeNG running")
return