Ein Python-Script mit Systemd als Daemon (Systemd tut garnicht weh… 🙂 )

Einen Python-Server dauerhaft laufen zu lassen (z.B. auf einem Debian/Ubuntu Server oder einem Raspberry Pi) kann man mit Screen realisieren, oder -fast genauso einfach- mit Systemd…

Zuallererst brauchen wir ein Stück Python(3) Programmcode. In die Shebang-Zeile schreiben wir das ‚-u‘-Flag für Python3, damit die Scriptausgabe nicht gepuffert wird (wir wollen jede Ausgabe sofort im Log sehen):

#!/usr/bin/python3 -u
import socket

TCP_IP = '127.0.0.1'
TCP_PORT = 5005
BUFFER_SIZE = 20  # Normally 1024, but we want fast response

while 1:
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   s.bind((TCP_IP, TCP_PORT))
   s.listen(1)
   
   conn, addr = s.accept()
   print('Connection address:', addr)
   while 1:
      data = conn.recv(BUFFER_SIZE)
      if not data: break
      print("received data:", data)
      conn.send(data)  # echo
   conn.close()

Wir speichern das Script unter ‚/etc/pyserver/pyserver.py‘ (Ordner vorher anlegen) und machen es mit ‚chmod +x /etc/pyserver/pyserver.py‘ ausführbar.

Das Script gibt alles aus was es empfangen hat, und schickt es unverändert zurück. Wird die Connection beendet so wartet es auf eine neue. Da das script nicht forkt/multithreaded ist kann dieser Server natürlich immer nur eine gleichzeitige Connection bedienen!

Nun benötigen wir einen User unter dem der Server läuft, dieser bekommt auch den Ordner:

useradd -r -s /bin/false pyserveruser
chown -R pyserveruser:pyserveruser /etc/pyserver

Jetzt brauchen wir die sog. Unit-Datei. Sie erklärt systemd was wir für ein Dienst sind:

[Unit]
Description=My Python Server
After=syslog.target

[Service]
Type=simple
User=pyserveruser
Group=pyserveruser
WorkingDirectory=/etc/pyserver
ExecStart=/etc/pyserver/pyserver.py
SyslogIdentifier=pyserver
StandardOutput=syslog
StandardError=syslog
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target

Diese Datei bitte als ‚/etc/systemd/system/pyserver.service‘ speichern.
(Falls man die Datei ändert muss man dies systemd mittels ’systemctl daemon-reload‘ mitteilen)

Und schon können wir unseren Server starten:

systemctl enable pyserver
systemctl start pyserver

Mittels ’systemctl status pyserver‘ sollten wir nun sehen dass er läuft:

Active: active (running)

Jetzt testen wir ihn mal, indem wir uns mittels netcat zu unserem localhost auf den Port 5005 verbinden:

nc 127.0.0.1 5005

Wenn er alles zurückschickt was man ihm schreibt funktioniert alles.
Mittels Strg+C beendet man Netcat.

Übrigens überlebt unser Dienst dank den Restart-Zeilen in seinem Unitfile auch einen ‚kill -9‘ auf seinen Prozess. Systemd startet ihn einfach 3 Sekunden später neu.

Steuern kann man den Server nun mit den normalen systemd Kommandos:

systemctl status pyserver
systemctl start pyserver
systemctl stop pyserver
systemctl restart pyserver

Seine Logausgaben sieht man so:

journalctl -u pyserver

Die Logausgaben landen übrigens in der Datei ‚/var/log/syslog‘.
Da das noch nicht ganz so optimal ist korrigieren wir das noch!

In der Datei ‚/etc/rsyslog.d/50-default.conf‘ teilen wir unserem Syslog-Deamon mit dass das Programm ‚pyserver‘ bitte nach ‚/var/log/pyserver.log‘ geschrieben werden soll:

:programname,isequal,"pyserver"         /var/log/pyserver.log
& ~

Neustart des Rsyslog-Daemons mittels

systemctl restart rsyslog

nicht vergessen.

