Stopping a Blinkt! script started by systemd the "safe" way (= with all LEDs switched off)

Ahoy!

I’m now able to run the sample scripts delivered with Blinkt! from the command line. Hitting [Ctrl]+[C] terminates the script (I mostly used larson_hue.py) the “safe” way, which includes switching off all LEDs. When trying to autostart the script upon boot by using systemd, however, the LEDs keep on showing the last pattern after stopping the script and even after a halt of the Pi.

Here is my service file:

[Unit]
Description=Background process for the Pimoroni Blinkt! LED stripe

[Service]
Type=simple
User=mixtile
ExecStart=/home/mixtile/Pimoroni/blinkt/larson_hue
ExecStop=/bin/kill -SIGINT $MAINPID
ExecReload=/bin/kill -SIGINT $MAINPID
IgnoreSIGPIPE=false
KillMode=control-group
Restart=on-failure
RemainAfterExit=no 
SendSIGHUP=yes

[Install]
WantedBy=multi-user.target
Alias=blinkt.service

…whereas mixtile is the standard user on my Pi. And this is the shell script, which in turn starts the Python script supplied by you. Please note that contatenating the two lines of shell script with && and using them as ExecStart parameter in the service file does not work:

#!/bin/sh

. /home/mixtile/.virtualenvs/pimoroni/bin/activate
python3 /home/mixtile/Pimoroni/blinkt/examples/larson_hue.py

Without the ExecStop and ExecReload statements, stopping the service leaves me with a still runninng Python script. Querying the service status after stopping gives me this:

May 11 17:36:01 pizero systemd[1]: Started blinkt.service - Background process for the Pimoroni Blinkt! LED stripe.
May 11 17:36:06 pizero systemd[1]: Stopping blinkt.service - Background process for the Pimoroni Blinkt! LED stripe.
May 11 17:36:06 pizero systemd[1]: blinkt.service: Deactivated successfully.
May 11 17:36:06 pizero systemd[1]: Stopped blinkt.service - Background process for the Pimoroni Blinkt! LED stripe.
May 11 17:36:06 pizero systemd[1]: blinkt.service: Consumed 3.688s CPU time.

Replacing SIGINT with SIGTERM does not help.

So: Is there a possibility to stop the script “safely”, so that all LEDs are shut down upon service stop?

Look at the script and what it does when you hit CTRL-c. Then write a script that does exactly that after killing the main-script. Use that script as ExecStop.

Another option: write a simple ExecStop-script that creates a /tmp/foo-whatever file. From the main script, check this script from within the loop and exit gracefully when the file turns up.

When running the script from the shell and stopping it by using [Ctrl]+[C] after a couple of seconds, I get this:

mixtile@pizero:/lib/systemd/system $ /home/mixtile/Pimoroni/blinkt/larson_hue
^CTraceback (most recent call last):
  File "/home/mixtile/Pimoroni/blinkt/examples/larson_hue.py", line 49, in <module>
    blinkt.show()
  File "/home/mixtile/.virtualenvs/pimoroni/lib/python3.11/site-packages/blinkt/__init__.py", line 107, in show
    _write_byte(b)
  File "/home/mixtile/.virtualenvs/pimoroni/lib/python3.11/site-packages/blinkt/__init__.py", line 57, in _write_byte
    time.sleep(sleep_time)
KeyboardInterrupt

The only thing, which is apparently different from terminating the process by sending a TERM or INT signal, is the nature of the interrupt: [Ctrl]+[C] raises a KeyboardInterrupt. This puzzles me 'cause a keyboard interrupt in fact sends an INT signal to the running process:

Pressing Ctrl-C while a Python script is running sends a SIGINT (signal interrupt) to the Python interpreter, telling it to immediately stop whatever it’s doing . This SIGINT signal effectively pauses and interrupts the execution of the program. It raises a KeyboardInterrupt exception within the Python script.

Theoretically, upon arrival of an INT signal, the _exit() method in blinkt.py should be called, but for whatever reason, this is not the case when the sgnal comes from systemd.

Got it: It’s necessary to handle the corresponding signals by using a handler, as shown in this tutorial.

The start of the Python script now reads as follows:

#!/usr/bin/env python

import colorsys
import math
import time
import signal
import sys

import blinkt

FALLOFF = 1.9
SCAN_SPEED = 4

def interrupt_handler(signum, frame):
    print(f'Handling signal {signum} ({signal.Signals(signum).name}).')

    blinkt._exit()
    sys.exit(0)


blinkt.set_clear_on_exit()
signal.signal(signal.SIGINT,  interrupt_handler)
signal.signal(signal.SIGTERM, interrupt_handler)

Now stopping the script from systemd works neatly. :)

1 Like