rpi-rgb-led-matrix
rpi-rgb-led-matrix copied to clipboard
Problem launching led-image-viewer via subprocess.Popen from root Python
I am trying to launch led-image-viewer from a Python script using subprocess.Popen, running the script as root, but the viewer subprocess exits with "Must run as root to be able to access /dev/mem".
I'm executing in the usual way: sudo /path/to/venv/python /path/to/script.py This is on a fresh rasbian lite install with python3 on a pi3b
Confirmation of Root: I have verified the Python script runs as root (EUID 0) using os.geteuid().
Confirmation of Subprocess Root: I have verified a simple subprocess (id -u) launched by the script also runs as root (EUID 0).
Confirmation of Viewer Functionality: Running the led-image-viewer directly from the terminal with sudo using a standard image file does work.
Temporary File Details: My script downloads the image, saves it to /tmp as a temporary file (which for some reason ends up being owned by daemon:daemon, though I've added os.chmod(0o644) to make it readable), and that sudo file /path/to/tempfile.jpg confirms it's a valid JPEG.
The Exact Error: Some output from my script:
Starting Sonos Album Art Display Script... Process nice value set to: -10 --- DEBUG: Script running with Effective User ID: 0 --- DEBUG: Testing privilege level of subprocess... --- DEBUG: Subprocess 'id -u' reported EUID: 0 Initializing LED matrix hardware object... Setting disable_hardware_pulsing = True (equivalent to --led-no-hardware-pulse) Setting pwm_lsb_nanoseconds = 130 LED matrix hardware object initialized successfully. Initial matrix clear complete. Attempting to discover Sonos speaker: 'Office'... Found Sonos speakers: ['Office', 'Living Room'] Successfully connected to Sonos speaker: 'Office' at IP: XXX.XXX.XXX.XXX Starting main loop to monitor Sonos playback...
Sonos playback state: PLAYING DEBUG: Full track_info dictionary: {'title': 'Feel Good Inc.', 'artist': 'Gorillaz, De La Soul', 'album': 'Demon Days', 'album_art': 'http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7', 'position': '0:00:06', 'playlist_position': '1', 'duration': '0:03:43', 'uri': 'x-sonosapi-hls-static:ALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2?sid=284&flags=8&sn=7', 'metadata': '<DIDL-Lite xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:r="urn:schemas-rinconnetworks-com:metadata-1-0/" xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/"><item id="-1" parentID="-1" restricted="true"><res protocolInfo="sonos.com-http::application/x-mpegURL:" duration="0:03:43">x-sonosapi-hls-static:ALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2?sid=284&flags=8&sn=7<r:streamContent></r:streamContent>upnp:albumArtURI/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7</upnp:albumArtURI>dc:titleFeel Good Inc.</dc:title>upnp:classobject.item.audioItem.musicTrack</upnp:class>dc:creatorGorillaz, De La Soul</dc:creator>upnp:albumDemon Days</upnp:album></DIDL-Lite>'} Currently playing: 'Feel Good Inc.' by 'Gorillaz, De La Soul' Album art URI obtained: 'http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7' Detected state change (new song/art) or viewer is not running. New song or new album art detected. Old URI: 'None', New URI: 'http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7' Processing album art URL string: 'http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7' Using safe URL after encoding path: http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7 --- DEBUG: Checking if executable exists at: '/home/XXXX/rpi-rgb-led-matrix/utils/led-image-viewer' Requesting image data from: http://XXX.XXX.XXX.XXX:1400/getaa?s=1&u=x-sonosapi-hls-static%3aALkSOiEQY4McqzFWuxa5vAq7TmIJB_pzAsWB-PTgyJ7m5Fe2%3fsid%3d284%26flags%3d8%26sn%3d7 --- DEBUG: Successfully downloaded image data. --- DEBUG: Downloaded image Content-Type: None --- DEBUG: Writing image data to temporary file: /tmp/tmpgmwrhvjk.jpg --- DEBUG: Set temporary file permissions to 0o644: /tmp/tmpgmwrhvjk.jpg --- DEBUG: Launching led-image-viewer with command: /home/XXXX/rpi-rgb-led-matrix/utils/led-image-viewer --led-rows=64 --led-cols=64 --led-chain=1 --led-parallel=1 --led-brightness=75 --led-pwm-bits=11 --led-gpio-mapping=adafruit-hat-pwm --led-scan-mode=1 --led-rgb-sequence=RGB --led-no-hardware-pulse /tmp/tmpgmwrhvjk.jpg --- DEBUG: led-image-viewer process started with PID: 1207 Album art viewer launched on LED matrix. --- DEBUG: Cleaning up temporary file: /tmp/tmpgmwrhvjk.jpg --- WARNING: led-image-viewer process (PID 1207) stopped unexpectedly (Exit Code: 1). Clearing. --- VIEWER STDERR: Must run as root to be able to access /dev/mem Prepend 'sudo' to the command
Clearing matrix display. Stopping existing image viewer process.
Python Code Snippets: Here are some code snippets. Import and configuration:
import subprocess
import tempfile
import os
import urllib.parse # For URL processing
import requests # For downloading
# Other imports like soco, rgbmatrix
# ... (Other Configuration like TARGET_SONOS_SPEAKER_NAME) ...
# LED Matrix Configuration (These become command line args for viewer)
MATRIX_ROWS = 64
MATRIX_COLS = 64
MATRIX_CHAIN = 1
MATRIX_PARALLEL = 1
MATRIX_BRIGHTNESS = 75
MATRIX_PWM_BITS = 11
MATRIX_GPIO_MAPPING = "adafruit-hat-pwm"
MATRIX_SCAN_MODE = 1
MATRIX_LED_RGB_SEQUENCE = "RGB"
# --- LED Viewer Configuration ---
LED_VIEWER_PATH = "/home/XXXX/rpi-rgb-led-matrix/utils/led-image-viewer" # <--- Your actual path
# --- End Configuration ---
# Global variables
# ... (sonos_device, track_uri, etc.) ...
viewer_process = None # Global variable to hold the viewer process handle
EUID checks:
# --- Check effective user ID ---
try:
euid = os.geteuid()
print(f"--- DEBUG: Script running with Effective User ID: {euid}")
if euid != 0:
print("--- WARNING: Script is NOT running as root (EUID is not 0).")
except Exception as e:
print(f"--- ERROR: Could not determine Effective User ID: {e}")
# --- End check ---
# --- Check subprocess effective user ID ---
print("--- DEBUG: Testing privilege level of subprocess...")
try:
result = subprocess.run(['id', '-u'], capture_output=True, text=True, check=True)
subprocess_euid = result.stdout.strip()
print(f"--- DEBUG: Subprocess 'id -u' reported EUID: {subprocess_euid}")
if subprocess_euid != '0':
print("--- WARNING: Subprocess is NOT reporting root privileges (EUID is not 0).")
except FileNotFoundError:
print("--- ERROR: 'id' command not found. Cannot test subprocess EUID.")
except subprocess.CalledProcessError as e:
print(f"--- ERROR: Subprocess 'id -u' failed with exit code {e.returncode}. Stderr:\n{e.stderr}")
except Exception as e:
print(f"--- ERROR: An unexpected error occurred during subprocess EUID test: {e}")
# --- End subprocess check ---
Clearing the matrix
def clear_matrix():
"""Clears the LED matrix by stopping the image viewer process and clearing the Python object."""
global viewer_process, matrix
print("Clearing matrix display.")
# Stop the external image viewer process
if viewer_process is not None:
print("Stopping existing image viewer process.")
try:
viewer_process.terminate() # Use terminate() first
# Give it a moment to terminate, then kill if necessary
viewer_process.wait(timeout=2)
except subprocess.TimeoutExpired:
print("Old viewer process did not terminate gracefully, killing.")
viewer_process.kill()
except Exception as e:
print(f"Error stopping viewer process: {e}")
viewer_process = None # Clear the process handle
# Also clear the matrix using the Python object (optional, depends on setup)
if matrix:
try:
matrix.Clear() # Or matrix.SetImage(black_image)
except Exception as e:
print(f"Error clearing matrix with Python object: {e}")
Displaying the album art
def display_album_art(image_url):
"""Downloads album art, saves to temp file with permissions, and launches led-image-viewer."""
global viewer_process
if not image_url:
print("No image URL provided. Clearing matrix.")
clear_matrix()
return
# Ensure the viewer executable exists (Optional but good practice)
if not os.path.exists(LED_VIEWER_PATH):
print(f"--- ERROR: LED viewer executable not found at '{LED_VIEWER_PATH}'. Cannot display image.")
# clear_matrix() # Decide error handling
return
# Construct the base command line arguments for the viewer
viewer_command = [
LED_VIEWER_PATH,
f"--led-rows={MATRIX_ROWS}",
f"--led-cols={MATRIX_COLS}",
f"--led-chain={MATRIX_CHAIN}",
f"--led-parallel={MATRIX_PARALLEL}",
f"--led-brightness={MATRIX_BRIGHTNESS}",
f"--led-pwm-bits={MATRIX_PWM_BITS}",
f"--led-gpio-mapping={MATRIX_GPIO_MAPPING}",
f"--led-scan-mode={MATRIX_SCAN_MODE}",
f"--led-rgb-sequence={MATRIX_LED_RGB_SEQUENCE}",
"--led-no-hardware-pulse", # Example flag, include your necessary flags
# Add other flags like --led-slowdown-gpio=1 etc.
]
temp_file_path = None # Initialize temporary file path
try:
# Assume image_url is already processed by get_sonos_album_art_url
safe_image_url = image_url
print(f"Requesting image data from: {safe_image_url}")
response = requests.get(safe_image_url, stream=True, timeout=10)
response.raise_for_status()
print("--- DEBUG: Successfully downloaded image data.")
print(f"--- DEBUG: Downloaded image Content-Type: {response.headers.get('Content-Type')}") # Shows 'None'
# Create a temporary file with a .jpg suffix
temp_file = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
temp_file_path = temp_file.name
print(f"--- DEBUG: Writing image data to temporary file: {temp_file_path}")
temp_file.write(response.content)
temp_file.close() # Close the file handle
# *** Crucial: Set permissions so viewer (even if somehow dropping privileges or owned by daemon) can read ***
os.chmod(temp_file_path, 0o644) # Set permissions to -rw-r--r--
print(f"--- DEBUG: Set temporary file permissions to 0o644: {temp_file_path}")
# Include the ls -l output you got showing it was daemon:daemon owned with -rw------- before chmod
# --- STOP any existing viewer process ---
if viewer_process is not None:
print("Stopping existing image viewer process before starting new one.")
try:
viewer_process.terminate()
viewer_process.wait(timeout=2)
except subprocess.TimeoutExpired:
print("Old viewer process did not terminate gracefully, killing.")
viewer_process.kill()
except Exception as e:
print(f"--- ERROR: Error stopping old viewer process: {e}")
viewer_process = None # Clear the handle
# Add the temporary file path to the command
final_command = viewer_command + [temp_file_path]
# --- LAUNCH the led-image-viewer process ---
print(f"--- DEBUG: Launching led-image-viewer with command: {' '.join(final_command)}")
# Use Popen to run it in the background, inherit environment
viewer_process = subprocess.Popen(final_command,
stdout=subprocess.PIPE, # Capture stdout
stderr=subprocess.PIPE, # Capture stderr
env=os.environ.copy()) # Explicitly pass environment
print(f"--- DEBUG: led-image-viewer process started with PID: {viewer_process.pid}")
print("Album art viewer launched on LED matrix.")
except requests.exceptions.RequestException as e:
print(f"--- ERROR: Error downloading image: {e}")
# Error handling...
pass
except FileNotFoundError:
print(f"--- ERROR: LED viewer executable not found at '{LED_VIEWER_PATH}'. Check the path.")
# Error handling...
pass
except Exception as e:
print(f"--- ERROR: An unexpected error occurred in display_album_art (download/launch): {e}")
# Error handling...
pass
finally:
# --- CLEANUP TEMPORARY FILE ---
# Delete the temporary file after attempting to launch the viewer
if temp_file_path and os.path.exists(temp_file_path):
try:
print(f"--- DEBUG: Cleaning up temporary file: {temp_file_path}")
os.remove(temp_file_path) # Re-enable os.remove
except Exception as e:
print(f"--- ERROR: Failed to clean up temporary file {temp_file_path}: {e}")
# --- END CLEANUP ---
I've this in the main loop to show error capture
# Inside the while True loop in main_loop:
# Optional: Check if the viewer process is still running periodically
if viewer_process is not None and viewer_process.poll() is not None:
print(f"--- WARNING: led-image-viewer process (PID {viewer_process.pid}) stopped unexpectedly (Exit Code: {viewer_process.returncode}). Clearing.")
# Read any stderr output for debugging crashed viewer
try:
stderr_output = viewer_process.stderr.read().decode('utf-8', errors='ignore')
if stderr_output:
print(f"--- VIEWER STDERR:\n{stderr_output}")
except Exception as e:
print(f"--- ERROR: Failed to read viewer stderr: {e}")
# Clear state to force display_album_art call on next playing state
clear_matrix() # This also sets viewer_process = None
# Reset state variables if needed
# current_track_album_art_uri = None
# last_known_track_title = None
# ... (rest of main_loop logic checking Sonos state and calling display_album_art) ...
This is puzzling because both the parent script and other subprocesses appear to have root, but this specific executable fails its root check in this context. Am I missing something blindingly obvious?