mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-11 01:26:37 +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
@@ -26,6 +26,36 @@ import web_interface.blueprints.api_v3 as _pkg
|
||||
# package is the only patch point that covers every caller.
|
||||
|
||||
|
||||
def _ownership_hint(err: BaseException):
|
||||
"""An actionable message when the apps directory is not writable.
|
||||
|
||||
The display service runs as root and the web interface as the login user
|
||||
(see systemd/ledmatrix.service and systemd/ledmatrix-web.service). The
|
||||
starlark-apps directory is not in the repository, so whichever service
|
||||
reaches it first creates it -- and when that is the display service, the
|
||||
web user cannot write into it and every install fails.
|
||||
|
||||
The plugin now hands the directory back on startup, so this should not be
|
||||
reachable. It is kept because the failure is otherwise invisible: the
|
||||
generic message names no path and no cause, and the one user who hit it
|
||||
had to read the service logs to find it. If the handover is ever prevented
|
||||
-- an exotic mount, a directory root-owned for another reason -- this says
|
||||
what to do instead of costing somebody an evening.
|
||||
|
||||
Returns None when `err` is not a permission problem.
|
||||
"""
|
||||
if not isinstance(err, PermissionError):
|
||||
return None
|
||||
return (
|
||||
f"Cannot write to {_STARLARK_APPS_DIR}. It is owned by another user "
|
||||
f"-- usually because the display service, which runs as root, created "
|
||||
f"it before the web interface did. Restarting the display service "
|
||||
f"(sudo systemctl restart ledmatrix) repairs the ownership "
|
||||
f"automatically. To fix it by hand: "
|
||||
f"sudo chown -R $USER:$USER {_STARLARK_APPS_DIR}"
|
||||
)
|
||||
|
||||
|
||||
@api_v3.route('/starlark/status', methods=['GET'])
|
||||
def get_starlark_status():
|
||||
"""Get Starlark plugin status and Pixlet availability."""
|
||||
@@ -278,7 +308,8 @@ def upload_starlark_app():
|
||||
# without it, though, and describe_exception redacts credentials and
|
||||
# truncates -- the same trade-off every other handler here makes.
|
||||
logger.exception("[Starlark] File error uploading starlark app: %s", err)
|
||||
return jsonify({'status': 'error', 'message': 'File error during upload',
|
||||
return jsonify({'status': 'error',
|
||||
'message': _ownership_hint(err) or 'File error during upload',
|
||||
'details': describe_exception(err)}), 500
|
||||
except ImportError as err:
|
||||
logger.exception("[Starlark] Module load error uploading starlark app: %s", err)
|
||||
@@ -702,6 +733,10 @@ def install_from_tronbyte_repository():
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("[Starlark] install_from_tronbyte_repository failed")
|
||||
hint = _ownership_hint(e)
|
||||
if hint:
|
||||
return jsonify({'status': 'error', 'message': hint,
|
||||
'details': describe_exception(e)}), 500
|
||||
return jsonify({'status': 'error', 'message': 'Failed to install from repository', 'details': describe_exception(e)}), 500
|
||||
@api_v3.route('/starlark/repository/categories', methods=['GET'])
|
||||
def get_tronbyte_categories():
|
||||
|
||||
Reference in New Issue
Block a user