installer: make dashboard own install flow
This commit is contained in:
6 files changed
+287
-352
No files matched your search
Executable
+137
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env bash
|
||||
# Local smoke test for the foreground installer UI supervisor.
|
||||
# Runs without rebuilding the ISO.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DASHBOARD="$ROOT/configs/airootfs/usr/local/bin/omarchy-install-dashboard"
|
||||
AUTOMATED="$ROOT/configs/airootfs/root/.automated_script.sh"
|
||||
ORCH="$ROOT/configs/airootfs/usr/share/omarchy-iso/orchestrator"
|
||||
OMARCHY_PATH_FOR_TEST="${OMARCHY_PATH_FOR_TEST:-$ROOT/../omarchy-installer}"
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
strip_ansi() {
|
||||
python - "$1" <<'PY'
|
||||
from pathlib import Path
|
||||
import re, sys
|
||||
s = Path(sys.argv[1]).read_text(errors="ignore")
|
||||
s = re.sub(r"\x1b\[[0-9;?]*[A-Za-z]", "", s)
|
||||
sys.stdout.write(s)
|
||||
PY
|
||||
}
|
||||
|
||||
assert_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if ! grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local file="$1" needle="$2" stripped
|
||||
stripped="$tmp/stripped.$(basename "$file")"
|
||||
strip_ansi "$file" >"$stripped"
|
||||
if grep -Fq "$needle" "$stripped"; then
|
||||
echo "Expected $file not to contain: $needle" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
write_state_py='import json, sys, time, pathlib
|
||||
path=pathlib.Path(sys.argv[1]); phase=sys.argv[2]
|
||||
state={"started_at":time.time(),"current_phase":phase,"phase_started_at":time.time(),"phases":[]}
|
||||
tmp=path.with_name(".state.json.tmp")
|
||||
tmp.write_text(json.dumps(state))
|
||||
tmp.replace(path)'
|
||||
|
||||
success_worker="$tmp/success-worker"
|
||||
cat >"$success_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"; log="\$2"
|
||||
mkdir -p "\$(dirname "\$state")" "\$(dirname "\$log")"
|
||||
python - "\$state" "Preparing live environment" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
python - "\$state" "Installing Arch + Omarchy" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.4
|
||||
python - "\$state" "Validating boot setup" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
cat >>"\$log" <<'LOG'
|
||||
=== Installation Time Summary ===
|
||||
Omarchy: 1m 40s
|
||||
=================================
|
||||
LOG
|
||||
EOF
|
||||
chmod +x "$success_worker"
|
||||
|
||||
fail_worker="$tmp/fail-worker"
|
||||
cat >"$fail_worker" <<EOF
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
state="\$1"
|
||||
mkdir -p "\$(dirname "\$state")"
|
||||
python - "\$state" "Installing Arch + Omarchy" <<'PY'
|
||||
$write_state_py
|
||||
PY
|
||||
sleep 0.2
|
||||
exit 7
|
||||
EOF
|
||||
chmod +x "$fail_worker"
|
||||
|
||||
success_state="$tmp/success-state.json"
|
||||
success_log="$tmp/success.log"
|
||||
success_capture="$tmp/success.capture"
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$success_log' '$success_state' -- '$success_worker' '$success_state' '$success_log'" \
|
||||
"$success_capture" >/dev/null
|
||||
assert_contains "$success_capture" "Installing Omarchy"
|
||||
assert_contains "$success_capture" "Installed in 1m 40s"
|
||||
|
||||
fail_state="$tmp/fail-state.json"
|
||||
fail_log="$tmp/fail.log"
|
||||
fail_capture="$tmp/fail.capture"
|
||||
set +e
|
||||
OMARCHY_UI_AUTO_REBOOT=no OMARCHY_PATH="$OMARCHY_PATH_FOR_TEST" script -qfec \
|
||||
"$DASHBOARD '$fail_log' '$fail_state' -- '$fail_worker' '$fail_state'" \
|
||||
"$fail_capture" >/dev/null
|
||||
fail_status=$?
|
||||
set -e
|
||||
if [[ $fail_status -ne 7 ]]; then
|
||||
echo "Expected failure worker status 7, got $fail_status" >&2
|
||||
exit 1
|
||||
fi
|
||||
assert_not_contains "$fail_capture" "Installed in"
|
||||
assert_not_contains "$fail_capture" "Finished installing"
|
||||
|
||||
bash -n "$DASHBOARD" "$AUTOMATED"
|
||||
python -m py_compile "$ORCH"/*.py
|
||||
git -C "$ROOT" diff --check
|
||||
|
||||
if rg -n 'dashboard\.pid|dashboard\.stop|OMARCHY_INSTALL_DASHBOARD|_stop_install_dashboard|_dashboard_|killpg|current_phase.*Finishing|confirm\(|Reboot now' \
|
||||
"$AUTOMATED" "$DASHBOARD" "$ORCH"; then
|
||||
echo "Forbidden legacy dashboard lifecycle pattern found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! grep -Fq '/usr/local/bin/omarchy-install-dashboard \' "$AUTOMATED"; then
|
||||
echo "Expected .automated_script.sh to call dashboard in foreground" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -Eq 'omarchy-install-dashboard .*&' "$AUTOMATED"; then
|
||||
echo "Dashboard must not be launched in background" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "installer UI flow smoke test passed"
|
||||
@@ -43,85 +43,18 @@ export FORCE_COLOR=1
|
||||
cd /root
|
||||
./configurator
|
||||
|
||||
# Keep the actual install screen calm: the dashboard owns /dev/tty while the
|
||||
# noisy installer stream is captured to the support log.
|
||||
dashboard_tty=$(tty)
|
||||
export OMARCHY_DASHBOARD_TTY="$dashboard_tty"
|
||||
rm -f /run/omarchy-install/dashboard.stop /run/omarchy-install/dashboard.pid /run/omarchy-install/state.json
|
||||
setsid /usr/local/bin/omarchy-install-dashboard "$OMARCHY_INSTALL_LOG_FILE" /run/omarchy-install/state.json <"$dashboard_tty" >"$dashboard_tty" 2>&1 &
|
||||
dashboard_pid=$!
|
||||
# setsid may fork/exec differently across environments. The dashboard writes
|
||||
# its own real PID as soon as it starts; prefer that for shutdown.
|
||||
for _ in {1..20}; do
|
||||
[[ -s /run/omarchy-install/dashboard.pid ]] && break
|
||||
sleep 0.05
|
||||
done
|
||||
if [[ -s /run/omarchy-install/dashboard.pid ]]; then
|
||||
dashboard_pid=$(cat /run/omarchy-install/dashboard.pid)
|
||||
fi
|
||||
export OMARCHY_INSTALL_DASHBOARD_PID="$dashboard_pid"
|
||||
|
||||
stop_install_dashboard() {
|
||||
touch /run/omarchy-install/dashboard.stop 2>/dev/null || true
|
||||
if [[ -s /run/omarchy-install/dashboard.pid ]]; then
|
||||
dashboard_pid=$(cat /run/omarchy-install/dashboard.pid)
|
||||
fi
|
||||
if [[ -n ${dashboard_pid:-} ]]; then
|
||||
if kill -0 "$dashboard_pid" 2>/dev/null; then
|
||||
# The dashboard runs under setsid, so $dashboard_pid is also its process
|
||||
# group. Kill the group first so child sleep/tte processes cannot keep
|
||||
# the dashboard alive over the final/error UI.
|
||||
kill -- -"$dashboard_pid" 2>/dev/null || true
|
||||
kill "$dashboard_pid" 2>/dev/null || true
|
||||
for _ in {1..20}; do
|
||||
kill -0 "$dashboard_pid" 2>/dev/null || break
|
||||
sleep 0.05
|
||||
done
|
||||
if kill -0 "$dashboard_pid" 2>/dev/null; then
|
||||
kill -9 -- -"$dashboard_pid" 2>/dev/null || true
|
||||
kill -9 "$dashboard_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
wait "$dashboard_pid" 2>/dev/null || true
|
||||
unset dashboard_pid
|
||||
fi
|
||||
printf "\033[?25h" >/dev/tty
|
||||
}
|
||||
dashboard_error_handler() {
|
||||
[[ $ERROR_HANDLING == "true" ]] && return
|
||||
local exit_code=$?
|
||||
stop_install_dashboard
|
||||
catch_errors "$exit_code"
|
||||
}
|
||||
|
||||
dashboard_exit_handler() {
|
||||
local exit_code=$?
|
||||
stop_install_dashboard
|
||||
if (( exit_code != 0 )) && [[ $ERROR_HANDLING != "true" ]]; then
|
||||
catch_errors "$exit_code"
|
||||
else
|
||||
show_cursor
|
||||
fi
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
trap dashboard_error_handler ERR
|
||||
trap dashboard_exit_handler EXIT
|
||||
trap 'stop_install_dashboard; catch_errors 130' INT
|
||||
trap 'stop_install_dashboard; catch_errors 143' TERM
|
||||
|
||||
# Absolute paths because omarchy-iso-install cd's into /usr/share/omarchy-iso
|
||||
# before exec'ing python; relative paths would resolve against the wrong dir.
|
||||
set +e
|
||||
/usr/local/bin/omarchy-iso-install \
|
||||
--config /root/user_configuration.json \
|
||||
--creds /root/user_credentials.json \
|
||||
--full-name-file /root/user_full_name.txt \
|
||||
--email-file /root/user_email_address.txt \
|
||||
--encrypt-file /root/user_encrypt_installation.txt \
|
||||
> >(sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>"$OMARCHY_INSTALL_LOG_FILE") 2>&1
|
||||
install_status=$?
|
||||
set -e
|
||||
|
||||
stop_install_dashboard
|
||||
exit "$install_status"
|
||||
# The foreground dashboard is now the sole visible install UI owner. It starts
|
||||
# the actual installer as a non-interactive child, logs child output, waits for
|
||||
# completion, then renders the final installed-time/reboot prompt itself.
|
||||
export OMARCHY_DASHBOARD_TTY="$(tty)"
|
||||
rm -f /run/omarchy-install/state.json
|
||||
/usr/local/bin/omarchy-install-dashboard \
|
||||
"$OMARCHY_INSTALL_LOG_FILE" \
|
||||
/run/omarchy-install/state.json \
|
||||
-- \
|
||||
/usr/local/bin/omarchy-iso-install \
|
||||
--config /root/user_configuration.json \
|
||||
--creds /root/user_credentials.json \
|
||||
--full-name-file /root/user_full_name.txt \
|
||||
--email-file /root/user_email_address.txt \
|
||||
--encrypt-file /root/user_encrypt_installation.txt
|
||||
@@ -1,17 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Render the live Omarchy install dashboard on tty1 while installer output is
|
||||
# captured to logs. This is presentation only: it must never read input or gate
|
||||
# install completion.
|
||||
# Foreground Omarchy ISO install UI supervisor.
|
||||
#
|
||||
# This process is the only owner of the visible install UI. It launches the
|
||||
# actual installer as a non-interactive child, sends child output to the support
|
||||
# log, renders progress while the child runs, then renders the final installed
|
||||
# time + reboot prompt itself. There is deliberately no background dashboard.
|
||||
set -euo pipefail
|
||||
|
||||
LOG_FILE="${1:-/var/log/omarchy-install.log}"
|
||||
STATE_FILE="${2:-/run/omarchy-install/state.json}"
|
||||
STATE_DIR="$(dirname "$STATE_FILE")"
|
||||
STOP_FILE="$STATE_DIR/dashboard.stop"
|
||||
PID_FILE="$STATE_DIR/dashboard.pid"
|
||||
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
|
||||
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
|
||||
usage() {
|
||||
echo "Usage: $0 <log-file> <state-file> -- <installer command...>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
(( $# >= 4 )) || usage
|
||||
LOG_FILE="$1"
|
||||
STATE_FILE="$2"
|
||||
shift 2
|
||||
[[ ${1:-} == "--" ]] || usage
|
||||
shift
|
||||
(( $# > 0 )) || usage
|
||||
INSTALL_CMD=("$@")
|
||||
|
||||
TTY_PATH="${OMARCHY_DASHBOARD_TTY:-/dev/tty}"
|
||||
LOGO_PATH="${OMARCHY_PATH:-/usr/share/omarchy}/logo.txt"
|
||||
CSI=$'\033['
|
||||
RESET="${CSI}0m"
|
||||
DIM="${CSI}2m"
|
||||
@@ -20,11 +31,12 @@ SHOW_CURSOR="${CSI}?25h"
|
||||
CLEAR="${CSI}2J${CSI}H"
|
||||
CLEAR_LINE="${CSI}2K"
|
||||
CLEAR_TO_END="${CSI}J"
|
||||
# Use the 16-color ANSI palette configured by .automated_script.sh. The Linux
|
||||
# VT does not reliably map 256-color indexes the same way terminal emulators do.
|
||||
GREEN="${CSI}32m"
|
||||
DARK="${CSI}90m"
|
||||
|
||||
child_pid=""
|
||||
child_pgid=""
|
||||
|
||||
whimsy=(
|
||||
"making Linux feel like home"
|
||||
"installing the good defaults"
|
||||
@@ -46,10 +58,37 @@ whimsy=(
|
||||
|
||||
cleanup() {
|
||||
printf '%s%s' "$RESET" "$SHOW_CURSOR" >"$TTY_PATH" 2>/dev/null || true
|
||||
rm -f "$PID_FILE" 2>/dev/null || true
|
||||
}
|
||||
|
||||
stop_child() {
|
||||
[[ -n ${child_pid:-} ]] || return 0
|
||||
if kill -0 "$child_pid" 2>/dev/null; then
|
||||
if [[ -n ${child_pgid:-} ]]; then
|
||||
kill -- -"$child_pgid" 2>/dev/null || true
|
||||
fi
|
||||
kill "$child_pid" 2>/dev/null || true
|
||||
for _ in {1..20}; do
|
||||
kill -0 "$child_pid" 2>/dev/null || break
|
||||
sleep 0.05
|
||||
done
|
||||
if kill -0 "$child_pid" 2>/dev/null; then
|
||||
[[ -n ${child_pgid:-} ]] && kill -9 -- -"$child_pgid" 2>/dev/null || true
|
||||
kill -9 "$child_pid" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
wait "$child_pid" 2>/dev/null || true
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
local code="$1"
|
||||
stop_child
|
||||
cleanup
|
||||
exit "$code"
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
trap 'cleanup; exit 0' INT TERM
|
||||
trap 'interrupt 130' INT
|
||||
trap 'interrupt 143' TERM
|
||||
|
||||
term_size() { stty size 2>/dev/null <"$TTY_PATH" || printf '24 80\n'; }
|
||||
term_cols() { local size; size="$(term_size)"; printf '%s\n' "${size#* }"; }
|
||||
@@ -150,8 +189,6 @@ phase = state.get('current_phase') or 'Starting installation'
|
||||
if state.get('finished_at') or phase == 'Installation complete':
|
||||
print(100, end='')
|
||||
raise SystemExit
|
||||
# Internal weighting only. Keep estimates conservative but close to observed
|
||||
# ISO installs so the bar does not sit at 12% then jump to 90%.
|
||||
weights = [
|
||||
('Starting installation', 1, 2, 2),
|
||||
('Preparing live environment', 2, 6, 5),
|
||||
@@ -163,7 +200,6 @@ weights = [
|
||||
('Configuring login', 98, 99, 3),
|
||||
('Validating protected boot setup', 99, 100, 5),
|
||||
('Validating boot setup', 99, 100, 5),
|
||||
('Finishing', 99, 100, 3),
|
||||
]
|
||||
start, end, expected = 1, 99, 90
|
||||
for name, s, e, seconds in weights:
|
||||
@@ -178,22 +214,6 @@ print(min(100, start + active), end='')
|
||||
PY
|
||||
}
|
||||
|
||||
dashboard_should_stop() {
|
||||
[[ -f $STATE_FILE ]] || return 1
|
||||
python - "$STATE_FILE" <<'PY' 2>/dev/null
|
||||
import json, sys
|
||||
try:
|
||||
state = json.load(open(sys.argv[1]))
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
phase = state.get('current_phase')
|
||||
# The final installed-time/reboot prompt is rendered inside the Finishing
|
||||
# phase. Stop before drawing another dashboard frame so the prompt cannot be
|
||||
# covered while it waits for input.
|
||||
raise SystemExit(0 if state.get('finished_at') or phase in ('Finishing', 'Installation complete') else 1)
|
||||
PY
|
||||
}
|
||||
|
||||
current_whimsy() {
|
||||
local frame="$1" pct="$2"
|
||||
printf '%s' "${whimsy[$(( (pct / 8 + frame / 8) % ${#whimsy[@]} ))]}"
|
||||
@@ -204,11 +224,10 @@ render_static() {
|
||||
blank_line
|
||||
|
||||
if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then
|
||||
if timeout 8s tte -i "$LOGO_PATH" --canvas-width 0 --anchor-text c --frame-rate 920 laseretch 2>/dev/null; then
|
||||
if timeout 8s tte -i "$LOGO_PATH" --canvas-width 0 --anchor-text c --frame-rate 920 laseretch >"$TTY_PATH" 2>/dev/null; then
|
||||
blank_line
|
||||
return
|
||||
fi
|
||||
# If animation fails or times out, clear any partial frame before fallback.
|
||||
printf '%s' "$CLEAR"
|
||||
blank_line
|
||||
fi
|
||||
@@ -239,22 +258,92 @@ render_dynamic() {
|
||||
printf '%s' "$CLEAR_TO_END"
|
||||
}
|
||||
|
||||
if [[ ! -e $TTY_PATH ]]; then
|
||||
exit 0
|
||||
fi
|
||||
install_duration() {
|
||||
local path line duration
|
||||
for path in /mnt/var/log/omarchy-install.log "$LOG_FILE"; do
|
||||
[[ -f $path ]] || continue
|
||||
for prefix in Total Omarchy; do
|
||||
line="$(grep "^$prefix:" "$path" 2>/dev/null | tail -n 1 || true)"
|
||||
[[ -n $line ]] || continue
|
||||
duration="${line#*:}"
|
||||
duration="${duration#${duration%%[![:space:]]*}}"
|
||||
[[ -n $duration ]] && { printf '%s' "$duration"; return; }
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
mkdir -p "$STATE_DIR"
|
||||
echo "$$" >"$PID_FILE"
|
||||
render_finish() {
|
||||
local duration message cols
|
||||
cols="$(term_cols)"
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH"
|
||||
if command -v tte >/dev/null 2>&1 && command -v timeout >/dev/null 2>&1 && [[ -f $LOGO_PATH ]]; then
|
||||
timeout 8s tte -i "$LOGO_PATH" --canvas-width 0 --anchor-text c --frame-rate 920 laseretch >"$TTY_PATH" 2>/dev/null || true
|
||||
else
|
||||
render_logo >"$TTY_PATH"
|
||||
fi
|
||||
duration="$(install_duration)"
|
||||
message="Finished installing"
|
||||
[[ -n $duration ]] && message="Installed in $duration"
|
||||
{
|
||||
blank_line
|
||||
center "$message" "$cols"
|
||||
blank_line
|
||||
} >"$TTY_PATH"
|
||||
}
|
||||
|
||||
reboot_prompt() {
|
||||
local prompt_pad
|
||||
prompt_pad=$(( ( $(term_cols) - 18 ) / 2 ))
|
||||
(( prompt_pad < 0 )) && prompt_pad=0
|
||||
gum confirm \
|
||||
--padding "0 0 0 $prompt_pad" \
|
||||
--show-help=false \
|
||||
--default \
|
||||
--affirmative "Reboot Now" \
|
||||
--negative "" \
|
||||
"" <"$TTY_PATH" >"$TTY_PATH" 2>&1
|
||||
}
|
||||
|
||||
launch_child() {
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
# Child is non-interactive. All visible UI belongs to this supervisor.
|
||||
setsid "${INSTALL_CMD[@]}" </dev/null > >(sed -u 's/\x1b\[[0-9;]*[a-zA-Z]//g' >>"$LOG_FILE") 2>&1 &
|
||||
child_pid=$!
|
||||
child_pgid=$child_pid
|
||||
}
|
||||
|
||||
[[ -e $TTY_PATH ]] || exit 2
|
||||
printf '%s' "$HIDE_CURSOR" >"$TTY_PATH"
|
||||
render_static >"$TTY_PATH"
|
||||
launch_child
|
||||
|
||||
frame=0
|
||||
while true; do
|
||||
[[ -e $STOP_FILE ]] && break
|
||||
dashboard_should_stop && break
|
||||
while jobs -pr | grep -qx "$child_pid"; do
|
||||
render_dynamic "$frame" >"$TTY_PATH"
|
||||
[[ -e $STOP_FILE ]] && break
|
||||
dashboard_should_stop && break
|
||||
frame=$((frame + 1))
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
set +e
|
||||
wait "$child_pid"
|
||||
status=$?
|
||||
set -e
|
||||
child_pid=""
|
||||
child_pgid=""
|
||||
|
||||
if (( status != 0 )); then
|
||||
printf '%s%s' "$SHOW_CURSOR" "$CLEAR" >"$TTY_PATH"
|
||||
exit "$status"
|
||||
fi
|
||||
|
||||
render_finish
|
||||
if [[ ${OMARCHY_UI_AUTO_REBOOT:-} == "no" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if reboot_prompt; then
|
||||
printf '%s' "$CLEAR" >"$TTY_PATH"
|
||||
reboot 2>/dev/null || systemctl reboot 2>/dev/null || true
|
||||
fi
|
||||
|
||||
exit 0
|
||||
@@ -38,7 +38,6 @@ def build_phases(ctx: InstallContext):
|
||||
configure_login,
|
||||
validate_boot_full,
|
||||
validate_boot_protected,
|
||||
finish,
|
||||
)
|
||||
|
||||
if ctx.is_protected:
|
||||
@@ -50,7 +49,6 @@ def build_phases(ctx: InstallContext):
|
||||
("Finalizing in chroot", run_chroot_finalizer),
|
||||
("Configuring login", configure_login),
|
||||
("Validating protected boot setup", validate_boot_protected),
|
||||
("Finishing", finish),
|
||||
]
|
||||
|
||||
return [
|
||||
@@ -59,7 +57,6 @@ def build_phases(ctx: InstallContext):
|
||||
("Finalizing in chroot", run_chroot_finalizer),
|
||||
("Configuring login", configure_login),
|
||||
("Validating boot setup", validate_boot_full),
|
||||
("Finishing", finish),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Phase ordering (full-disk):
|
||||
run_chroot_finalizer → bind mounts + sudoers shim + arch-chroot finalize.sh
|
||||
configure_login → sddm autologin for unencrypted installs
|
||||
validate_boot_full → assert UKI / limine.conf / kernel cmdline are sane
|
||||
finish → reboot prompt
|
||||
|
||||
Phase ordering (protected / pre-mounted):
|
||||
|
||||
@@ -26,13 +25,11 @@ Phase ordering (protected / pre-mounted):
|
||||
run_chroot_finalizer → same
|
||||
configure_login → same
|
||||
validate_boot_protected → implemented in Step 8
|
||||
finish → same
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
@@ -42,7 +39,7 @@ from pathlib import Path
|
||||
|
||||
from . import archinstall_adapter as arch
|
||||
from .context import InstallContext
|
||||
from .ui import confirm, info
|
||||
from .ui import info
|
||||
|
||||
|
||||
# Packages installed BEFORE useradd. omarchy-settings and omarchy-nvim
|
||||
@@ -1045,215 +1042,3 @@ def cleanup_protected_state(ctx: InstallContext) -> None:
|
||||
check=False,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# finish: prompt for reboot. Bind mounts are unwound in main()'s finally.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _dashboard_stopped(pid: int) -> bool:
|
||||
stat = Path(f"/proc/{pid}/stat")
|
||||
if not stat.exists():
|
||||
return True
|
||||
try:
|
||||
# /proc/<pid>/stat field 3 is process state. A zombie cannot write to
|
||||
# the tty anymore; the shell parent will reap it when control returns.
|
||||
return stat.read_text().split()[2] == "Z"
|
||||
except OSError:
|
||||
return True
|
||||
|
||||
|
||||
def _pid_is_dashboard(pid: int) -> bool:
|
||||
try:
|
||||
cmdline = Path(f"/proc/{pid}/cmdline").read_bytes().replace(b"\0", b" ")
|
||||
except OSError:
|
||||
return False
|
||||
return b"omarchy-install-dashboard" in cmdline
|
||||
|
||||
|
||||
def _add_dashboard_pid(pids: list[int], pid_text: str | None) -> None:
|
||||
if not pid_text:
|
||||
return
|
||||
try:
|
||||
pid = int(pid_text.strip())
|
||||
except ValueError:
|
||||
return
|
||||
if pid not in pids and _pid_is_dashboard(pid):
|
||||
pids.append(pid)
|
||||
|
||||
|
||||
def _dashboard_pids() -> list[int]:
|
||||
pids: list[int] = []
|
||||
|
||||
_add_dashboard_pid(pids, os.environ.get("OMARCHY_INSTALL_DASHBOARD_PID"))
|
||||
|
||||
pid_file = Path("/run/omarchy-install/dashboard.pid")
|
||||
try:
|
||||
_add_dashboard_pid(pids, pid_file.read_text())
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Last-resort /proc scan. Do not rely on pgrep being present in the ISO.
|
||||
for proc_dir in Path("/proc").iterdir():
|
||||
if not proc_dir.name.isdigit():
|
||||
continue
|
||||
_add_dashboard_pid(pids, proc_dir.name)
|
||||
|
||||
return pids
|
||||
|
||||
|
||||
def _signal_dashboard(pid: int, signal_number: int) -> None:
|
||||
# Dashboard is launched with setsid, making its PID the process group ID.
|
||||
# Signal the group first so child sleep/tte processes cannot keep drawing.
|
||||
try:
|
||||
os.killpg(pid, signal_number)
|
||||
except OSError:
|
||||
pass
|
||||
try:
|
||||
os.kill(pid, signal_number)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _stop_install_dashboard() -> None:
|
||||
stop_file = Path("/run/omarchy-install/dashboard.stop")
|
||||
try:
|
||||
stop_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
stop_file.touch()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
pids = _dashboard_pids()
|
||||
for signal_number in (15, 9):
|
||||
for pid in pids:
|
||||
if _dashboard_stopped(pid):
|
||||
continue
|
||||
if not _pid_is_dashboard(pid):
|
||||
continue
|
||||
_signal_dashboard(pid, signal_number)
|
||||
deadline = time.time() + (1.0 if signal_number == 15 else 0.5)
|
||||
while time.time() < deadline:
|
||||
if all(_dashboard_stopped(pid) for pid in pids):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
if all(_dashboard_stopped(pid) for pid in pids):
|
||||
break
|
||||
|
||||
try:
|
||||
with open("/dev/tty", "w", encoding="utf-8") as tty:
|
||||
tty.write("\033[?25h\033[H\033[2J")
|
||||
tty.flush()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _tty_size() -> tuple[int, int]:
|
||||
try:
|
||||
with open("/dev/tty", "rb") as tty:
|
||||
res = subprocess.run(
|
||||
["stty", "size"],
|
||||
stdin=tty,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
rows, cols = res.stdout.strip().split()
|
||||
return int(rows), int(cols)
|
||||
except Exception: # noqa: BLE001
|
||||
return 24, 80
|
||||
|
||||
|
||||
def _center_text(text: str, width: int) -> str:
|
||||
pad = max((width - len(text)) // 2, 0)
|
||||
return " " * pad + text
|
||||
|
||||
|
||||
def _install_duration(ctx: InstallContext) -> str | None:
|
||||
candidates = [
|
||||
ctx.target / "var" / "log" / "omarchy-install.log",
|
||||
Path("/var/log/omarchy-install.log"),
|
||||
]
|
||||
patterns = ("Total:", "Omarchy:")
|
||||
for path in candidates:
|
||||
if not path.exists():
|
||||
continue
|
||||
try:
|
||||
lines = path.read_text(errors="ignore").splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
for prefix in patterns:
|
||||
for line in reversed(lines[-80:]):
|
||||
if line.startswith(prefix):
|
||||
duration = line.split(":", 1)[1].strip()
|
||||
if duration:
|
||||
return duration
|
||||
return None
|
||||
|
||||
|
||||
def _render_tte_logo(logo_path: Path) -> bool:
|
||||
if not logo_path.exists() or not shutil.which("tte"):
|
||||
return False
|
||||
try:
|
||||
with open("/dev/tty", "rb", buffering=0) as stdin, open("/dev/tty", "wb", buffering=0) as stdout:
|
||||
subprocess.run(
|
||||
[
|
||||
"tte",
|
||||
"-i", str(logo_path),
|
||||
"--canvas-width", "0",
|
||||
"--anchor-text", "c",
|
||||
"--frame-rate", "920",
|
||||
"laseretch",
|
||||
],
|
||||
stdin=stdin,
|
||||
stdout=stdout,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=8,
|
||||
check=True,
|
||||
)
|
||||
return True
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return False
|
||||
|
||||
|
||||
def _render_static_finish_logo(tty, logo_path: Path, cols: int) -> None:
|
||||
green = "\033[32m"
|
||||
reset = "\033[0m"
|
||||
if logo_path.exists():
|
||||
logo_lines = logo_path.read_text(errors="ignore").splitlines()
|
||||
logo_width = max((len(line) for line in logo_lines), default=0)
|
||||
left = max((cols - logo_width) // 2, 0)
|
||||
for line in logo_lines:
|
||||
tty.write(" " * left + green + line + reset + "\n")
|
||||
else:
|
||||
tty.write(_center_text("Omarchy", cols) + "\n")
|
||||
|
||||
|
||||
def _render_finish_screen(ctx: InstallContext) -> None:
|
||||
_, cols = _tty_size()
|
||||
logo_path = Path("/usr/share/omarchy/logo.txt")
|
||||
try:
|
||||
with open("/dev/tty", "w", encoding="utf-8") as tty:
|
||||
tty.write("\033[?25h\033[H\033[2J\n")
|
||||
tty.flush()
|
||||
|
||||
if not _render_tte_logo(logo_path):
|
||||
with open("/dev/tty", "w", encoding="utf-8") as tty:
|
||||
tty.write("\033[?25h\033[H\033[2J\n")
|
||||
_render_static_finish_logo(tty, logo_path, cols)
|
||||
tty.flush()
|
||||
|
||||
with open("/dev/tty", "w", encoding="utf-8") as tty:
|
||||
tty.write("\n")
|
||||
duration = _install_duration(ctx)
|
||||
message = f"Installed in {duration}" if duration else "Finished installing"
|
||||
tty.write(_center_text(message, cols) + "\n\n")
|
||||
tty.flush()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def finish(ctx: InstallContext) -> None:
|
||||
_stop_install_dashboard()
|
||||
_render_finish_screen(ctx)
|
||||
if confirm("Reboot now?", default=True):
|
||||
os.system("reboot")
|
||||
@@ -16,12 +16,6 @@ def style(text: str, *, foreground: str | None = None, padding: str | None = Non
|
||||
subprocess.run(cmd, check=False)
|
||||
|
||||
|
||||
def confirm(prompt: str, *, default: bool = True) -> bool:
|
||||
cmd = ["gum", "confirm", "--default" if default else "--no-default", prompt]
|
||||
with open("/dev/tty", "rb") as stdin, open("/dev/tty", "wb") as stdout:
|
||||
return subprocess.run(cmd, stdin=stdin, stdout=stdout, stderr=stdout).returncode == 0
|
||||
|
||||
|
||||
def info(text: str) -> None:
|
||||
style(text, foreground="3", padding="1 0 0 4")
|
||||
|
||||
|
||||
Reference in new issue
Block a user