fix: prevent root-owned data files from breaking radiosonde start

Running via sudo creates data/radiosonde/ as root. On next run the
config write fails with an unhandled OSError, Flask returns an HTML 500,
and the frontend shows a cryptic JSON parse error.

Three-layer fix:
- start.sh: pre-create known data dirs before chown, add certs/ to the
  list, export INTERCEPT_SUDO_UID/GID for runtime use
- generate_station_cfg: catch OSError with actionable message, chown
  newly created files to the real user via _fix_data_ownership()
- start_radiosonde: wrap config generation in try/except so it returns
  JSON instead of letting Flask emit an HTML error page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-02 16:27:20 +00:00
parent 90b455aa6c
commit bcf447fe4e
2 changed files with 59 additions and 14 deletions

View File

@@ -243,13 +243,40 @@ radius_temporary_block = False
sonde_time_threshold = 3
"""
with open(cfg_path, 'w') as f:
f.write(cfg)
try:
with open(cfg_path, 'w') as f:
f.write(cfg)
except OSError as e:
logger.error(f"Cannot write station.cfg to {cfg_path}: {e}")
raise RuntimeError(
f"Cannot write radiosonde config to {cfg_path}: {e}. "
f"Fix permissions with: sudo chown -R $(whoami) {cfg_dir}"
) from e
# When running as root via sudo, fix ownership so next non-root run
# can still read/write these files.
_fix_data_ownership(cfg_dir)
logger.info(f"Generated station.cfg at {cfg_path}")
return cfg_path
def _fix_data_ownership(path: str) -> None:
"""Recursively chown a path to the real (non-root) user when running via sudo."""
uid = os.environ.get('INTERCEPT_SUDO_UID')
gid = os.environ.get('INTERCEPT_SUDO_GID')
if uid is None or gid is None:
return
try:
uid_int, gid_int = int(uid), int(gid)
for dirpath, dirnames, filenames in os.walk(path):
os.chown(dirpath, uid_int, gid_int)
for fname in filenames:
os.chown(os.path.join(dirpath, fname), uid_int, gid_int)
except OSError as e:
logger.warning(f"Could not fix ownership of {path}: {e}")
def parse_radiosonde_udp(udp_port: int) -> None:
"""Thread function: listen for radiosonde_auto_rx UDP JSON telemetry."""
global radiosonde_running, _udp_socket
@@ -532,17 +559,22 @@ def start_radiosonde():
}), 409
# Generate config
cfg_path = generate_station_cfg(
freq_min=freq_min,
freq_max=freq_max,
gain=gain,
device_index=device_int,
ppm=ppm,
bias_t=bias_t,
latitude=latitude,
longitude=longitude,
gpsd_enabled=gpsd_enabled,
)
try:
cfg_path = generate_station_cfg(
freq_min=freq_min,
freq_max=freq_max,
gain=gain,
device_index=device_int,
ppm=ppm,
bias_t=bias_t,
latitude=latitude,
longitude=longitude,
gpsd_enabled=gpsd_enabled,
)
except (OSError, RuntimeError) as e:
app_module.release_sdr_device(device_int, sdr_type_str)
logger.error(f"Failed to generate radiosonde config: {e}")
return jsonify({'status': 'error', 'message': str(e)}), 500
# Build command - auto_rx -c expects a config DIRECTORY containing station.cfg
cfg_dir = os.path.dirname(os.path.abspath(cfg_path))

View File

@@ -79,12 +79,25 @@ export INTERCEPT_HOST="$HOST"
export INTERCEPT_PORT="$PORT"
# ── Fix ownership of user data dirs when run via sudo ────────────────────────
# When invoked via sudo the server process runs as root, so every file it
# creates (configs, logs, database) ends up owned by root. On the *next*
# startup we fix that retroactively, and we also pre-create known runtime
# directories so they get correct ownership from the start.
if [[ "$(id -u)" -eq 0 && -n "${SUDO_USER:-}" ]]; then
for dir in instance data; do
# Pre-create directories that routes may need at runtime
mkdir -p "$SCRIPT_DIR/instance" \
"$SCRIPT_DIR/data/radiosonde/logs" \
"$SCRIPT_DIR/data/weather_sat"
for dir in instance data certs; do
if [[ -d "$SCRIPT_DIR/$dir" ]]; then
chown -R "$SUDO_USER" "$SCRIPT_DIR/$dir"
fi
done
# Export real user identity so Python can chown runtime-created files
export INTERCEPT_SUDO_UID="$(id -u "$SUDO_USER")"
export INTERCEPT_SUDO_GID="$(id -g "$SUDO_USER")"
fi
# ── Dependency check (delegate to intercept.py) ─────────────────────────────