Skip to content

Do not re-open the SPI handle on every module_init() - #429

Open
qnology wants to merge 1 commit into
waveshareteam:masterfrom
qnology:fix/spi-handle-leak-on-repeated-module-init
Open

Do not re-open the SPI handle on every module_init()#429
qnology wants to merge 1 commit into
waveshareteam:masterfrom
qnology:fix/spi-handle-leak-on-repeated-module-init

Conversation

@qnology

@qnology qnology commented Sep 10, 2026

Copy link
Copy Markdown

Fixes #428.

RaspberryPi.module_init() calls self.SPI.open(0, 0) unconditionally. spidev's open() does not close an existing handle, so every call after the first leaks one /dev/spidev0.0 file descriptor.

This is reachable from the documented API — switching a panel between 4Gray and 1Gray requires calling init(mode) again, and examples/epd_3in7_test.py does exactly that (init(0), init(1), init(0), with a single module_exit()), leaking two descriptors per run.

Measured on a Pi 3B refreshing an epd3in7 once a minute: 1024 descriptors held, 1012 of them spidev, after 16.7 hours. Past the NOFILE ceiling the process could no longer open a socket either, so unrelated HTTPS calls began failing with [Errno 22] Invalid argument — which is what makes this awkward to diagnose. Full write-up in #428.

The change

Uses the guard SunriseX3.module_init() already uses a few classes down in this same file, so the two implementations now behave consistently:

  • Flag = 0 class attribute on RaspberryPi, matching SunriseX3
  • module_init() opens SPI only when Flag == 0
  • module_exit() resets Flag so a genuine teardown still re-opens

Only the cleanup=False branch is guarded; the cleanup=True path uses DEV_SPI and is untouched.

Behaviour

before after
1000 × module_init() 1000 opens 1 open
module_exit() then module_init() re-opens re-opens

No API change, and no change for callers that already do a single init() / module_exit().

Verified on the affected hardware: a Pi 3B driving an epd3in7 at one refresh per minute now holds a steady 13 file descriptors, 1 of them spidev, across sustained operation — previously it gained one per minute until it died.

🤖 Generated with Claude Code

RaspberryPi.module_init() calls self.SPI.open(0, 0) unconditionally. spidev's
open() does not close an existing handle, so every call after the first leaks
one /dev/spidev0.0 file descriptor.

This is reachable from the documented API. Switching a panel between 4Gray and
1Gray requires calling init(mode) again, and the bundled example does exactly
that: epd_3in7_test.py calls init(0), init(1), then init(0), with a single
module_exit() at the end, leaking two descriptors per run.

A long-running program that redraws periodically hits the process NOFILE limit.
Measured on a Pi 3B refreshing an epd3in7 once a minute: 1024 descriptors held,
1012 of them spidev, after 16.7 hours. Past that the process cannot open a
socket either, so unrelated network calls start failing with EINVAL.

Fixed with the same Flag guard SunriseX3.module_init() already uses in this
file, and reset in module_exit() so a genuine teardown still re-opens.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

RaspberryPi.module_init() leaks an SPI file descriptor on every call

2 participants