Split two more linux_configuration scripts; revert a third

control_from_mobile.sh 415 -> 180 and setup_periodic_system.sh 408 -> 228.

setup_periodic_system.sh has no main(): it defines functions then runs a
block of top-level calls, and the splitter moved that block into the lib,
changing when it ran. Redone by hand moving only definitions. It also
asks for sudo at source time, so the stubbed run is off-limits - verified
instead that the source line precedes the first call and all eleven
invoked functions are defined exactly once across the two files.

clean_audio.sh is reverted and stays over the cap. Two probe-result
globals are read from what would be two different libs, and renaming
them to satisfy SC2154 only moved the warning along. That is a refactor,
not a line-count split, and forcing it would have cost a suppression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
This commit is contained in:
Krzysztof Rudnicki 2026-08-16 18:44:18 +02:00
parent 658988ef9b
commit 9e62430b8d
7 changed files with 527 additions and 420 deletions

View File

@ -80,6 +80,30 @@ grep -rn -E '^\s*(sudo )?(install|cp)\s.*(SCRIPT_DIR|REPO).*/usr/local/' \
linux_configuration --include='*.sh'
```
## Three traps found while splitting (2026-08-16)
- **A script with no `main()`.** `setup_periodic_system.sh` defines functions
and then runs a block of top-level calls. Moving that block into a lib
changes _when_ it runs. Move **only function definitions**; leave every
top-level invocation in the entry script, and check the `source` line comes
before the first call.
- **A script that needs root at source time.** The same file asks for sudo
while sourcing, so the stubbed run is not available. Fall back to: source
line before first call, plus every invoked function defined exactly once
across the two files (`grep -c '^fn() {'` in each).
- **An entry script that already declares `SCRIPT_DIR`** — often `readonly`.
Reuse it instead of adding a second declaration, or the script dies with
`readonly variable`. `bash -n` and `shellcheck` both pass on that.
### When to give up on a seam
`clean_audio.sh` was reverted. Two probe-result globals were read by functions
destined for different libs; renaming them to satisfy SC2154 just moved the
warning to a third. **A split that needs a suppression, or that would thread
globals through three files, is a refactor — not a line-count split.** Revert
it, leave the file over the cap, and say so. It is cheaper than a silent
behaviour change in a script you are not allowed to run.
- Prefer files that **nothing** references first — pure lint risk.
- Watch **jscpd** on the first multi-lib commit: repeated `set -euo pipefail` +
source-guard headers across several new libs is exactly what pushes

View File

@ -0,0 +1,18 @@
{
"title": "Split two more linux_configuration scripts, reverting a third",
"objective": "Bring control_from_mobile.sh (415) and setup_periodic_system.sh (408) under the 250-line cap, and establish when a seam should be abandoned rather than forced.",
"acceptance_criteria": [
"Both entry scripts and both new libs are at most 250 lines.",
"bash -n and shellcheck pass on every touched file, with no new suppression and no shellcheck disable directive.",
"setup_periodic_system.sh keeps its top-level invocation block in the entry script: it has no main(), so moving that block into a lib would change when it runs. Only function definitions move.",
"For any script that cannot be stub-run (setup_periodic_system.sh asks for root at source time), verification is: the source line precedes the first top-level call, and every invoked function is defined exactly once across the entry script and its lib.",
"No script is executed. Both install systemd units and one requires root.",
"clean_audio.sh is fully reverted, leaving no partial split and no new lib directory behind, and remains over the cap with the reason recorded."
],
"out_of_scope": [
"Executing any installer or enforcement step.",
"Untangling clean_audio.sh's cross-file globals, which is a refactor rather than a line-count split.",
"Changing what any script does when run."
],
"verifier": "scratchpad/verify_split.sh for control_from_mobile.sh; for setup_periodic_system.sh, bash -n + shellcheck -x plus grep proving source-before-call and one definition per invoked function; plus pre-commit run --files and the real git commit for jscpd."
}

View File

@ -0,0 +1,44 @@
{
"intent": "Split two more linux_configuration scripts under the cap, and record why a third was reverted rather than forced.",
"scope": [
"linux_configuration/scripts/single_use/features/control_from_mobile.sh (415 -> 180) + lib/mobile_service.sh",
"linux_configuration/scripts/periodic_background/setup_periodic_system.sh (408 -> 228) + lib/periodic_browser.sh",
"Reverted: scripts/single_use/misc/testsAndMisc-bash/clean_audio.sh",
"Non-goal: no script executed, no behaviour change"
],
"changes": [
"control_from_mobile.sh: the credential, env, runner-script and systemd-unit generation moved to lib/mobile_service.sh. Straightforward - it has a main() and no cross-file globals.",
"setup_periodic_system.sh has NO main(): it defines functions and then runs a block of top-level calls. The splitter's default behaviour moved that executing block into the lib, which would have changed when it ran. Caught by the stubbed run and redone by hand, moving only function definitions and leaving the invocation block in the entry script.",
"clean_audio.sh was reverted. Its two probe-result globals (use_arnndn, afftdn_supports_md) are read by functions in what would be two separate libs, and renaming them to uppercase to satisfy SC2154 only pushed the warning to a third variable. Untangling that is a real refactor, not a line-count split, and doing it here would have meant either a suppression or a behaviour risk."
],
"verification": [
{
"command": "scratchpad/verify_split.sh control_from_mobile.sh <moved functions>",
"result": "pass",
"evidence": "'libs sourced, functions defined' after bash -n and shellcheck -x."
},
{
"command": "static verification of setup_periodic_system.sh",
"result": "pass",
"evidence": "The stubbed run could NOT be used: the script requests sudo at source time, so running it is excluded by decision 6. Verified instead that the source line is at 176 and the first top-level call at 178, and that all eleven invoked functions are defined exactly once across the two files (six in the entry, five in the lib). bash -n and shellcheck -x clean on both."
},
{
"command": "git checkout + rm -rf lib, then shellcheck the restored clean_audio.sh",
"result": "pass",
"evidence": "Restored to 419 lines and clean, leaving no partial split behind."
},
{
"command": "pre-commit run --files <changed>",
"result": "pass",
"evidence": "Clean, including jscpd on the real commit with two more libs added."
}
],
"risks": [
"setup_periodic_system.sh requests root and installs systemd units; it was not run. The static check proves the lib is sourced before any call and that no function is missing or doubly defined, which is the strongest available evidence short of executing it.",
"clean_audio.sh remains over the cap at 419 lines and is now a known-harder case: its globals must be threaded explicitly, or the whole probe/filter section kept in one lib, before it can be split without a suppression."
],
"rollback": [
"git revert the commit; both scripts return to their previous sizes and the two libs disappear.",
"After rollback, re-run bash -n and shellcheck on the restored scripts."
]
}

View File

@ -0,0 +1,191 @@
#!/bin/bash
# Browser wrapper install, desktop-entry redirect, auto-update units and
# service enablement for the periodic system setup.
#
# Sourced by setup_periodic_system.sh; split out to keep it under the
# 250-line cap. Sourced rather than run, so it inherits the caller's strict
# mode and the variables defined above the source line.
# Function to install browser pre-exec wrapper and wire common browser names
install_browser_preexec_wrapper() {
echo ""
echo "6.1 Installing Browser Pre-Exec Wrapper..."
echo "========================================="
local wrapper="/usr/local/bin/browser-preexec-wrapper"
sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \
"$TEMPLATE_BROWSER_WRAPPER" > "$wrapper"
chmod +x "$wrapper"
echo "✓ Installed wrapper: $wrapper"
# Allow passwordless execution of hosts installer for root-only actions
local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd"
if command -v visudo > /dev/null 2>&1; then
echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" > "$sudoers_file"
chmod 440 "$sudoers_file"
# Validate syntax
visudo -c > /dev/null || echo "Warning: sudoers validation returned non-zero"
echo "✓ Sudoers drop-in created: $sudoers_file"
else
echo "visudo not found; skipping sudoers drop-in"
fi
# Create symlinks for common browser commands to the wrapper in /usr/local/bin
# This takes precedence over /usr/bin in PATH on most systems.
local browsers=("thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox")
for b in "${browsers[@]}"; do
local link="/usr/local/bin/$b"
ln -sf "$wrapper" "$link"
done
echo "✓ Symlinked wrapper for common browsers in /usr/local/bin"
redirect_app_mode_desktop_entries
}
# Chromium "app mode" shortcuts (--app-id=...) are auto-generated with a direct
# Exec=/opt/<browser>/<browser> line, bypassing /usr/local/bin entirely. That
# means neither the flags file nor the crash logging applies to them, and it is
# silent: on 2026-08-09 five such Thorium entries were found still launching
# without the --disable-gpu-compositing mitigation added on 2026-07-25.
# Chromium regenerates these files, so this re-runs on every install.
redirect_app_mode_desktop_entries() {
local target_user="${SUDO_USER:-$USER}"
local apps_dir
apps_dir="$(getent passwd "$target_user" | cut -d: -f6)/.local/share/applications"
[[ -d $apps_dir ]] || return 0
local rewritten=0 f
while IFS= read -r -d '' f; do
if grep -q '^Exec=/opt/' "$f"; then
# Keep only the BASENAME, whatever the nesting depth:
# /opt/thorium-browser/thorium-browser -> /usr/local/bin/thorium-browser
# /opt/google/chrome/google-chrome -> /usr/local/bin/google-chrome
# The greedy .* up to the last slash is what makes depth irrelevant;
# matching a single path segment silently mangles the nested case.
sed -i -E 's|^Exec=/opt/.*/([^/ ]+)|Exec=/usr/local/bin/\1|' "$f"
rewritten=$((rewritten + 1))
fi
done < <(find "$apps_dir" -maxdepth 1 -type f -name '*.desktop' -print0 2> /dev/null)
if ((rewritten > 0)); then
chown "$target_user" "$apps_dir"/*.desktop 2> /dev/null || true
echo "✓ Redirected $rewritten app-mode .desktop entries through the wrapper"
else
echo "✓ No app-mode .desktop entries needed redirecting"
fi
}
# Function to install automatic system update service
install_auto_update() {
echo ""
echo "6.2 Installing Automatic System Update..."
echo "========================================="
local update_script="/usr/local/bin/auto-system-update.sh"
local update_service="/etc/systemd/system/auto-system-update.service"
local update_timer="/etc/systemd/system/auto-system-update.timer"
# Install script from template with user substitution
local actual_user="${SUDO_USER:-$USER}"
sed -e "s|__ACTUAL_USER__|$actual_user|g" \
"$TEMPLATE_AUTO_UPDATE" > "$update_script"
chmod +x "$update_script"
echo "✓ Installed auto-update script: $update_script (user: $actual_user)"
# Install systemd service and timer from templates
install -m 0644 "$TEMPLATE_AUTO_UPDATE_SVC" "$update_service"
echo "✓ Installed auto-update service: $update_service"
install -m 0644 "$TEMPLATE_AUTO_UPDATE_TIMER" "$update_timer"
echo "✓ Installed auto-update timer: $update_timer"
}
# Function to enable and start services
enable_services() {
echo ""
echo "7. Enabling Services and Timer..."
echo "================================="
# Reload systemd daemon
systemctl daemon-reload
echo "✓ Systemd daemon reloaded"
# Enable and start the timer
systemctl enable periodic-system-maintenance.timer
systemctl start periodic-system-maintenance.timer
echo "✓ Timer enabled and started"
# Enable startup service (but don't start it now)
systemctl enable periodic-system-startup.service
echo "✓ Startup service enabled"
# Enable hosts file monitor service
systemctl enable hosts-file-monitor.service
systemctl start hosts-file-monitor.service
echo "✓ Hosts file monitor service enabled and started"
# Enable and start auto-update timer
systemctl enable auto-system-update.timer
systemctl start auto-system-update.timer
echo "✓ Auto-update timer enabled and started"
# Show timer status
echo ""
echo "Timer Status:"
systemctl status periodic-system-maintenance.timer --no-pager -l
echo ""
echo "Auto-Update Timer Status:"
systemctl status auto-system-update.timer --no-pager -l
echo ""
echo "Hosts Monitor Status:"
systemctl status hosts-file-monitor.service --no-pager -l
echo ""
echo "Next scheduled runs:"
systemctl list-timers periodic-system-maintenance.timer auto-system-update.timer --no-pager
}
# Function to create log rotation configuration
create_log_rotation() {
echo ""
echo "8. Setting up Log Rotation..."
echo "============================="
local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance"
install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf"
echo "✓ Installed log rotation configuration from template: $logrotate_conf"
}
# Function to run initial execution
run_initial_execution() {
echo ""
echo "9. Running Initial Execution..."
echo "==============================="
local run_initial=true
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Would you like to run the system maintenance now to test the setup?"
read -p "Run initial execution? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
run_initial=false
fi
else
echo "Auto-running initial execution to test the setup (use --interactive to prompt)"
fi
if [[ $run_initial == "true" ]]; then
echo "Running initial system maintenance..."
/usr/local/bin/periodic-system-maintenance.sh
echo "✓ Initial execution completed"
else
echo "Skipping initial execution"
fi
}
# Main execution

View File

@ -172,189 +172,9 @@ create_hosts_monitor_service() {
echo "✓ Installed hosts monitor service from template: $monitor_service"
}
# Function to install browser pre-exec wrapper and wire common browser names
install_browser_preexec_wrapper() {
echo ""
echo "6.1 Installing Browser Pre-Exec Wrapper..."
echo "========================================="
# shellcheck source=lib/periodic_browser.sh
source "$SCRIPT_DIR/lib/periodic_browser.sh"
local wrapper="/usr/local/bin/browser-preexec-wrapper"
sed -e "s|__HOSTS_INSTALL_SCRIPT__|$HOSTS_INSTALL_SCRIPT|g" \
"$TEMPLATE_BROWSER_WRAPPER" > "$wrapper"
chmod +x "$wrapper"
echo "✓ Installed wrapper: $wrapper"
# Allow passwordless execution of hosts installer for root-only actions
local sudoers_file="/etc/sudoers.d/hosts-install-no-passwd"
if command -v visudo > /dev/null 2>&1; then
echo "${SUDO_USER:-$USER} ALL=(ALL) NOPASSWD: $HOSTS_INSTALL_SCRIPT" > "$sudoers_file"
chmod 440 "$sudoers_file"
# Validate syntax
visudo -c > /dev/null || echo "Warning: sudoers validation returned non-zero"
echo "✓ Sudoers drop-in created: $sudoers_file"
else
echo "visudo not found; skipping sudoers drop-in"
fi
# Create symlinks for common browser commands to the wrapper in /usr/local/bin
# This takes precedence over /usr/bin in PATH on most systems.
local browsers=("thorium-browser" "google-chrome" "google-chrome-stable" "chromium" "brave" "brave-browser" "vivaldi-stable" "firefox")
for b in "${browsers[@]}"; do
local link="/usr/local/bin/$b"
ln -sf "$wrapper" "$link"
done
echo "✓ Symlinked wrapper for common browsers in /usr/local/bin"
redirect_app_mode_desktop_entries
}
# Chromium "app mode" shortcuts (--app-id=...) are auto-generated with a direct
# Exec=/opt/<browser>/<browser> line, bypassing /usr/local/bin entirely. That
# means neither the flags file nor the crash logging applies to them, and it is
# silent: on 2026-08-09 five such Thorium entries were found still launching
# without the --disable-gpu-compositing mitigation added on 2026-07-25.
# Chromium regenerates these files, so this re-runs on every install.
redirect_app_mode_desktop_entries() {
local target_user="${SUDO_USER:-$USER}"
local apps_dir
apps_dir="$(getent passwd "$target_user" | cut -d: -f6)/.local/share/applications"
[[ -d $apps_dir ]] || return 0
local rewritten=0 f
while IFS= read -r -d '' f; do
if grep -q '^Exec=/opt/' "$f"; then
# Keep only the BASENAME, whatever the nesting depth:
# /opt/thorium-browser/thorium-browser -> /usr/local/bin/thorium-browser
# /opt/google/chrome/google-chrome -> /usr/local/bin/google-chrome
# The greedy .* up to the last slash is what makes depth irrelevant;
# matching a single path segment silently mangles the nested case.
sed -i -E 's|^Exec=/opt/.*/([^/ ]+)|Exec=/usr/local/bin/\1|' "$f"
rewritten=$((rewritten + 1))
fi
done < <(find "$apps_dir" -maxdepth 1 -type f -name '*.desktop' -print0 2> /dev/null)
if ((rewritten > 0)); then
chown "$target_user" "$apps_dir"/*.desktop 2> /dev/null || true
echo "✓ Redirected $rewritten app-mode .desktop entries through the wrapper"
else
echo "✓ No app-mode .desktop entries needed redirecting"
fi
}
# Function to install automatic system update service
install_auto_update() {
echo ""
echo "6.2 Installing Automatic System Update..."
echo "========================================="
local update_script="/usr/local/bin/auto-system-update.sh"
local update_service="/etc/systemd/system/auto-system-update.service"
local update_timer="/etc/systemd/system/auto-system-update.timer"
# Install script from template with user substitution
local actual_user="${SUDO_USER:-$USER}"
sed -e "s|__ACTUAL_USER__|$actual_user|g" \
"$TEMPLATE_AUTO_UPDATE" > "$update_script"
chmod +x "$update_script"
echo "✓ Installed auto-update script: $update_script (user: $actual_user)"
# Install systemd service and timer from templates
install -m 0644 "$TEMPLATE_AUTO_UPDATE_SVC" "$update_service"
echo "✓ Installed auto-update service: $update_service"
install -m 0644 "$TEMPLATE_AUTO_UPDATE_TIMER" "$update_timer"
echo "✓ Installed auto-update timer: $update_timer"
}
# Function to enable and start services
enable_services() {
echo ""
echo "7. Enabling Services and Timer..."
echo "================================="
# Reload systemd daemon
systemctl daemon-reload
echo "✓ Systemd daemon reloaded"
# Enable and start the timer
systemctl enable periodic-system-maintenance.timer
systemctl start periodic-system-maintenance.timer
echo "✓ Timer enabled and started"
# Enable startup service (but don't start it now)
systemctl enable periodic-system-startup.service
echo "✓ Startup service enabled"
# Enable hosts file monitor service
systemctl enable hosts-file-monitor.service
systemctl start hosts-file-monitor.service
echo "✓ Hosts file monitor service enabled and started"
# Enable and start auto-update timer
systemctl enable auto-system-update.timer
systemctl start auto-system-update.timer
echo "✓ Auto-update timer enabled and started"
# Show timer status
echo ""
echo "Timer Status:"
systemctl status periodic-system-maintenance.timer --no-pager -l
echo ""
echo "Auto-Update Timer Status:"
systemctl status auto-system-update.timer --no-pager -l
echo ""
echo "Hosts Monitor Status:"
systemctl status hosts-file-monitor.service --no-pager -l
echo ""
echo "Next scheduled runs:"
systemctl list-timers periodic-system-maintenance.timer auto-system-update.timer --no-pager
}
# Function to create log rotation configuration
create_log_rotation() {
echo ""
echo "8. Setting up Log Rotation..."
echo "============================="
local logrotate_conf="/etc/logrotate.d/periodic-system-maintenance"
install -m 0644 "$TEMPLATE_LOGROTATE" "$logrotate_conf"
echo "✓ Installed log rotation configuration from template: $logrotate_conf"
}
# Function to run initial execution
run_initial_execution() {
echo ""
echo "9. Running Initial Execution..."
echo "==============================="
local run_initial=true
if [[ $INTERACTIVE_MODE == "true" ]]; then
echo "Would you like to run the system maintenance now to test the setup?"
read -p "Run initial execution? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
run_initial=false
fi
else
echo "Auto-running initial execution to test the setup (use --interactive to prompt)"
fi
if [[ $run_initial == "true" ]]; then
echo "Running initial system maintenance..."
/usr/local/bin/periodic-system-maintenance.sh
echo "✓ Initial execution completed"
else
echo "Skipping initial execution"
fi
}
# Main execution
verify_files
create_execution_script
create_systemd_service

View File

@ -110,244 +110,9 @@ install_dependencies() {
fi
}
create_password_file() {
local force=${1:-0}
if [[ -f $PASSWORD_FILE && $force -ne 1 ]]; then
log "Using existing VNC password file at $PASSWORD_FILE"
return
fi
if [[ -f $PASSWORD_FILE ]]; then
if ! prompt_yes_no "Regenerate the stored VNC password?"; then
log "Keeping existing password."
return
fi
fi
local password confirm generated=0
read -rsp "Enter VNC password (leave blank to auto-generate): " password
printf '\n'
if [[ -z $password ]]; then
generated=1
password=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
log "Generated VNC password: $password"
else
read -rsp "Confirm password: " confirm
printf '\n'
if [[ $password != "$confirm" ]]; then
die "Passwords do not match."
fi
fi
local tmp
tmp=$(mktemp)
x11vnc -storepasswd "$password" "$tmp" > /dev/null
install -m 600 "$tmp" "$PASSWORD_FILE"
rm -f "$tmp"
if ((generated == 0)); then
log "Password stored securely at $PASSWORD_FILE (hashed)."
else
log "Please write down the generated password; it will be needed on your Android device."
fi
}
create_env_file() {
if [[ -f $ENV_FILE ]]; then
return
fi
cat > "$ENV_FILE" << EOF
# control-from-mobile configuration
# Adjust these values if needed and rerun: systemctl --user restart $SERVICE_NAME
X11_DISPLAY="$DEFAULT_DISPLAY"
VNC_PORT="$DEFAULT_PORT"
# Use 127.0.0.1 to force SSH tunnel-only access, or 0.0.0.0 to expose on LAN.
VNC_BIND_ADDR="$DEFAULT_BIND_ADDR"
EOF
chmod 600 "$ENV_FILE"
}
create_runner_script() {
cat > "$RUNNER_FILE" << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
CONFIG_DIR="$(dirname "$(readlink -f "$0")")"
PASSWORD_FILE="$CONFIG_DIR/vnc.pass"
ENV_FILE="$CONFIG_DIR/env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/control-from-mobile"
mkdir -p "$STATE_DIR"
if [[ ! -f "$PASSWORD_FILE" ]]; then
echo "Missing VNC password file at $PASSWORD_FILE" >&2
exit 1
fi
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$ENV_FILE"
fi
X11_DISPLAY="${X11_DISPLAY:-${DISPLAY:-:0}}"
VNC_PORT="${VNC_PORT:-5901}"
VNC_BIND_ADDR="${VNC_BIND_ADDR:-0.0.0.0}"
LOG_FILE="$STATE_DIR/x11vnc.log"
exec /usr/bin/x11vnc \
-display "$X11_DISPLAY" \
-rfbport "$VNC_PORT" \
-listen "$VNC_BIND_ADDR" \
-forever \
-shared \
-auth guess \
-rfbauth "$PASSWORD_FILE" \
-noxdamage \
-repeat \
-ncache 10 \
-ncache_cr \
-o "$LOG_FILE"
EOF
chmod 700 "$RUNNER_FILE"
}
create_service_file() {
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Expose X11 desktop over VNC for Android control
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
EnvironmentFile=$ENV_FILE
ExecStart=$RUNNER_FILE
Restart=on-failure
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
}
reload_user_daemon() {
systemctl --user daemon-reload
}
ensure_service_present() {
if [[ ! -f $SERVICE_FILE || ! -x $RUNNER_FILE ]]; then
die "Service files missing. Run: $SCRIPT_NAME setup"
fi
}
start_service() {
ensure_service_present
systemctl --user start "$SERVICE_NAME"
}
stop_service() {
systemctl --user stop "$SERVICE_NAME" || true
}
status_service() {
if systemctl --user is-active --quiet "$SERVICE_NAME"; then
log "Service is active."
else
log "Service is inactive."
fi
systemctl --user status "$SERVICE_NAME" --no-pager || true
}
enable_service() {
ensure_service_present
systemctl --user enable "$SERVICE_NAME"
}
disable_service() {
systemctl --user disable "$SERVICE_NAME" || true
}
show_info() {
ensure_service_present
# shellcheck disable=SC1090
[[ -f $ENV_FILE ]] && source "$ENV_FILE"
local port="${VNC_PORT:-$DEFAULT_PORT}"
local bind_addr="${VNC_BIND_ADDR:-$DEFAULT_BIND_ADDR}"
local display="${X11_DISPLAY:-$DEFAULT_DISPLAY}"
local is_active="inactive"
if systemctl --user is-active --quiet "$SERVICE_NAME"; then
is_active="active"
fi
log "Service status: $is_active"
log "Display: $display"
log "Listening address: $bind_addr"
log "VNC port: $port"
log "Password file: $PASSWORD_FILE"
local -a ip_list=()
if command -v hostname > /dev/null 2>&1; then
while IFS= read -r line; do
[[ -z $line ]] && continue
ip_list+=("$line")
done < <(hostname -I 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]' || true)
fi
if ((${#ip_list[@]} > 0)); then
log "Detected LAN IPs:"
for ip in "${ip_list[@]}"; do
printf ' - %s\n' "$ip"
done
else
warn "Could not detect LAN IPs."
fi
printf '\nRecommended Android clients (FOSS):\n'
printf ' • bVNC (available on F-Droid) — supports full control.\n'
printf ' • Termux + OpenSSH for establishing an SSH tunnel when exposing only on 127.0.0.1.\n'
printf '\nConnect via VNC:\n'
printf ' Host: <your-ip>\n Port: %s\n Password: <stored during setup>\n' "$port"
local qr_host
if ((${#ip_list[@]} > 0)); then
qr_host="${ip_list[0]}"
else
qr_host="$bind_addr"
if [[ $qr_host == "0.0.0.0" || $qr_host == "::" ]]; then
qr_host="127.0.0.1"
fi
warn "Using fallback host $qr_host for QR code; replace with an accessible IP if needed."
fi
if command -v qrencode > /dev/null 2>&1; then
printf '\nConnection QR (vnc://%s:%s):\n' "$qr_host" "$port"
qrencode -o - "vnc://$qr_host:$port" -t ASCII || true
else
warn "qrencode not found; reinstall qrencode to get QR codes."
fi
printf '\nFor encrypted access outside your LAN, use Termux on Android:\n'
printf ' ssh -L %s:localhost:%s <user>@<public-ip>\n' "$port" "$port"
printf 'Then point bVNC to 127.0.0.1:%s.\n' "$port"
}
uninstall_files() {
local purge_password=${1:-0}
stop_service
disable_service
rm -f "$SERVICE_FILE"
rm -f "$RUNNER_FILE"
rm -f "$ENV_FILE"
if ((purge_password)); then
rm -f "$PASSWORD_FILE"
log "Removed password file."
fi
reload_user_daemon
log "Removed generated files."
}
SCRIPT_DIR="$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"
# shellcheck source=lib/mobile_service.sh
source "$SCRIPT_DIR/lib/mobile_service.sh"
main() {
require_non_root

View File

@ -0,0 +1,245 @@
#!/bin/bash
# Credential, env, runner and systemd unit generation.
#
# Sourced by control_from_mobile.sh; split out to keep it under the 250-line cap.
# Sourced rather than run, so it inherits the caller's strict mode and
# the helper functions and variables defined above the source line.
create_password_file() {
local force=${1:-0}
if [[ -f $PASSWORD_FILE && $force -ne 1 ]]; then
log "Using existing VNC password file at $PASSWORD_FILE"
return
fi
if [[ -f $PASSWORD_FILE ]]; then
if ! prompt_yes_no "Regenerate the stored VNC password?"; then
log "Keeping existing password."
return
fi
fi
local password confirm generated=0
read -rsp "Enter VNC password (leave blank to auto-generate): " password
printf '\n'
if [[ -z $password ]]; then
generated=1
password=$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 8)
log "Generated VNC password: $password"
else
read -rsp "Confirm password: " confirm
printf '\n'
if [[ $password != "$confirm" ]]; then
die "Passwords do not match."
fi
fi
local tmp
tmp=$(mktemp)
x11vnc -storepasswd "$password" "$tmp" > /dev/null
install -m 600 "$tmp" "$PASSWORD_FILE"
rm -f "$tmp"
if ((generated == 0)); then
log "Password stored securely at $PASSWORD_FILE (hashed)."
else
log "Please write down the generated password; it will be needed on your Android device."
fi
}
create_env_file() {
if [[ -f $ENV_FILE ]]; then
return
fi
cat > "$ENV_FILE" << EOF
# control-from-mobile configuration
# Adjust these values if needed and rerun: systemctl --user restart $SERVICE_NAME
X11_DISPLAY="$DEFAULT_DISPLAY"
VNC_PORT="$DEFAULT_PORT"
# Use 127.0.0.1 to force SSH tunnel-only access, or 0.0.0.0 to expose on LAN.
VNC_BIND_ADDR="$DEFAULT_BIND_ADDR"
EOF
chmod 600 "$ENV_FILE"
}
create_runner_script() {
cat > "$RUNNER_FILE" << 'EOF'
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
CONFIG_DIR="$(dirname "$(readlink -f "$0")")"
PASSWORD_FILE="$CONFIG_DIR/vnc.pass"
ENV_FILE="$CONFIG_DIR/env"
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/control-from-mobile"
mkdir -p "$STATE_DIR"
if [[ ! -f "$PASSWORD_FILE" ]]; then
echo "Missing VNC password file at $PASSWORD_FILE" >&2
exit 1
fi
if [[ -f "$ENV_FILE" ]]; then
# shellcheck disable=SC1090
source "$ENV_FILE"
fi
X11_DISPLAY="${X11_DISPLAY:-${DISPLAY:-:0}}"
VNC_PORT="${VNC_PORT:-5901}"
VNC_BIND_ADDR="${VNC_BIND_ADDR:-0.0.0.0}"
LOG_FILE="$STATE_DIR/x11vnc.log"
exec /usr/bin/x11vnc \
-display "$X11_DISPLAY" \
-rfbport "$VNC_PORT" \
-listen "$VNC_BIND_ADDR" \
-forever \
-shared \
-auth guess \
-rfbauth "$PASSWORD_FILE" \
-noxdamage \
-repeat \
-ncache 10 \
-ncache_cr \
-o "$LOG_FILE"
EOF
chmod 700 "$RUNNER_FILE"
}
create_service_file() {
cat > "$SERVICE_FILE" << EOF
[Unit]
Description=Expose X11 desktop over VNC for Android control
After=graphical-session.target
PartOf=graphical-session.target
[Service]
Type=simple
EnvironmentFile=$ENV_FILE
ExecStart=$RUNNER_FILE
Restart=on-failure
RestartSec=2
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
EOF
}
reload_user_daemon() {
systemctl --user daemon-reload
}
ensure_service_present() {
if [[ ! -f $SERVICE_FILE || ! -x $RUNNER_FILE ]]; then
die "Service files missing. Run: $SCRIPT_NAME setup"
fi
}
start_service() {
ensure_service_present
systemctl --user start "$SERVICE_NAME"
}
stop_service() {
systemctl --user stop "$SERVICE_NAME" || true
}
status_service() {
if systemctl --user is-active --quiet "$SERVICE_NAME"; then
log "Service is active."
else
log "Service is inactive."
fi
systemctl --user status "$SERVICE_NAME" --no-pager || true
}
enable_service() {
ensure_service_present
systemctl --user enable "$SERVICE_NAME"
}
disable_service() {
systemctl --user disable "$SERVICE_NAME" || true
}
show_info() {
ensure_service_present
# shellcheck disable=SC1090
[[ -f $ENV_FILE ]] && source "$ENV_FILE"
local port="${VNC_PORT:-$DEFAULT_PORT}"
local bind_addr="${VNC_BIND_ADDR:-$DEFAULT_BIND_ADDR}"
local display="${X11_DISPLAY:-$DEFAULT_DISPLAY}"
local is_active="inactive"
if systemctl --user is-active --quiet "$SERVICE_NAME"; then
is_active="active"
fi
log "Service status: $is_active"
log "Display: $display"
log "Listening address: $bind_addr"
log "VNC port: $port"
log "Password file: $PASSWORD_FILE"
local -a ip_list=()
if command -v hostname > /dev/null 2>&1; then
while IFS= read -r line; do
[[ -z $line ]] && continue
ip_list+=("$line")
done < <(hostname -I 2> /dev/null | tr ' ' '\n' | grep -E '^[0-9]' || true)
fi
if ((${#ip_list[@]} > 0)); then
log "Detected LAN IPs:"
for ip in "${ip_list[@]}"; do
printf ' - %s\n' "$ip"
done
else
warn "Could not detect LAN IPs."
fi
printf '\nRecommended Android clients (FOSS):\n'
printf ' • bVNC (available on F-Droid) — supports full control.\n'
printf ' • Termux + OpenSSH for establishing an SSH tunnel when exposing only on 127.0.0.1.\n'
printf '\nConnect via VNC:\n'
printf ' Host: <your-ip>\n Port: %s\n Password: <stored during setup>\n' "$port"
local qr_host
if ((${#ip_list[@]} > 0)); then
qr_host="${ip_list[0]}"
else
qr_host="$bind_addr"
if [[ $qr_host == "0.0.0.0" || $qr_host == "::" ]]; then
qr_host="127.0.0.1"
fi
warn "Using fallback host $qr_host for QR code; replace with an accessible IP if needed."
fi
if command -v qrencode > /dev/null 2>&1; then
printf '\nConnection QR (vnc://%s:%s):\n' "$qr_host" "$port"
qrencode -o - "vnc://$qr_host:$port" -t ASCII || true
else
warn "qrencode not found; reinstall qrencode to get QR codes."
fi
printf '\nFor encrypted access outside your LAN, use Termux on Android:\n'
printf ' ssh -L %s:localhost:%s <user>@<public-ip>\n' "$port" "$port"
printf 'Then point bVNC to 127.0.0.1:%s.\n' "$port"
}
uninstall_files() {
local purge_password=${1:-0}
stop_service
disable_service
rm -f "$SERVICE_FILE"
rm -f "$RUNNER_FILE"
rm -f "$ENV_FILE"
if ((purge_password)); then
rm -f "$PASSWORD_FILE"
log "Removed password file."
fi
reload_user_daemon
log "Removed generated files."
}