mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-11 09:36:36 +00:00
fix(starlark): stop the root display service locking the web UI out
Reported after a fresh install: installing an app from the Starlark tab
failed with "install failed: Failed to install from repository", and so did
uploading a .star file and installing from a GitHub directory. The reporter
found the cause only by reading service logs, and fixed it with
sudo chown -R ledpi:ledpi /home/ledpi/LEDMatrix/starlark-apps
starlark-apps is gitignored, so it is never checked out -- it is created
lazily by whichever process reaches it first. Those processes run as
different users. systemd/ledmatrix.service is User=root and constructs this
plugin at startup, which is where _get_apps_directory() is called from;
systemd/ledmatrix-web.service runs as the login user and is what actually
installs apps.
The documented first step is to install pixlet and reboot, so on a fresh
machine the display service usually wins that race and mkdir() leaves the
directory root-owned. The web process then fails in _install_star_file() on
app_dir.mkdir(), which catches nothing, so PermissionError reaches the
route's outer `except Exception` and becomes the generic message the user
saw. All three install paths write to the same directory, which is why all
three failed.
The web user cannot repair this -- chown needs root. So root does it, on
every startup, which also heals machines already broken by this without the
owner having to find the chown themselves. It is a no-op when not root, when
the platform has no POSIX ownership, and when the checkout genuinely belongs
to root; a chown that fails warns rather than killing startup.
Also made the failure legible if the handover is ever prevented: a
PermissionError now names the directory, the automatic repair, and the
manual chown, instead of a message that names neither path nor cause.
Verified by mutation: dropping the handover call, chowning a genuinely
root-owned checkout, and letting a non-root process chown each fail their
own test. 121 starlark tests pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014RRtqXDCnvnY6EQwhT5CV9
This commit is contained in:
co-authored by
Claude Opus 5
parent
967f3a0567
commit
965d509864
@@ -457,12 +457,66 @@ class StarlarkAppsPlugin(BasePlugin):
|
||||
apps_dir = project_root / "starlark-apps"
|
||||
except Exception:
|
||||
# Fallback to current working directory
|
||||
apps_dir = Path.cwd() / "starlark-apps"
|
||||
project_root = Path.cwd()
|
||||
apps_dir = project_root / "starlark-apps"
|
||||
|
||||
# Create directory if it doesn't exist
|
||||
apps_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._hand_apps_dir_to_checkout_owner(apps_dir, project_root)
|
||||
return apps_dir
|
||||
|
||||
def _hand_apps_dir_to_checkout_owner(self, apps_dir: Path, project_root: Path) -> None:
|
||||
"""Give the apps directory to whoever owns the checkout.
|
||||
|
||||
This directory is not in the repository, so it is created lazily by
|
||||
whichever process reaches it first -- and the two that do run as
|
||||
different users. The display service is `User=root`
|
||||
(systemd/ledmatrix.service) and instantiates this plugin at startup,
|
||||
which is where `_get_apps_directory` is called from. The web interface
|
||||
is `User=<login user>` (systemd/ledmatrix-web.service) and is what
|
||||
actually installs apps.
|
||||
|
||||
On a fresh install the display service usually wins that race -- the
|
||||
documented first step is to install pixlet and reboot -- so the
|
||||
directory lands root-owned, and every subsequent install from the web
|
||||
UI fails on PermissionError. The user sees only "Failed to install
|
||||
from repository", with nothing pointing at ownership.
|
||||
|
||||
The web user cannot repair this; it lacks permission to chown. Root
|
||||
can, so root does it here, on every startup. That also heals installs
|
||||
already broken by this, without the user having to find the chown.
|
||||
"""
|
||||
geteuid = getattr(os, "geteuid", None)
|
||||
chown = getattr(os, "chown", None)
|
||||
if geteuid is None or chown is None or geteuid() != 0:
|
||||
# Not root, or not a platform with POSIX ownership. If the
|
||||
# directory is wrong we cannot fix it, and must not pretend to.
|
||||
return
|
||||
try:
|
||||
owner = project_root.stat()
|
||||
except OSError:
|
||||
return
|
||||
if owner.st_uid == 0:
|
||||
# The checkout genuinely belongs to root, so root owning the apps
|
||||
# directory is correct and there is nobody to hand it to.
|
||||
return
|
||||
|
||||
for path in (apps_dir, *apps_dir.rglob("*")):
|
||||
try:
|
||||
st = path.stat()
|
||||
except OSError:
|
||||
continue
|
||||
if st.st_uid == owner.st_uid and st.st_gid == owner.st_gid:
|
||||
continue
|
||||
try:
|
||||
chown(path, owner.st_uid, owner.st_gid)
|
||||
except OSError as e:
|
||||
self.logger.warning(
|
||||
"Could not hand %s to uid %s: %s -- installs from the web "
|
||||
"interface will fail until this is chowned manually",
|
||||
path, owner.st_uid, e,
|
||||
)
|
||||
|
||||
def _sanitize_app_id(self, app_id: str) -> str:
|
||||
"""
|
||||
Sanitize app_id into a safe slug for use in file paths.
|
||||
|
||||
Reference in New Issue
Block a user