如何确保我的 Python 脚本正确处理 Ctrl-C?

问题描述 投票:0回答:1

系统:

  1. 使用 Python 3.7 运行 Buster 的 Raspberry Pi-4,并连接到 GPIO 引脚的 Adafruit 128x32 2.23" OLED Bonnet。参考:https://learn.adafruit.com/adafruit-2-23-monochrome-oled-bonnet

  2. 我正在使用的脚本的原始版本可以在以下位置找到:https://learn.adafruit.com/adafruit-2-23-monochrome-oled-bonnet/usage#verify-i2c-device-3064554

问题:
当通过终端窗口运行Python程序时,CTL-C并不总是产生我想要的有序关闭。  当在 Thonny 内运行 时,它似乎总是有效。


请注意以下 Python 脚本,该脚本是从 Adafruit 网站上逐字获取的,用于测试其 128x32 OLED Raspberry Pi“Bonnet”。

我修改了它,添加了一些代码来捕获和处理信号中断,以便在程序结束时清除并关闭显示。

即:

#!/usr/bin/python3.7

# SPDX-FileCopyrightText: <text> 2020 Tony DiCola, James DeVito,
# and 2020 Melissa LeBlanc-Williams, for Adafruit Industries </text>

# SPDX-License-Identifier: MIT


# This example is for use on (Linux) computers that are using CPython with
# Adafruit Blinka to support CircuitPython libraries. CircuitPython does
# not support PIL/pillow (python imaging library)!

# Modified 2024/10/12 by Jim Harris to allow for signal capture
# and a graceful shutdown

import os
import signal
import sys
from threading import Condition, Thread, Event
import time
import subprocess
from board import SCL, SDA, D4
import busio
import digitalio
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1305

# check if it's ran with Python3
assert sys.version_info[0:1] == (3,)

# for triggering the shutdown procedure when a signal is detected
keyboard_trigger = Event()
def signal_handler(signal, frame):
    print('\nSignal detected. Stopping threads.')
    keyboard_trigger.set()

# registering both types of signals
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Get PID of this running script
pid = os.getpid()

# Define the Reset Pin
oled_reset = digitalio.DigitalInOut(D4)

# Create the I2C interface.
i2c = busio.I2C(SCL, SDA)

# Create the SSD1305 OLED class.
# The first two parameters are the pixel width and pixel height.  Change these
# to the right size for your display!
disp = adafruit_ssd1305.SSD1305_I2C(128, 32, i2c, reset=oled_reset)