So, und nun sorgen wir als letztes noch dafür dass Logrotate unsere Logs täglich zippt und maximal 5 Logs aufbewahrt, Datei ‚/etc/logrotate.d/pysever‘ erstellen, folgender Inhalt:

/var/log/pyserver.log { 
    su root syslog
    daily
    rotate 5
    compress
    delaycompress
    missingok
    postrotate
        systemctl restart rsyslog > /dev/null
    endscript    
}

Und nun testen wir das wegrotieren erstmal trocken:

logrotate -d /etc/logrotate.d/pysever

Und wenn es keine Fehler gab können wir ihn zwingen zu rotieren, obwohl der Tag noch nicht abgelaufen ist:

logrotate --force /etc/logrotate.d/pysever

Und so einfach haben wir unseren eigenen kleinen Daemon erschaffen! (an dieser Stelle bitte Bösewicht-Gelächter vorstellen!)

Die Anleitung sollte auch auf Debian gehen, da muss nur die Gruppe in der Logrotate-Datei von ’syslog‘ auf ‚adm‘ geändert werden.
Wie immer, bitte in den Kommentaren melden wenn ich irgendeinen schrecklichen Patzer gemacht hab und meine Anleitung den Server nach ein paar Stunden abbrennen lässt – danke 🙂


Beitrag veröffentlicht

in

, , ,

von

Schlagwörter:

Kommentare

