Wahrscheinlich passt das Thema unter jedem debian System, ich hab es aber auf dem Raspberry unter Jessie getestet.
Mein Ziel war es, selbst einen Dienst einzurichten und dabei die Unterschiede zwischen wheezy (
SysVinit) und Jessie (
SystemD) zu lernen.
Eine gute Beschreibung für meinen Start habe ich
hier gefunden. Python ist wie Perl als Grundlage im Debian System schon vorhanden, man braucht also keine weitere Installation. Das Python Script aus dem genannten Artikel ist eine gute Grundlage und ich habe es etwas modifiziert.
Alle Scripte müssen im Unix Format (nur lf als Zeilenwechsel) gespeichert werden!
Das Demo-Script
Es schreibt kontinuierlich in eine "Tages"-Logdatei im /tmp Verzeichnis und kann bei normalen Start entweder mit ctrl-c oder im Hintergrund einfach mit kill beendet werden. Das Script demonstriert ein paar nützliche Techniken, ich habe lediglich den Abbruch am Ende entfernt und das Intervall auf eine Minute heraufgesetzt. So läuft das Script "ewig". Im Script selbst ist das Wichtigste dokumentiert.
#!/usr/bin/env python
import logging
import logging.handlers
import argparse
import sys
import time # this is only being used as part of the example
# Defaults
LOG_FILENAME = "/tmp/myservice.log"
LOG_LEVEL = logging.INFO # Could be e.g. "DEBUG" or "WARNING"
# Define and parse command line arguments
parser = argparse.ArgumentParser(description="My simple Python service")
parser.add_argument("-l", "--log", help="file to write log to (default '" + LOG_FILENAME + "')")
# If the log file is specified on the command line then override the default
args = parser.parse_args()
if args.log:
LOG_FILENAME = args.log
# Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data
# Give the logger a unique name (good practice)
logger = logging.getLogger(__name__)
# Set the log level to LOG_LEVEL
logger.setLevel(LOG_LEVEL)
# Make a handler that writes to a file, making a new file at midnight and keeping 3 backups
handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=3)
# Format each log message like this
formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s')
# Attach the formatter to the handler
handler.setFormatter(formatter)
# Attach the handler to the logger
logger.addHandler(handler)
# Make a class we can use to capture stdout and sterr in the log
class MyLogger(object):
def __init__(self, logger, level):
"""Needs a logger and a logger level."""
self.logger = logger
self.level = level
def write(self, message):
# Only log if there is a message (not just a new line)
if message.rstrip() != "":
self.logger.log(self.level, message.rstrip())
# Replace stdout with logging to file at INFO level
sys.stdout = MyLogger(logger, logging.INFO)
# Replace stderr with logging to file at ERROR level
sys.stderr = MyLogger(logger, logging.ERROR)
i = 0
# Loop forever, doing something useful hopefully:
while True:
logger.info("The counter is now " + str(i))
print "This is a print"
i += 1
time.sleep(60)
SysVinit konfigurieren
Init-Script
Ich möchte den Dienst unter einem normalen User laufen lassen und nicht als root (default). Im Pfad /etc/init.d/ befinden sich die Scripte welche zur Dienste Steuerung verwendet werden. Dort gibt es auch ein Script Namens skeleton, welches als Template verwendet werden kann. Also zunächst mal das Steuerungs Script, wiederum aus dem eingangs erwähnten Artikel, leicht modifiziert. Wie zu sehen ist, soll der Dienst unter dem User pi ($DAEMON_USER) und im Homedirectory ($DIR) vom User laufen.
#!/bin/sh
### BEGIN INIT INFO
# Provides: myservice
# Required-Start: $remote_fs $syslog
# Required-Stop: $remote_fs $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Put a short description of the service here
# Description: Put a long description of the service here
### END INIT INFO
# Change the next 3 lines to suit where you install your script and what you want to call it
DIR=/home/pi
DAEMON=$DIR/myservice.py
DAEMON_NAME=myservice
# Add any command line options for your daemon here
DAEMON_OPTS=""
# This next line determines what user the script runs as.
# Root generally not recommended but necessary if you are using the Raspberry Pi GPIO from Python.
DAEMON_USER=pi
# The process ID of the script when it runs is stored here:
PIDFILE=/var/run/$DAEMON_NAME.pid
. /lib/lsb/init-functions
do_start () {
log_daemon_msg "Starting system $DAEMON_NAME daemon"
start-stop-daemon --start --background --pidfile $PIDFILE --make-pidfile --user $DAEMON_USER --chuid $DAEMON_USER --startas $DAEMON -- $DAEMON_OPTS
log_end_msg $?
}
do_stop () {
log_daemon_msg "Stopping system $DAEMON_NAME daemon"
start-stop-daemon --stop --pidfile $PIDFILE --retry 10
log_end_msg $?
}
case "$1" in
start|stop)
do_${1}
;;
restart|reload|force-reload)
do_stop
do_start
;;
status)
status_of_proc "$DAEMON_NAME" "$DAEMON" && exit 0 || exit $?
;;
*)
echo "Usage: /etc/init.d/$DAEMON_NAME {start|stop|restart|status}"
exit 1
;;
esac
exit 0
Scripte kopieren und einrichten
Mit winscp werden die Scripte ins Home Verzeichnis von pi kopiert. Dann mit putty ein ssh Terminal öffnen und das Init-Script an Ort und Stelle kopieren, Rechte setzen und den Dienst einrichten.
sudo cp myservice.sh /etc/init.d/
chmod +x myservice.py
chmod +x /etc/init.d/myservice.sh
sudo update-rc.d myservice.sh defaults
Jetzt kann man mit den üblichen Befehlen oder durch direkten Script Aufruf den Dienst starten, abfragen und beenden.
sudo /etc/init.d/myservice.sh start
service myservice status
sudo service myservice stop
Will man den Dienst wieder entfernen, hilft uns update-rc.d mit den Optionen disable und remove.
SystemD
SystemD wird unter jessie mit installiert und läuft kompatibel und transparent. Man kann also sofort den Dienst auch mit systemctl <start|status|stop> myservice kontrollieren. Ich will aber nun die "alte" Einrichtung deaktivieren und den Dienst unter SystemD einrichten.
Ein Dienst in SystemD wird mit sogenannten Unit Files eingerichtet.
1. Variante: einfach das init.d Script verwenden
Im FHEM Forum gibt es ein
HowTo wie man den existierenden Service in einen SystemD Service umwandelt. Die folgende Datei wird als myservice.service gespeichert und mit winscp wieder nach /home/pi übertragen.
Wie man sieht, wird dabei gleich das alte SysVinit Script recycelt.
[Unit]
Description=Test myservice
[Service]
Type=forking
ExecStart=/etc/init.d/myservice.sh start
ExecStop=/etc/init.d/myservice.sh stop
[Install]
WantedBy=multi-user.target
Zunächst also den Dienst wieder entfernen, die Datei nach /etc/systemd/system kopieren und systemd über die Änderung informieren.
sudo update-rc.d myservice.sh disable
sudo cp myservice.service /etc/systemd/system/
sudo systemctl --system daemon-reload
Jetzt kann man zur Probe den Dienst mit systemctl <start|status|stop> myservice starten. Wenn alles funktioniert muss er noch aktiviert werden.
sudo systemctl enable myservice
2. Variante: extra Startscript verwenden
Hierauf basierend habe ich mir ein universelles Startscript erstellt. Zumindest mit meinem Pythonscript funktioniert das einwandfrei. Im Endeffekt ist es aber in der Funktion dem SysVinit adäquat. Die Funktion ist anders realisiert, ich finde es besser lesbar und der SysVinit Abschnitt am Anfang fehlt. Letztendlich könnte man sogar den Scriptaufruf als Parameter übergeben.
#!/bin/bash
PID=""
script="python myservice.py"
function get_pid {
PID=`pidof $script`
}
function stop {
get_pid
if [ -z $PID ]; then
echo "server is not running."
exit 1
else
echo -n "Stopping server.."
kill -9 $PID
sleep 1
echo ".. Done."
fi
}
function start {
get_pid
if [ -z $PID ]; then
echo "Starting server.."
$script &
get_pid
echo "Done. PID=$PID"
else
echo "server is already running, PID=$PID"
fi
}
function restart {
echo "Restarting server.."
get_pid
if [ -z $PID ]; then
start
else
stop
sleep 5
start
fi
}
function status {
get_pid
if [ -z $PID ]; then
echo "Server is not running."
exit 1
else
echo "Server is running, PID=$PID"
fi
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
restart
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
esac
Praktisch gesehen ist der Unit Teil bei Systemd vom Startscript getrennt. Basierend auf dieser
Dokumentation habe ich mal eine ziemlich "aufwändige" Variante erstellt. Hierbei wird start|stop|restart durch das obige Script gesteuert.
[Unit]
Description=Test myservice
After=network.target
[Service]
Type=forking
User=pi
WorkingDirectory=/home/pi
ExecStart=/home/pi/server.sh start
ExecStop=/home/pi/server.sh stop
ExecReload=/home/pi/server.sh restart
KillMode=none
[Install]
WantedBy=multi-user.target
3. Variante: Kurz und knapp einfach ein Unitfile erstellt
Hier übernimmt systemd die komplette Steuerung. Überraschenderweise funktioniert dies auch mit meinem Endlosscript. Ich habe die Anregung dazu von
hier. Dort ist noch ein besseres Beispiel mit einem kleinem Webserver als Testscript dargestellt. Lässt man den User= Eintrag weg startet der Dienst als root.
[Unit]
Description=Test systemd myservice
[Service]
User=pi
ExecStart=/home/pi/myservice.py
StandardOutput=null
[Install]
WantedBy=multi-user.target