# Clear display.
disp.fill(0)
disp.show()

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
width = disp.width
height = disp.height
image = Image.new("1", (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image.
draw.rectangle((0, 0, width, height), outline=0, fill=0)

# Draw some shapes.
# First define some constants to allow easy resizing of shapes.
padding = -2
top = padding
bottom = height - padding
# Move left to right keeping track of the current x position for drawing shapes.
x = 0


# Load default font.
#font = ImageFont.load_default()

# Alternatively load a TTF font.  Make sure the .ttf font file is in the
# same directory as the python script!
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 9)
print("Started system statistics printout as PID", pid)

# Main drawing routine
#while True:
while not keyboard_trigger.is_set():
    # Draw a black filled box to clear the image.
    draw.rectangle((0, 0, width, height), outline=0, fill=0)

    # Shell scripts for system monitoring from here:
    # https://unix.stackexchange.com/questions/119126/command-to-display-memory-usage-disk-usage-and-cpu-load
    cmd = "hostname -I | cut -d' ' -f1"
    IP = subprocess.check_output(cmd, shell=True).decode("utf-8")
    cmd = "top -bn1 | grep load | awk '{printf \"CPU Load: %.2f\", $(NF-2)}'"
    CPU = subprocess.check_output(cmd, shell=True).decode("utf-8")
    cmd = "free -m | awk 'NR==2{printf \"Mem: %s/%s MB  %.2f%%\", $3,$2,$3*100/$2 }'"
    MemUsage = subprocess.check_output(cmd, shell=True).decode("utf-8")
    cmd = 'df -h | awk \'$NF=="/"{printf "Disk: %d/%d GB  %s", $3,$2,$5}\''
    Disk = subprocess.check_output(cmd, shell=True).decode("utf-8")

    # Write four lines of text.

    draw.text((x, top + 0), "IP: " + IP, font=font, fill=255)
    draw.text((x, top + 8), CPU, font=font, fill=255)
    draw.text((x, top + 16), MemUsage, font=font, fill=255)
    draw.text((x, top + 25), Disk, font=font, fill=255)

    # Display image.
    disp.image(image)
    disp.show()
    time.sleep(0.1)

# until some keyboard event is detected
print("\nA keyboard event was detected.")
print("Clearing and shutting down display")

# Clear display.
disp.fill(0)
disp.show()
sys.exit(0)

案例1:从Thonny内部逃跑
从 Thonny always 中运行会在 Thonny 的终端 (REPL) 窗口中产生以下输出。

Python 3.7.3 (/usr/bin/python3)
>>> %cd /home/pi/startup_scripts
>>> %Run Stats.py
Started system statistics printout as PID 24225

Signal detected. Stopping threads.

A keyboard event was detected.
Clearing and shutting down display

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
Python 3.7.3 (/usr/bin/python3)
>>> 

每次,脚本都会通过干净关闭并清除显示来响应 CTL-C,这是期望和预期的行为。

案例 2:在终端窗口中运行。
当在终端中运行时,我得到以下信息:

尝试 1:从另一个终端窗口发送显式终止信号:

pi@Adafruit128x32:~/startup_scripts $ ./Stats.py
Started system statistics printout as PID 15893

Signal detected. Stopping threads.

A keyboard event was detected.
Clearing and shutting down display
pi@Adafruit128x32:~/startup_scripts $

每次我通过另一个终端“杀死”进程时都会发生这种情况。

尝试 2:使用 CTL-C 在正在运行的终端窗口中杀死

尝试1:

pi@Adafruit128x32:~/startup_scripts $ ./Stats.py
Started system statistics printout as PID 16921
^C
Signal detected. Stopping threads.

A keyboard event was detected.
Clearing and shutting down display
pi@Adafruit128x32:~/startup_scripts $

尝试2:

pi@Adafruit128x32:~/startup_scripts $ ./Stats.py
Started system statistics printout as PID 17587
^C
Signal detected. Stopping threads.
Traceback (most recent call last):
  File "./Stats.py", line 104, in <module>
    Disk = subprocess.check_output(cmd, shell=True).decode("utf-8")
  File "/usr/lib/python3.7/subprocess.py", line 395, in check_output
    **kwargs).stdout
  File "/usr/lib/python3.7/subprocess.py", line 487, in run
    output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command 'df -h | awk '$NF=="/"{printf "Disk: %d/%d GB  %s", $3,$2,$5}'' died with <Signals.SIGINT: 2>.
pi@Adafruit128x32:~/startup_scripts $ 

这里抛出了一个未处理的异常,这是最常见的结果。 (它起作用的尝试实际上让我感到惊讶。)

我怀疑正在发生的事情是,还有其他一些正在运行的库代码捕获了 CTL-C 键盘信号,并且“库代码位”没有将异常“沿着梯子”传递到我所在的进程处理错误。

问题:

  1. 为什么会出现这种情况?

  2. 有没有办法确保我的代码捕获键盘信号,以便它可以干净地关闭,而不会出现通常发生的混乱的未处理异常?

python raspberry-pi
1个回答
0
投票

Stats.py
fork 新进程,将其添加到当前进程组中。

当您将

SIGINT
发送到
Stats.py
时,其他进程根本不会收到该信号。

当您使用 Control-C 时,终端会将

SIGINT
传递给当前进程组中的每个进程。您的代码没有准备好让它们被某些外部信号停止;它假设它们将运行完成,除非被
Stats.py
本身中断。您是否收到错误取决于您按下 Control-C 的确切时间,以及当时是否正在执行任何给定的子进程。 据我所知,您无法更改子进程的信号处理程序;您只需对它们

do

如何处理 SIGINT 做出反应,或者(我不确定具体如何去做)确保它们在与您的脚本执行所在的

separate
进程组中运行。(这并不意味着他们会对 SIGINT 做出不同的反应,但确实确保当您按 Control-C 时,终端不会向他们发送
SIGINT
。)
    

© www.soinside.com 2019 - 2024. All rights reserved.