17 Antworten zu „Ein Python-Script mit Systemd als Daemon (Systemd tut garnicht weh… 🙂 )“

  1. […] ein Programm beim starten des Servers ausführen will. Der richtige Weg wäre hier natürlich ein Systemd-Script anzulegen – aber meistens ist man ja eher […]

  2. Hannes

    Vielen dank, super Artikel!!

  3. Bjoern

    Danke für den Artikel! Guter Überblick, schnell umzusetzen und anzupassen.

  4. Hallo Thomas
    ich habe auch ein Anliegen, denn ich möchte ein Server, welcher die SPI Schnittstelle vom Raspi ersetzt betreiben.
    Grund: Raspi ist mit einem Controller verbunden und soll den Datentaustausch zwischen raspi (python) und Controller (C) realisieren. Das Daten senden vom Raspi an den Controller funktioniert prima. Sendet der Controller Daten an den Raspi bleibt der ganze Laden (raspi und Controller) stehen.
    Da habe ich zur Not versucht die SPI Schnittstelle von Python als Master zu betreiben hatte auch nicht zum Erfolg geführt. Nun will ich die GPIO-PINS vom Raspi direkt anschalten und den Datenaustausch (abrufen der Daten vom Controller) auf diesem Wege zu realisieren.
    Dieses ist jetzt eine normale Python Datei. Diese Datei wollte ich dann als Service im Hintergrund laufen lassen, welche die Daten bei Bedarf vom Controller abruft.
    So nun ist es denn möglich eine Normale Datei mit while loop als Server laufen zu lassen?
    Ja mit den UNITS und systemd habe ich schon herumgearbeitet, aber ich habe keine Ahnung ob es mit einer normalen Datei mit wihle Loop auch funktionieren kann.
    Währe nett, wenn Du mir ein kleinen Hinweis geben könntest, wie ich diese Datei als Service laufen lassen kann

  5. Thomas

    Hallo Wilhelm,

    klar, wenn du ein Python Script mittels ‚crontab -e‘ in die Crontab-Datei einträgst und als Zeitpunkt einfach ‚@reboot‘ schreibst dann startet das direkt nach dem reboot im Hintergrund und kann auch eine Endlosschleife beinhalten.

    Problem ist dabei dann halt dass diese Endlosschleife tatsächlich einen Prozessorcore fast auf 100% auslastet. Damit verbraucht dein Pi mehr Strom und wird wärmer.

    Wenns dir drum geht ein Pin Togglen abzufangen so wäre ein GPIO Interrupt (die RasPi GPIO Library kann das) sinnvoller.
    Alternativ könnte man mal debuggen warum „der ganze Laden stehen bleibt“ wenn du normales SPI verwendest. Stehenbleiben sollte da eigentlich gar nichts.

    Wenn du mir die Programmcodes gibst kann ich das evtl. nachstellen – vorausgesetzt ich habe den richtigen Controller zur Hand. Nen Pi kann ich bieten 🙂

    Thomas

  6. Thomas

    Schönes Tutorial.

    Ich hab das mal soweit versucht zu benutzen um einen instabot https://github.com/instabot-py/instabot.py auf einem Pi Zero W laufen zu lassen.

    Laufen tut er auch. Ich habe zwar ein paar Probleme damit dass die Kommandozeile bei systemctl stop instabot das ganze nicht sauber beendet, aber nun ja. Er soll ja auch laufen.

    Viel mehr bereitet mir das Log Probleme. Das Skript ist zum einen nur mit python 2 lauffähig und ich glaube das da die -u Option nicht funktioniert (oder es liegt an der Art der Programmierung).

    Wenn ich es direkt über die Konsole starte, kommt eine recht schöne logausgabe was gerade gemacht wurde. Richte ich den rsyslogd mit dem 50-default.conf file ein, wird aber gar nichts in die Logdatei geloggt.

    Auch wenn ich rsyslogd da rauslasse und alles in die syslog loggen lasse sehe ich weder mit journalctl -u instabot noch mit tail -f syslog irgendwelche logausgaben.

    Wie kann ich denn da vorgehen, dass das Logging noch funktioniert? Oder nutzt der so ein dämliches Logging?

  7. Thomas

    Hmmm, der nutzt wohl die Python logging Facility https://docs.python.org/2.7/library/logging.html
    Kann man vielleicht einfach bei dem Bot das Log Output verändern?
    Der scheint ja in der instabot.py das logging in eine Datei zu schreiben:
    logging.basicConfig(filename=’errors.log‘, level=logging.INFO)

    1. Thomas

      Hi.

      Ja, genau so hab ich es letztendlich auch gemacht, nachdem ich verstanden hatte, wie da das logging funktioniert.

      Danke für deinen Beitrag und für die Antwort!

      Grüße

  8. Witt

    Hallo, danke für die gute Einführung!

    Ich möchte für mein Python-Script die Möglichkeit haben Konfigurationsdaten mitzugeben. Ich vermute mal, dass man diese geschickt in der ‚pyserver.service‘-Datei integrieren kann. Frage ist wie das korrekt gemacht wird, und wie man am besten vom Script aus diese Konfigurationsdaten ausliest.

    Danke und Gruß

  9. Peter

    Auch ein fettes Danke!!! von mir. Das hilft mir sehr weiter meine Sachen zu automatisieren und nach einem Neustart nichts mehr zu vergessen.

  10. So ein wertvoller Artikel!
    Bei Debian Buster scheint es die Gruppe ’syslog‘ nicht mehr ab Werk zu geben. Das war aber eine sehr überschaubare Änderung, damit bleibt dieser Artikel weiterhin gültig und sehr, sehr hilfreich.

  11. […] zugeben, dass das „Daemonisieren“ einer Software mit systemd erfreulich einfach geht: mit Hilfe dieser Anleitung war das eine Sache von wenigen […]

  12. Bene

    Hi, top! Vielen Dank! Das funktioniert 1a und so einfach… 🙂 VG

  13. Du hast logrotate –force /etc/logrotate.d/pysever geschriebn und nd logrotate –force /etc/logrotate.d/pyserver

  14. Raoul

    Vielen Dank. Sauber geschrieben, hat mich 10 min gekostet, das auf meinen Anwendungsfall unter Ubuntu 20.04.3 LTS abzuändern. Grandios.

  15. Beach

    Unter Raspberry OS Bookworm (Debian12) läuft die komplette Log über Journalctl
    Daher kann man sich alles bezüglich dem Logging sparen.
    Bzw. sollte man die Einstellungen in der journalctl.conf entsprechend anpassen.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert