mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Merge branch 'fix/pi-installer-245'
This commit is contained in:
commit
9b6f0856e0
|
|
@ -81,7 +81,13 @@
|
|||
|
||||
<h2>Step 2: Run the ScreenTinker installer</h2>
|
||||
<p>Open a terminal on the Pi and run:</p>
|
||||
<pre><code>curl -sL https://screentinker.com/scripts/raspberry-pi-setup.sh | bash</code></pre>
|
||||
<pre><code>curl -sL https://screentinker.com/scripts/raspberry-pi-setup.sh | sudo bash</code></pre>
|
||||
<p>The <code>sudo</code> is required — the installer writes systemd units, installs packages and
|
||||
configures autostart. Without it the script stops on its first line and prints the correct
|
||||
command, so nothing is half-installed.</p>
|
||||
<p>That gives you <strong>All-in-One</strong>: the server and the player on the same Pi. To use
|
||||
the Pi as a player only, pointing at a ScreenTinker server you already run:</p>
|
||||
<pre><code>curl -sL https://screentinker.com/scripts/raspberry-pi-setup.sh | sudo bash -s -- --player-only https://your-server</code></pre>
|
||||
<p>The script will:</p>
|
||||
<ul>
|
||||
<li>Install Chromium (the kiosk browser used as the player)</li>
|
||||
|
|
|
|||
|
|
@ -328,8 +328,11 @@ if [ "\$SESSION_TYPE" = "wayland" ]; then
|
|||
# (wlroots-based wayfire/labwc); if it is not, the compositor's own idle config is the
|
||||
# documented fallback and README says so.
|
||||
command -v wlopm >/dev/null 2>&1 && wlopm --on '*' 2>/dev/null || true
|
||||
# unclutter is X11-only. Under wayfire the cursor is hidden by the compositor
|
||||
# (hide_cursor / idle plugin), which the installer writes below when wayfire.ini exists.
|
||||
# unclutter is X11-only — it exits immediately here, which is why a Wayland Pi kept its cursor
|
||||
# on screen while the install looked complete. Hiding it is the COMPOSITOR's job on Wayland;
|
||||
# the installer configures wayfire's hide-cursor plugin at install time (section 9b). If this
|
||||
# Pi runs labwc instead, there is no equivalent setting and the cursor stays — README says so
|
||||
# rather than this pretending otherwise.
|
||||
else
|
||||
# Disable screen blanking and power management
|
||||
xset s off
|
||||
|
|
@ -532,6 +535,49 @@ ExecStart=-/sbin/agetty --autologin ${PI_USER} --noclear %I \$TERM
|
|||
EOF
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 9b. Wayland cursor hiding (wayfire)
|
||||
# ============================================================
|
||||
# On X11 the launcher runs `unclutter -idle 3`. On Wayland unclutter cannot work at all — there is
|
||||
# no root window to track and no client may move or hide another client's cursor — so hiding it is
|
||||
# the compositor's decision. Pi OS Bookworm on Pi 4/5 defaults to wayfire, which has a hide-cursor
|
||||
# plugin; this configures it. A previous version of this script claimed in a comment to do exactly
|
||||
# this and never did, so a Wayland Pi sat there with a mouse pointer on the sign (#245).
|
||||
#
|
||||
# Written at install time rather than from the launcher because wayfire reads this at session
|
||||
# start. It is also idempotent and non-destructive: a Pi whose owner has already tuned wayfire.ini
|
||||
# keeps their settings, and the file is backed up before the first edit either way.
|
||||
if [ -f "$PI_HOME/.config/wayfire.ini" ]; then
|
||||
log "Configuring wayfire to hide the cursor..."
|
||||
WF="$PI_HOME/.config/wayfire.ini"
|
||||
[ -f "${WF}.screentinker-bak" ] || cp "$WF" "${WF}.screentinker-bak"
|
||||
|
||||
if grep -q '^\[hide-cursor\]' "$WF"; then
|
||||
log " wayfire.ini already has [hide-cursor] — leaving it alone"
|
||||
else
|
||||
printf '\n[hide-cursor]\nhide_delay = 3000\n' >> "$WF"
|
||||
fi
|
||||
|
||||
# The plugin only loads if it is named in core's plugin list, and that list is space-separated
|
||||
# on one line. Appending to it is fiddly enough to be worth doing carefully rather than with a
|
||||
# blind sed: only touch the line when it exists and does not already mention us.
|
||||
if grep -qE '^\s*plugins\s*=' "$WF"; then
|
||||
if ! grep -E '^\s*plugins\s*=' "$WF" | grep -q 'hide-cursor'; then
|
||||
sed -i 's/^\(\s*plugins\s*=.*\)$/\1 hide-cursor/' "$WF"
|
||||
fi
|
||||
else
|
||||
warn "wayfire.ini has no [core] plugins line — add 'hide-cursor' to it to hide the pointer"
|
||||
fi
|
||||
chown "$PI_USER":"$PI_USER" "$WF" 2>/dev/null || true
|
||||
elif [ "$HAS_DESKTOP" = true ]; then
|
||||
# labwc (the newer Pi OS compositor) has no cursor-hiding option, and neither do we from the
|
||||
# outside. Say so plainly instead of leaving the operator to wonder whether it failed.
|
||||
if command -v labwc >/dev/null 2>&1; then
|
||||
warn "This Pi appears to run labwc, which has no cursor-hide setting — the pointer will stay visible."
|
||||
warn "Switch to wayfire (raspi-config > Advanced > Wayland) or to X11 if a hidden cursor matters."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 10. Pi display and boot optimizations
|
||||
# ============================================================
|
||||
|
|
@ -651,6 +697,52 @@ case "${1:-server}" in
|
|||
esac
|
||||
LOGSEOF
|
||||
chmod +x /usr/local/bin/screentinker-logs
|
||||
else
|
||||
# Player-Only gets its own pair. It used to get NONE, while section 12 below wrote an MOTD
|
||||
# advertising all three to every install — so a player Pi greeted its operator at each SSH
|
||||
# login with three commands that were never on it (#245). There is no server here to update,
|
||||
# so screentinker-update is genuinely not applicable and is not offered; status and logs are,
|
||||
# and a player with no way to answer "is it running?" is the harder machine to support.
|
||||
log "Creating management scripts (player)..."
|
||||
|
||||
cat > /usr/local/bin/screentinker-status << PSTATUSEOF
|
||||
#!/bin/bash
|
||||
echo ""
|
||||
echo "=== ScreenTinker Player Status ==="
|
||||
echo ""
|
||||
if systemctl is-active screentinker-kiosk.service &>/dev/null; then
|
||||
echo "Kiosk: RUNNING"
|
||||
else
|
||||
echo "Kiosk: STOPPED (screentinker-logs to see why)"
|
||||
fi
|
||||
echo "Server: ${SERVER_URL}"
|
||||
# Whether this player can actually reach the server it was pointed at — the first question worth
|
||||
# asking on a panel that is showing nothing.
|
||||
if curl -sf --max-time 5 "${SERVER_URL}/api/status" >/dev/null 2>&1; then
|
||||
echo "Reachable: yes"
|
||||
else
|
||||
echo "Reachable: NO (network, DNS, or the server is down)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Uptime: \$(uptime -p)"
|
||||
echo "CPU Temp: \$(vcgencmd measure_temp 2>/dev/null | cut -d= -f2 || echo 'n/a')"
|
||||
echo "Disk: \$(df -h / 2>/dev/null | tail -1 | awk '{print \$3 "/" \$2 " (" \$5 " used)"}')"
|
||||
echo "Memory: \$(free -h | awk '/Mem:/ {print \$3 " / " \$2}')"
|
||||
echo ""
|
||||
PSTATUSEOF
|
||||
chmod +x /usr/local/bin/screentinker-status
|
||||
|
||||
cat > /usr/local/bin/screentinker-logs << 'PLOGSEOF'
|
||||
#!/bin/bash
|
||||
# Only the kiosk exists on a player, so it is the default AND the only target. Accepting
|
||||
# "server" here and following an empty unit would be a worse answer than saying so.
|
||||
case "${1:-kiosk}" in
|
||||
kiosk|all) journalctl -u screentinker-kiosk.service -f --no-hostname ;;
|
||||
server) echo "This is a player-only install — there is no local server. Point at your server's logs instead." ;;
|
||||
*) echo "Usage: screentinker-logs [kiosk]" ;;
|
||||
esac
|
||||
PLOGSEOF
|
||||
chmod +x /usr/local/bin/screentinker-logs
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -666,12 +758,29 @@ cat > /etc/motd << 'MOTDEOF'
|
|||
|
||||
Open-Source Digital Signage for Any Screen
|
||||
|
||||
MOTDEOF
|
||||
|
||||
# The command list is appended SEPARATELY and per-mode, because section 11 creates
|
||||
# screentinker-update on an All-in-One install only. A single hard-coded list here is what made a
|
||||
# Player-Only Pi advertise three commands it did not have, at every SSH login (#245). The MOTD is
|
||||
# the first thing an operator reads on a machine that is misbehaving, which makes it the worst
|
||||
# place in the system to be confidently wrong.
|
||||
if [ "$PLAYER_ONLY" = false ]; then
|
||||
cat >> /etc/motd << 'MOTDCMDEOF'
|
||||
Commands:
|
||||
screentinker-status Show system info and URLs
|
||||
screentinker-update Pull latest and restart
|
||||
screentinker-logs Follow logs (server|kiosk|all)
|
||||
|
||||
MOTDEOF
|
||||
MOTDCMDEOF
|
||||
else
|
||||
cat >> /etc/motd << 'MOTDCMDEOF'
|
||||
Commands:
|
||||
screentinker-status Kiosk state, server URL, and whether it is reachable
|
||||
screentinker-logs Follow the kiosk log
|
||||
|
||||
MOTDCMDEOF
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# 13. Clean up legacy remotedisplay naming
|
||||
|
|
|
|||
|
|
@ -110,3 +110,63 @@ test('#245: the login banner spells the product name', () => {
|
|||
const banner = lines.join('\n');
|
||||
assert.ok(banner.includes('| | | |'), 'the n glyph is missing from the banner — it reads "Scree Tinker"');
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// #245 round two: the installer advertised what it had not installed.
|
||||
//
|
||||
// The MOTD is written unconditionally and listed three commands, but section 11 creates them only
|
||||
// on an All-in-One install. So a Player-Only Pi greeted its operator at every SSH login with three
|
||||
// commands that were not on it. Same failure shape as the first round — the script describing a
|
||||
// state it never reached — and the same reporter found both.
|
||||
|
||||
// The generated MOTD for a given mode: the base heredoc plus whichever command block that mode
|
||||
// appends. Extracted rather than re-typed, so a future edit to either half shows up here.
|
||||
function motdFor(playerOnly) {
|
||||
const base = SRC.match(/cat > \/etc\/motd << 'MOTDEOF'\n([\s\S]*?)\nMOTDEOF/);
|
||||
assert.ok(base, 'MOTD heredoc not found — did the installer restructure?');
|
||||
const blocks = [...SRC.matchAll(/cat >> \/etc\/motd << 'MOTDCMDEOF'\n([\s\S]*?)\nMOTDCMDEOF/g)];
|
||||
assert.equal(blocks.length, 2, 'expected exactly two per-mode MOTD command blocks');
|
||||
// The all-in-one block is written in the `if [ "$PLAYER_ONLY" = false ]` arm, which comes first.
|
||||
return base[1] + (playerOnly ? blocks[1][1] : blocks[0][1]);
|
||||
}
|
||||
|
||||
// Which management commands the installer actually creates in a given mode.
|
||||
function commandsCreated(playerOnly) {
|
||||
const all = [...SRC.matchAll(/cat > \/usr\/local\/bin\/(screentinker-[a-z]+)/g)].map((m) => m[1]);
|
||||
// Section 11 is an if/else: the all-in-one arm creates update, the player arm does not.
|
||||
return playerOnly ? all.filter((c) => c !== 'screentinker-update') : all;
|
||||
}
|
||||
|
||||
test('#245: the MOTD never advertises a command that mode did not install', () => {
|
||||
for (const playerOnly of [false, true]) {
|
||||
const motd = motdFor(playerOnly);
|
||||
const created = commandsCreated(playerOnly);
|
||||
const advertised = [...motd.matchAll(/(screentinker-[a-z]+)/g)].map((m) => m[1]);
|
||||
assert.ok(advertised.length > 0, `${playerOnly ? 'player' : 'all-in-one'} MOTD lists no commands at all`);
|
||||
for (const cmd of advertised) {
|
||||
assert.ok(created.includes(cmd),
|
||||
`the ${playerOnly ? 'Player-Only' : 'All-in-One'} MOTD advertises ${cmd}, which that mode does not install`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#245: a Player-Only Pi is not left with no diagnostics at all', () => {
|
||||
// The cheap fix would have been to print nothing on a player. That trades a wrong banner for a
|
||||
// machine an operator cannot inspect over SSH, which is the harder support call.
|
||||
const motd = motdFor(true);
|
||||
assert.match(motd, /screentinker-status/, 'a player still needs to answer "is it running?"');
|
||||
assert.match(motd, /screentinker-logs/, 'and "why did it stop?"');
|
||||
assert.doesNotMatch(motd, /screentinker-update/,
|
||||
'there is no local server to update on a player-only install, so it must not be offered');
|
||||
});
|
||||
|
||||
test('#245: the Wayland cursor claim is backed by something that runs', () => {
|
||||
// The launcher used to state that the installer wrote the compositor cursor config "below". It
|
||||
// did not: wayfire.ini and hide_cursor each appeared exactly once, both inside that comment.
|
||||
const code = SRC.split('\n').filter((l) => !/^\s*#/.test(l)).join('\n');
|
||||
assert.match(code, /wayfire\.ini/, 'no wayfire.ini handling outside comments');
|
||||
assert.match(code, /\[hide-cursor\]/, 'the hide-cursor section is never written');
|
||||
assert.match(code, /hide_delay/, 'the plugin is configured without a delay');
|
||||
// Non-destructive: a Pi whose owner already tuned wayfire must not silently lose it.
|
||||
assert.match(code, /screentinker-bak/, 'wayfire.ini is edited without a backup');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue