added sas to both images. A few minor updates to image generation for pi images.

This commit is contained in:
Storm Dragon
2026-06-04 23:52:44 -04:00
parent c94a71e0d6
commit f6e2e2f4c8
10 changed files with 431 additions and 1242 deletions
@@ -1,57 +0,0 @@
# XLibre Updater Design
## Goal
Add a repository-managed workflow for updating and building the Stormux XLibre package set in the correct dependency order, while keeping `xlibre-video-dummy-with-vt` manually maintained and reviewed against upstream `xlibre-video-dummy`.
## Scope
This design covers:
- correcting the package build order in `scripts/upgrade-xlibre.sh`
- reviewing and updating `scripts/xlibre-video-dummy-with-vt/PKGBUILD`
- regenerating `.SRCINFO` after PKGBUILD changes
This design does not automate AUR publication or rewrite the downstream package from upstream sources.
## Current State
The updater script currently builds packages in an order that does not match the XLibre dependency chain. `xlibre-video-dummy-with-vt` is also version-skewed relative to the current AUR `xlibre-video-dummy` package and needs a manual rebase of relevant packaging changes.
## Dependency Order
The package build order will be:
1. `xlibre-xserver-common`
2. `xlibre-xserver-devel`
3. `xlibre-input-libinput`
4. `xlibre-xserver`
5. `xlibre-video-fbdev`
6. `xlibre-video-dummy-with-vt`
This order reflects current AUR dependencies: `xlibre-xserver` requires `xlibre-xserver-common` and `xlibre-input-libinput`, while the input and video driver packages require `xlibre-xserver-devel` to build.
## Downstream Package Policy
`xlibre-video-dummy-with-vt` remains a separate, manually maintained package because it has a different maintainer and should not be auto-derived from the upstream AUR package.
The maintenance rule is:
- treat AUR `xlibre-video-dummy` as the packaging baseline
- manually port relevant upstream PKGBUILD changes into `xlibre-video-dummy-with-vt`
- keep only the intentional downstream delta needed for VT behavior
## Implementation Shape
`scripts/upgrade-xlibre.sh` will continue cloning packages from AUR and building them locally, but with the corrected order.
`scripts/xlibre-video-dummy-with-vt/PKGBUILD` will be updated to match the current upstream package structure where appropriate:
- current version and release
- current `depends` and `makedepends`
- current build flags and source layout
- regenerated checksums
The VT-specific patch remains the only behavioral divergence.
## Error Handling
The script should fail fast on clone or build errors and stop rather than continuing with a broken package chain. Because package order is intentional, partial success should be considered incomplete.
## Verification
Verification will be task-focused:
- confirm the updater script contains the corrected package order
- compare the downstream PKGBUILD against current upstream `xlibre-video-dummy`
- regenerate and verify `.SRCINFO`
- run `shellcheck` on the updater script
- run `makepkg --printsrcinfo` in the downstream package directory
+34 -17
View File
@@ -24,13 +24,6 @@ mounted=1
set -e # Don't want to destroy stuff if this goes majorly wrong. set -e # Don't want to destroy stuff if this goes majorly wrong.
trap cleanup EXIT # make sure the script cleans up after itself before closing. trap cleanup EXIT # make sure the script cleans up after itself before closing.
# shellcheck disable=SC2329 # verify_image is invoked from cleanup, which is invoked via trap EXIT
verify_image() {
echo "Checking completed image filesystems..."
fsck.vfat -n "${loopdev}p1"
e2fsck -fn "${loopdev}p2"
}
# shellcheck disable=SC2329 # compress_image is invoked from finish_build # shellcheck disable=SC2329 # compress_image is invoked from finish_build
compress_image() { compress_image() {
local compressedImage="${imageName}.xz" local compressedImage="${imageName}.xz"
@@ -50,16 +43,11 @@ compress_image() {
# shellcheck disable=SC2329 # cleanup_image is invoked from cleanup and finish_build # shellcheck disable=SC2329 # cleanup_image is invoked from cleanup and finish_build
cleanup_image() { cleanup_image() {
local verifyFilesystems="${1:-false}"
local cleanupStatus=0 local cleanupStatus=0
if [[ $mounted -eq 0 ]]; then if [[ $mounted -eq 0 ]]; then
if ! umount -R /mnt; then if ! umount -R /mnt; then
cleanupStatus=1 cleanupStatus=1
elif [[ "$verifyFilesystems" == true ]]; then
if ! verify_image; then
cleanupStatus=1
fi
fi fi
fi fi
@@ -85,12 +73,12 @@ cleanup() {
local status=$? # capture original exit status so failures propagate local status=$? # capture original exit status so failures propagate
local cleanupStatus=0 local cleanupStatus=0
if ! cleanup_image false; then if ! cleanup_image; then
cleanupStatus=1 cleanupStatus=1
fi fi
if [[ $status -eq 0 && $cleanupStatus -ne 0 ]]; then if [[ $status -eq 0 && $cleanupStatus -ne 0 ]]; then
echo "Image build commands completed, but cleanup or filesystem verification failed." echo "Image build commands completed, but cleanup failed."
status=1 status=1
fi fi
@@ -100,8 +88,8 @@ cleanup() {
finish_build() { finish_build() {
trap - EXIT trap - EXIT
if ! cleanup_image true; then if ! cleanup_image; then
echo "Image build commands completed, but cleanup or filesystem verification failed." echo "Image build commands completed, but cleanup failed."
exit 1 exit 1
fi fi
@@ -112,6 +100,30 @@ finish_build() {
exit 0 exit 0
} }
# shellcheck disable=SC2329 # install_sas is emitted into the chroot heredoc with declare -f
install_sas() {
local sasRepo="https://git.stormux.org/storm/sas"
local sasPath="/usr/local/bin/sas"
local tempDir
local installStatus=0
tempDir="$(mktemp -d)"
echo "Installing latest sas..."
if ! git clone --depth 1 "$sasRepo" "$tempDir"; then
rm -rf "$tempDir"
return 1
fi
rm -f "$sasPath"
if ! install -m 755 "$tempDir/sas.py" "$sasPath"; then
installStatus=1
fi
rm -rf "$tempDir"
return "$installStatus"
}
help() { help() {
echo -e "Usage:\n" echo -e "Usage:\n"
echo "With no arguments, build with default parameters." echo "With no arguments, build with default parameters."
@@ -196,7 +208,7 @@ for i in arch-install-scripts dosfstools parted ; do
exit 1 exit 1
fi fi
done done
for i in e2fsck fsck.vfat sha1sum xz ; do for i in sha1sum xz ; do
if ! command -v "$i" &> /dev/null ; then if ! command -v "$i" &> /dev/null ; then
echo "Please install ${i} before continuing." echo "Please install ${i} before continuing."
exit 1 exit 1
@@ -330,6 +342,7 @@ packages=(
bluez-utils bluez-utils
brltty brltty
cronie cronie
curl
dialog dialog
espeak-ng espeak-ng
fake-hwclock fake-hwclock
@@ -370,6 +383,7 @@ packages=(
rsync rsync
screen screen
sox sox
speech-dispatcher
w3m-git w3m-git
wget wget
wireless-regdb wireless-regdb
@@ -382,6 +396,9 @@ packages=(
pacman -Su --needed --noconfirm "\${packages[@]}" pacman -Su --needed --noconfirm "\${packages[@]}"
$(declare -f install_sas)
install_sas
# Fix mkinitcpio preset for linux-rpi - kms hook fails on aarch64 # Fix mkinitcpio preset for linux-rpi - kms hook fails on aarch64
# See: https://archlinuxarm.org/forum/viewtopic.php?t=16672 # See: https://archlinuxarm.org/forum/viewtopic.php?t=16672
if [[ -f /etc/mkinitcpio.d/linux-rpi.preset ]]; then if [[ -f /etc/mkinitcpio.d/linux-rpi.preset ]]; then
-582
View File
@@ -1,582 +0,0 @@
#!/usr/bin/env python3
import os
import re
import secrets
import signal
import string
import subprocess
import sys
import tempfile
import time
import pwd
import threading
stormuxAdmin = ("storm",)
ircServer = "irc.stormux.org"
ircPort = 6667
ircChannel = "#stormux"
remoteHost = "billysballoons.com"
sasUser = "sas"
pingIntervalSeconds = 180
pingCount = 5
maxWormholeFailures = 3
sudoKeepaliveThread = None
sudoKeepaliveStop = threading.Event()
def speak_message(message):
try:
subprocess.run(["spd-say", message], check=False)
except FileNotFoundError:
print(message, flush=True)
def say_or_print(message, useSpeech):
if useSpeech:
speak_message(message)
else:
print(message, flush=True)
def run_command(command, inputText=None, check=False, env=None):
return subprocess.run(
command,
input=inputText,
text=True,
capture_output=True,
check=check,
env=env,
)
def ensure_sudo(useSpeech):
if os.geteuid() == 0:
return True
if useSpeech:
speak_message("Sudo password required. Please enter your password now.")
result = run_command(["sudo", "-v"])
if result.returncode == 0:
start_sudo_keepalive()
return True
return False
def start_sudo_keepalive():
global sudoKeepaliveThread
if sudoKeepaliveThread and sudoKeepaliveThread.is_alive():
return
def keepalive_loop():
while not sudoKeepaliveStop.wait(240):
run_command(["sudo", "-n", "-v"])
sudoKeepaliveThread = threading.Thread(target=keepalive_loop, daemon=True)
sudoKeepaliveThread.start()
def run_privileged(command, useSpeech, inputText=None, check=True):
if os.geteuid() == 0:
fullCommand = command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo"] + command
return run_command(fullCommand, inputText=inputText, check=check)
def run_as_user(userName, command, useSpeech, check=True):
if os.geteuid() == 0:
fullCommand = ["sudo", "-u", userName, "-H"] + command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo", "-u", userName, "-H"] + command
return run_command(fullCommand, check=check)
def user_exists(userName):
result = run_command(["getent", "passwd", userName])
return result.returncode == 0
def ensure_wheel(userName, useSpeech):
result = run_command(["id", "-nG", userName])
groups = result.stdout.strip().split()
if "wheel" not in groups:
run_privileged(["usermod", "-a", "-G", "wheel", userName], useSpeech)
def generate_password():
allowedChars = string.ascii_letters + string.digits
length = secrets.randbelow(5) + 6
return "".join(secrets.choice(allowedChars) for _ in range(length))
def get_user_home(userName):
return pwd.getpwnam(userName).pw_dir
def set_password(userName, password, useSpeech):
run_privileged(["chpasswd"], useSpeech, inputText=f"{userName}:{password}\n")
def generate_ssh_key(userName, useSpeech):
userHome = get_user_home(userName)
sshDir = os.path.join(userHome, ".ssh")
privateKeyPath = os.path.join(sshDir, "id_ed25519")
publicKeyPath = f"{privateKeyPath}.pub"
run_privileged(["mkdir", "-p", sshDir], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", sshDir], useSpeech)
run_privileged(["chmod", "700", sshDir], useSpeech)
for entry in list_subdirs(sshDir):
if os.path.isfile(entry) or os.path.islink(entry):
run_privileged(["rm", "-f", entry], useSpeech, check=False)
run_as_user(
userName,
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", privateKeyPath],
useSpeech,
)
run_privileged(["chmod", "600", privateKeyPath], useSpeech)
run_privileged(["chmod", "644", publicKeyPath], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", privateKeyPath, publicKeyPath], useSpeech)
return privateKeyPath, publicKeyPath
def path_exists_for_user(path, userName, useSpeech):
result = run_as_user(userName, ["stat", path], useSpeech, check=False)
return result.returncode == 0
def extract_message_text(line):
if "> " in line:
return line.split("> ", 1)[1].strip()
if ": " in line:
return line.split(": ", 1)[1].strip()
return line.strip()
def parse_sender(line):
match = re.search(r"<([^>]+)>", line)
if match:
return match.group(1)
return None
def find_wormhole_code(message):
lowered = message.strip().lower()
if lowered in ("yes", "accept"):
return None
match = re.search(r"\b\d+-[A-Za-z0-9-]+\b", message)
if match:
return match.group(0)
return None
class IrcSession:
def __init__(self, server, port, nick, channel, baseDir):
self.server = server
self.port = port
self.nick = nick
self.channel = channel
self.baseDir = baseDir
self.serverDir = None
self.serverInPath = None
self.channelInPath = None
self.iiProcess = None
self.pmOffsets = {}
def start(self):
if not shutil_which("ii"):
raise RuntimeError("ii is not installed")
supportsI = ii_supports_i()
processEnv = os.environ.copy()
iiCommand = ["ii", "-s", self.server, "-p", str(self.port), "-n", self.nick]
if supportsI:
iiCommand += ["-i", self.baseDir]
else:
processEnv["HOME"] = self.baseDir
self.iiProcess = subprocess.Popen(
iiCommand,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=processEnv,
)
self.serverDir = self.wait_for_server_dir()
self.serverInPath = os.path.join(self.serverDir, "in")
def stop(self):
if self.channelInPath and os.path.exists(self.channelInPath):
try:
self.write_line(self.channelInPath, f"/part {self.channel}")
except OSError:
pass
if self.serverInPath and os.path.exists(self.serverInPath):
try:
self.write_line(self.serverInPath, "/quit")
except OSError:
pass
if self.iiProcess and self.iiProcess.poll() is None:
self.iiProcess.terminate()
try:
self.iiProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
self.iiProcess.kill()
def join_channel(self):
joinMessage = f"/join {self.channel}"
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
startTime = time.monotonic()
nextJoinTime = startTime
while time.monotonic() - startTime < 60:
if time.monotonic() >= nextJoinTime:
self.write_line(self.serverInPath, joinMessage)
nextJoinTime = time.monotonic() + 5
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
time.sleep(0.5)
self.channelInPath = None
def send_channel_message(self, message):
if not self.channelInPath:
self.refresh_channel_in_path()
if self.channelInPath and os.path.exists(self.channelInPath):
self.write_line(self.channelInPath, message)
else:
self.write_line(self.serverInPath, f"/msg {self.channel} {message}")
def send_private_message(self, nick, message):
nickDir = os.path.join(self.serverDir, nick)
inPath = os.path.join(nickDir, "in")
if os.path.exists(inPath):
self.write_line(inPath, message)
else:
self.write_line(self.serverInPath, f"/msg {nick} {message}")
def get_private_messages(self, allowedUsers):
messages = []
for nick in allowedUsers:
nickDir = os.path.join(self.serverDir, nick)
outPath = os.path.join(nickDir, "out")
if not os.path.exists(outPath):
continue
lastPos = self.pmOffsets.get(outPath, 0)
with open(outPath, "r", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.seek(lastPos)
for line in fileHandle:
sender = parse_sender(line)
if sender and sender == self.nick:
continue
if sender and sender != nick:
continue
messageText = extract_message_text(line)
if messageText:
messages.append((nick, messageText))
self.pmOffsets[outPath] = fileHandle.tell()
return messages
def refresh_channel_in_path(self):
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
def wait_for_server_dir(self):
for _ in range(120):
for rootDir in [self.baseDir] + list_subdirs(self.baseDir):
if not os.path.isdir(rootDir):
continue
for entry in os.listdir(rootDir):
path = os.path.join(rootDir, entry)
if os.path.isdir(path) and self.server in entry:
inPath = os.path.join(path, "in")
if os.path.exists(inPath):
return path
time.sleep(0.5)
raise RuntimeError("ii server directory not found")
@staticmethod
def write_line(path, message):
with open(path, "w", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.write(message + "\n")
fileHandle.flush()
def ii_supports_i():
result = run_command(["ii", "-h"])
output = (result.stdout or "") + (result.stderr or "")
return "-i" in output
def list_subdirs(path):
try:
return [os.path.join(path, entry) for entry in os.listdir(path)]
except OSError:
return []
def shutil_which(command):
for path in os.environ.get("PATH", "").split(os.pathsep):
candidate = os.path.join(path, command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def build_nick():
baseUser = os.environ.get("SUDO_USER") or os.environ.get("USER") or "sas"
return f"{baseUser}-{int(time.time())}"
def main():
say_or_print("Checking accessibility. Is your screen reader working? (y/n)", True)
answer = input().strip().lower()
useSpeech = answer in ("n", "no")
shouldRemoveUser = False
cleanupDone = False
tempDir = tempfile.mkdtemp(prefix="sas-ii-")
ircSession = None
sshProcess = None
def cleanup(exitMessage=None):
nonlocal cleanupDone
if cleanupDone:
return
cleanupDone = True
nonlocal sshProcess
if exitMessage:
say_or_print(exitMessage, useSpeech)
if sshProcess and sshProcess.poll() is None:
sshProcess.terminate()
try:
sshProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
sshProcess.kill()
if ircSession:
ircSession.stop()
if shouldRemoveUser:
try:
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
time.sleep(1)
result = run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
if result.returncode != 0 and user_exists(sasUser):
say_or_print(
"Cleanup warning: failed to remove sas user. Please remove it manually.",
useSpeech,
)
except Exception:
pass
sudoKeepaliveStop.set()
if sudoKeepaliveThread:
sudoKeepaliveThread.join(timeout=2)
try:
remove_tree(tempDir)
except Exception:
pass
def handle_signal(signum, frame):
cleanup("Interrupted. Cleaning up.")
sys.exit(1)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
try:
if not user_exists(sasUser):
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
else:
say_or_print(
"User 'sas' exists. Remove and recreate it? This will delete /home/sas. (y/n)",
useSpeech,
)
response = input().strip().lower()
if response not in ("y", "yes"):
cleanup("The sas user is unavailable. Remove it manually and try again.")
return 1
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
ensure_wheel(sasUser, useSpeech)
password = generate_password()
set_password(sasUser, password, useSpeech)
privateKeyPath, publicKeyPath = generate_ssh_key(sasUser, useSpeech)
sasHome = get_user_home(sasUser)
knownHostsPath = os.path.join(sasHome, ".ssh", "known_hosts_sas")
run_privileged(["touch", knownHostsPath], useSpeech)
run_privileged(["chmod", "600", knownHostsPath], useSpeech)
run_privileged(["chown", f"{sasUser}:{sasUser}", knownHostsPath], useSpeech)
nick = build_nick()
ircSession = IrcSession(ircServer, ircPort, nick, ircChannel, tempDir)
ircSession.start()
ircSession.join_channel()
say_or_print("Waiting for assistance on IRC.", useSpeech)
startTime = time.monotonic()
nextPingTime = startTime
pingsSent = 0
confirmedAdmin = None
while time.monotonic() - startTime < pingIntervalSeconds * pingCount:
now = time.monotonic()
if pingsSent < pingCount and now >= nextPingTime:
ircSession.send_channel_message(f"{nick} is requesting assistance.")
pingsSent += 1
nextPingTime = startTime + (pingsSent * pingIntervalSeconds)
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
if messageText.strip().lower() in ("yes", "accept"):
confirmedAdmin = adminNick
break
if confirmedAdmin:
break
time.sleep(1)
if not confirmedAdmin:
cleanup("No one was available to help, please try again later.")
return 1
ircSession.send_private_message(
confirmedAdmin,
f'password: "{password}" please send wormhole ssh invite code',
)
failures = 0
while failures < maxWormholeFailures:
inviteCode = None
while inviteCode is None:
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
inviteCode = find_wormhole_code(messageText)
if inviteCode:
break
if inviteCode:
break
time.sleep(1)
if not path_exists_for_user(publicKeyPath, sasUser, useSpeech):
raise RuntimeError(f"Public key missing: {publicKeyPath}")
wormholeCommand = [
"wormhole",
"ssh",
"accept",
"--yes",
inviteCode,
]
result = run_as_user(sasUser, wormholeCommand, useSpeech, check=False)
if result.returncode == 0:
say_or_print("Wormhole key transfer succeeded.", useSpeech)
break
failures += 1
errorTextFull = (result.stderr or result.stdout or "").strip()
if errorTextFull and not useSpeech:
print("Wormhole ssh accept error:", flush=True)
print(errorTextFull, flush=True)
errorText = errorTextFull
if errorText:
errorText = " ".join(errorText.split())
if len(errorText) > 400:
errorText = errorText[:400] + "..."
ircSession.send_private_message(
confirmedAdmin,
f"Wormhole ssh accept failed: {errorText}",
)
ircSession.send_private_message(
confirmedAdmin,
"Wormhole ssh accept failed. Please send a new invite code.",
)
if failures >= maxWormholeFailures:
cleanup("Wormhole failed too many times. Exiting.")
return 1
say_or_print("Starting reverse SSH tunnel. Press Ctrl+C to stop.", useSpeech)
sshCommand = [
"ssh",
"-N",
"-R",
"localhost:2232:localhost:22",
"-o",
"ExitOnForwardFailure=yes",
"-o",
"BatchMode=yes",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=3",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
f"UserKnownHostsFile={knownHostsPath}",
"-i",
privateKeyPath,
f"{sasUser}@{remoteHost}",
]
sshCommand = ["sudo", "-u", sasUser, "-H"] + sshCommand
sshProcess = subprocess.Popen(sshCommand)
sshProcess.wait()
except Exception as exc:
cleanup(f"Error: {exc}")
return 1
finally:
cleanup()
return 0
def remove_tree(path):
if not os.path.exists(path):
return
for rootDir, dirNames, fileNames in os.walk(path, topdown=False):
for fileName in fileNames:
try:
os.unlink(os.path.join(rootDir, fileName))
except OSError:
pass
for dirName in dirNames:
try:
os.rmdir(os.path.join(rootDir, dirName))
except OSError:
pass
try:
os.rmdir(path)
except OSError:
pass
if __name__ == "__main__":
sys.exit(main())
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
set -euo pipefail
readonly repoName="stormux"
readonly repoKeyUrl="https://packages.stormux.org/stormux_repo.pub"
readonly repoKeyId="52ADA49000F1FF0456F8AEEFB4CDE1CD56EF8E82"
readonly pacmanConf="${STORMUX_PACMAN_CONF:-/etc/pacman.conf}"
readonly osRelease="${STORMUX_OS_RELEASE:-/etc/os-release}"
readonly effectiveUid="${STORMUX_TEST_EUID:-${EUID}}"
keyFile=""
cleanup() {
if [[ -n "$keyFile" && -f "$keyFile" ]]; then
rm -f "$keyFile"
fi
}
die() {
printf 'Error: %s\n' "$1" >&2
exit 1
}
require_root() {
[[ "$effectiveUid" == "0" ]] || die "This script must be run as root."
}
require_command() {
local commandName="$1"
command -v "$commandName" >/dev/null 2>&1 || die "Required command not found: ${commandName}"
}
is_arch_based_system() {
local osId=""
local osIdLike=""
[[ -f "$osRelease" ]] || return 1
while IFS='=' read -r keyName keyValue; do
keyValue="${keyValue%\"}"
keyValue="${keyValue#\"}"
case "$keyName" in
ID)
osId="$keyValue"
;;
ID_LIKE)
osIdLike="$keyValue"
;;
esac
done < "$osRelease"
[[ "$osId" == "arch" || "$osId" == "archarm" || " ${osIdLike} " == *" arch "* ]]
}
check_prerequisites() {
require_root
require_command curl
require_command pacman
require_command pacman-key
is_arch_based_system || die "This script supports Arch-based pacman systems only."
[[ -f "$pacmanConf" ]] || die "pacman.conf not found: ${pacmanConf}"
}
download_and_trust_key() {
keyFile="$(mktemp)"
trap cleanup EXIT
printf 'Downloading Stormux repository key...\n'
curl -fsSL "$repoKeyUrl" > "$keyFile"
printf 'Adding Stormux repository key to pacman keyring...\n'
pacman-key --add "$keyFile"
printf 'Locally signing Stormux repository key...\n'
pacman-key --lsign-key "$repoKeyId"
}
update_pacman_conf() {
local tempConf
tempConf="$(mktemp)"
awk -v repoName="$repoName" '
function print_repo_block() {
print "[" repoName "]"
print "SigLevel = Required DatabaseOptional"
print "Server = https://packages.stormux.org/$arch"
}
/^\[stormux\][[:space:]]*$/ {
inStormux = 1
next
}
inStormux && /^\[[^]]+\][[:space:]]*$/ {
inStormux = 0
}
inStormux {
next
}
!inserted && /^\[core\][[:space:]]*$/ {
print_repo_block()
print ""
inserted = 1
}
{
print
}
END {
if (!inserted) {
print ""
print_repo_block()
}
}
' "$pacmanConf" > "$tempConf"
cat "$tempConf" > "$pacmanConf"
rm -f "$tempConf"
}
refresh_databases() {
printf 'Refreshing package databases...\n'
pacman -Sy
}
main() {
check_prerequisites
download_and_trust_key
update_pacman_conf
refresh_databases
printf 'Stormux repository is configured.\n'
}
main "$@"
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
repoDir="/var/www/packages.stormux.org"
aurRpcUrl="https://aur.archlinux.org/rpc/v5/info"
exclude=("gzdoom")
require_cmd() {
local cmd="$1"
if ! command -v "$cmd" >/dev/null 2>&1; then
printf 'Required command not found: %s\n' "$cmd" >&2
exit 1
fi
}
is_excluded() {
local packageName="$1"
local excludedPackage
for excludedPackage in "${exclude[@]}"; do
if [[ "$excludedPackage" == "$packageName" ]]; then
return 0
fi
done
return 1
}
record_local_package_version() {
local packageName="$1"
local packageVersion="$2"
local packageMapName="$3"
local -n packageMapRef="$packageMapName"
local existingVersion="${packageMapRef[$packageName]:-}"
if [[ -z "$existingVersion" ]] || (( $(vercmp "$packageVersion" "$existingVersion") > 0 )); then
packageMapRef["$packageName"]="$packageVersion"
fi
}
read_package_metadata() {
local packageFile="$1"
pacman -Qip "$packageFile" | parse_pacman_info
}
parse_pacman_info() {
awk '
/^Name[[:space:]]*:/ {
packageName=$0
sub(/^Name[[:space:]]*:[[:space:]]*/, "", packageName)
}
/^Version[[:space:]]*:/ {
packageVersion=$0
sub(/^Version[[:space:]]*:[[:space:]]*/, "", packageVersion)
}
END {
if (packageName == "" || packageVersion == "") {
exit 1
}
printf "%s\t%s\n", packageName, packageVersion
}
'
}
collect_local_packages() {
local packageMapName="$1"
# shellcheck disable=SC2178
local -n packageMapRef="$packageMapName"
local archDir packageFile metadata packageName packageVersion
local -a archDirs=("$repoDir/x86_64" "$repoDir/aarch64")
for archDir in "${archDirs[@]}"; do
if [[ ! -d "$archDir" ]]; then
continue
fi
for packageFile in "$archDir"/*.pkg.tar.zst "$archDir"/*.pkg.tar.xz; do
metadata="$(read_package_metadata "$packageFile")" || {
printf 'Unable to read package metadata: %s\n' "$packageFile" >&2
exit 1
}
packageName="${metadata%%$'\t'*}"
packageVersion="${metadata#*$'\t'}"
record_local_package_version "$packageName" "$packageVersion" "$packageMapName"
done
done
}
extract_aur_version_from_json() {
local packageName="$1"
jq -r --arg packageName "$packageName" '
.results[]
| select(.Name == $packageName)
| .Version
' | head -n1
}
fetch_aur_version() {
local packageName="$1"
curl -fsS --get \
--data-urlencode "arg[]=${packageName}" \
"$aurRpcUrl" |
extract_aur_version_from_json "$packageName"
}
print_outdated_packages() {
local packageMapName="$1"
# shellcheck disable=SC2178
local -n packageMapRef="$packageMapName"
local packageName localVersion aurVersion
local -a packageNames=()
mapfile -t packageNames < <(printf '%s\n' "${!packageMapRef[@]}" | sort)
for packageName in "${packageNames[@]}"; do
if is_excluded "$packageName"; then
continue
fi
localVersion="${packageMapRef[$packageName]}"
aurVersion="$(fetch_aur_version "$packageName" || true)"
if [[ -z "$aurVersion" ]]; then
continue
fi
if (( $(vercmp "$localVersion" "$aurVersion") < 0 )); then
printf '%s %s\n' "$packageName" "$aurVersion"
fi
done
}
assert_equals() {
local expected="$1"
local actual="$2"
local message="$3"
if [[ "$expected" != "$actual" ]]; then
printf 'FAIL: %s\nExpected: %s\nActual: %s\n' "$message" "$expected" "$actual" >&2
exit 1
fi
}
assert_success() {
local message="$1"
shift
if ! "$@"; then
printf 'FAIL: %s\n' "$message" >&2
exit 1
fi
}
assert_failure() {
local message="$1"
shift
if "$@"; then
printf 'FAIL: %s\n' "$message" >&2
exit 1
fi
}
main() {
if [[ "${1:-}" == "--self-test" ]]; then
run_self_tests
return
fi
require_cmd "curl"
require_cmd "jq"
require_cmd "pacman"
require_cmd "vercmp"
if [[ ! -d "$repoDir" ]]; then
printf 'Repo dir does not exist: %s\n' "$repoDir" >&2
exit 1
fi
# shellcheck disable=SC2034
declare -A localPackages=()
collect_local_packages localPackages
print_outdated_packages localPackages
}
run_self_tests() {
declare -A packageMap=()
local exactMatchJson noMatchJson
local extractedVersion=""
local pacmanInfo=""
assert_success "excluded package should match" is_excluded "gzdoom"
assert_failure "non-excluded package should not match" is_excluded "fenrir"
record_local_package_version "fenrir" "1:2026.01.20-1" packageMap
record_local_package_version "fenrir" "1:2026.01.28-1" packageMap
record_local_package_version "fenrir" "1:2026.01.10-1" packageMap
assert_equals "1:2026.01.28-1" "${packageMap[fenrir]}" "newest local version should win"
exactMatchJson='{"results":[{"Name":"fenrir-git","Version":"1:r3322.4672592d-1"},{"Name":"fenrir","Version":"1:2026.01.28-1"}]}'
extractedVersion="$(printf '%s\n' "$exactMatchJson" | extract_aur_version_from_json "fenrir")"
assert_equals "1:2026.01.28-1" "$extractedVersion" "exact package name should be selected from AUR JSON"
noMatchJson='{"results":[{"Name":"fenrir-git","Version":"1:r3322.4672592d-1"}]}'
extractedVersion="$(printf '%s\n' "$noMatchJson" | extract_aur_version_from_json "fenrir")"
assert_equals "" "$extractedVersion" "missing exact AUR match should stay empty"
pacmanInfo='Name : fenrir
Version : 1:2026.01.28-1
Description : A user space console screen reader written in python3'
extractedVersion="$(printf '%s\n' "$pacmanInfo" | parse_pacman_info)"
assert_equals $'fenrir\t1:2026.01.28-1' "$extractedVersion" "pacman metadata parsing should return name and version"
if (( $(vercmp "1:2026.01.20-1" "1:2026.01.28-1") >= 0 )); then
printf 'FAIL: older local version should compare lower than AUR version\n' >&2
exit 1
fi
printf 'Self-test passed\n'
}
main "$@"
@@ -5,6 +5,29 @@
set -e -u set -e -u
install_sas() {
local sasRepo="https://git.stormux.org/storm/sas"
local sasPath="/usr/local/bin/sas"
local tempDir
local installStatus=0
tempDir="$(mktemp -d)"
echo "Installing latest sas..."
if ! git clone --depth 1 "$sasRepo" "$tempDir"; then
rm -rf "$tempDir"
return 1
fi
rm -f "$sasPath"
if ! install -m 755 "$tempDir/sas.py" "$sasPath"; then
installStatus=1
fi
rm -rf "$tempDir"
return "$installStatus"
}
# Initialize pacman keyring # Initialize pacman keyring
echo "Initializing pacman keyring..." echo "Initializing pacman keyring..."
pacman-key --init pacman-key --init
@@ -25,6 +48,8 @@ fi
echo "en_US.UTF-8 UTF-8" > /etc/locale.gen echo "en_US.UTF-8 UTF-8" > /etc/locale.gen
locale-gen locale-gen
install_sas
# Enable system services # Enable system services
systemctl enable NetworkManager.service systemctl enable NetworkManager.service
systemctl enable fenrirscreenreader.service systemctl enable fenrirscreenreader.service
-1
View File
@@ -1 +0,0 @@
sas.sh
-582
View File
@@ -1,582 +0,0 @@
#!/usr/bin/env python3
import os
import re
import secrets
import signal
import string
import subprocess
import sys
import tempfile
import time
import pwd
import threading
stormuxAdmin = ("storm",)
ircServer = "irc.stormux.org"
ircPort = 6667
ircChannel = "#stormux"
remoteHost = "billysballoons.com"
sasUser = "sas"
pingIntervalSeconds = 180
pingCount = 5
maxWormholeFailures = 3
sudoKeepaliveThread = None
sudoKeepaliveStop = threading.Event()
def speak_message(message):
try:
subprocess.run(["spd-say", message], check=False)
except FileNotFoundError:
print(message, flush=True)
def say_or_print(message, useSpeech):
if useSpeech:
speak_message(message)
else:
print(message, flush=True)
def run_command(command, inputText=None, check=False, env=None):
return subprocess.run(
command,
input=inputText,
text=True,
capture_output=True,
check=check,
env=env,
)
def ensure_sudo(useSpeech):
if os.geteuid() == 0:
return True
if useSpeech:
speak_message("Sudo password required. Please enter your password now.")
result = run_command(["sudo", "-v"])
if result.returncode == 0:
start_sudo_keepalive()
return True
return False
def start_sudo_keepalive():
global sudoKeepaliveThread
if sudoKeepaliveThread and sudoKeepaliveThread.is_alive():
return
def keepalive_loop():
while not sudoKeepaliveStop.wait(240):
run_command(["sudo", "-n", "-v"])
sudoKeepaliveThread = threading.Thread(target=keepalive_loop, daemon=True)
sudoKeepaliveThread.start()
def run_privileged(command, useSpeech, inputText=None, check=True):
if os.geteuid() == 0:
fullCommand = command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo"] + command
return run_command(fullCommand, inputText=inputText, check=check)
def run_as_user(userName, command, useSpeech, check=True):
if os.geteuid() == 0:
fullCommand = ["sudo", "-u", userName, "-H"] + command
else:
if not ensure_sudo(useSpeech):
raise RuntimeError("sudo authentication failed")
fullCommand = ["sudo", "-u", userName, "-H"] + command
return run_command(fullCommand, check=check)
def user_exists(userName):
result = run_command(["getent", "passwd", userName])
return result.returncode == 0
def ensure_wheel(userName, useSpeech):
result = run_command(["id", "-nG", userName])
groups = result.stdout.strip().split()
if "wheel" not in groups:
run_privileged(["usermod", "-a", "-G", "wheel", userName], useSpeech)
def generate_password():
allowedChars = string.ascii_letters + string.digits
length = secrets.randbelow(5) + 6
return "".join(secrets.choice(allowedChars) for _ in range(length))
def get_user_home(userName):
return pwd.getpwnam(userName).pw_dir
def set_password(userName, password, useSpeech):
run_privileged(["chpasswd"], useSpeech, inputText=f"{userName}:{password}\n")
def generate_ssh_key(userName, useSpeech):
userHome = get_user_home(userName)
sshDir = os.path.join(userHome, ".ssh")
privateKeyPath = os.path.join(sshDir, "id_ed25519")
publicKeyPath = f"{privateKeyPath}.pub"
run_privileged(["mkdir", "-p", sshDir], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", sshDir], useSpeech)
run_privileged(["chmod", "700", sshDir], useSpeech)
for entry in list_subdirs(sshDir):
if os.path.isfile(entry) or os.path.islink(entry):
run_privileged(["rm", "-f", entry], useSpeech, check=False)
run_as_user(
userName,
["ssh-keygen", "-t", "ed25519", "-N", "", "-f", privateKeyPath],
useSpeech,
)
run_privileged(["chmod", "600", privateKeyPath], useSpeech)
run_privileged(["chmod", "644", publicKeyPath], useSpeech)
run_privileged(["chown", f"{userName}:{userName}", privateKeyPath, publicKeyPath], useSpeech)
return privateKeyPath, publicKeyPath
def path_exists_for_user(path, userName, useSpeech):
result = run_as_user(userName, ["stat", path], useSpeech, check=False)
return result.returncode == 0
def extract_message_text(line):
if "> " in line:
return line.split("> ", 1)[1].strip()
if ": " in line:
return line.split(": ", 1)[1].strip()
return line.strip()
def parse_sender(line):
match = re.search(r"<([^>]+)>", line)
if match:
return match.group(1)
return None
def find_wormhole_code(message):
lowered = message.strip().lower()
if lowered in ("yes", "accept"):
return None
match = re.search(r"\b\d+-[A-Za-z0-9-]+\b", message)
if match:
return match.group(0)
return None
class IrcSession:
def __init__(self, server, port, nick, channel, baseDir):
self.server = server
self.port = port
self.nick = nick
self.channel = channel
self.baseDir = baseDir
self.serverDir = None
self.serverInPath = None
self.channelInPath = None
self.iiProcess = None
self.pmOffsets = {}
def start(self):
if not shutil_which("ii"):
raise RuntimeError("ii is not installed")
supportsI = ii_supports_i()
processEnv = os.environ.copy()
iiCommand = ["ii", "-s", self.server, "-p", str(self.port), "-n", self.nick]
if supportsI:
iiCommand += ["-i", self.baseDir]
else:
processEnv["HOME"] = self.baseDir
self.iiProcess = subprocess.Popen(
iiCommand,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
env=processEnv,
)
self.serverDir = self.wait_for_server_dir()
self.serverInPath = os.path.join(self.serverDir, "in")
def stop(self):
if self.channelInPath and os.path.exists(self.channelInPath):
try:
self.write_line(self.channelInPath, f"/part {self.channel}")
except OSError:
pass
if self.serverInPath and os.path.exists(self.serverInPath):
try:
self.write_line(self.serverInPath, "/quit")
except OSError:
pass
if self.iiProcess and self.iiProcess.poll() is None:
self.iiProcess.terminate()
try:
self.iiProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
self.iiProcess.kill()
def join_channel(self):
joinMessage = f"/join {self.channel}"
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
startTime = time.monotonic()
nextJoinTime = startTime
while time.monotonic() - startTime < 60:
if time.monotonic() >= nextJoinTime:
self.write_line(self.serverInPath, joinMessage)
nextJoinTime = time.monotonic() + 5
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
time.sleep(0.5)
self.channelInPath = None
def send_channel_message(self, message):
if not self.channelInPath:
self.refresh_channel_in_path()
if self.channelInPath and os.path.exists(self.channelInPath):
self.write_line(self.channelInPath, message)
else:
self.write_line(self.serverInPath, f"/msg {self.channel} {message}")
def send_private_message(self, nick, message):
nickDir = os.path.join(self.serverDir, nick)
inPath = os.path.join(nickDir, "in")
if os.path.exists(inPath):
self.write_line(inPath, message)
else:
self.write_line(self.serverInPath, f"/msg {nick} {message}")
def get_private_messages(self, allowedUsers):
messages = []
for nick in allowedUsers:
nickDir = os.path.join(self.serverDir, nick)
outPath = os.path.join(nickDir, "out")
if not os.path.exists(outPath):
continue
lastPos = self.pmOffsets.get(outPath, 0)
with open(outPath, "r", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.seek(lastPos)
for line in fileHandle:
sender = parse_sender(line)
if sender and sender == self.nick:
continue
if sender and sender != nick:
continue
messageText = extract_message_text(line)
if messageText:
messages.append((nick, messageText))
self.pmOffsets[outPath] = fileHandle.tell()
return messages
def refresh_channel_in_path(self):
channelDir = os.path.join(self.serverDir, self.channel)
channelAltDir = os.path.join(self.serverDir, self.channel.lstrip("#"))
for candidate in (channelDir, channelAltDir):
inPath = os.path.join(candidate, "in")
if os.path.exists(inPath):
self.channelInPath = inPath
return
def wait_for_server_dir(self):
for _ in range(120):
for rootDir in [self.baseDir] + list_subdirs(self.baseDir):
if not os.path.isdir(rootDir):
continue
for entry in os.listdir(rootDir):
path = os.path.join(rootDir, entry)
if os.path.isdir(path) and self.server in entry:
inPath = os.path.join(path, "in")
if os.path.exists(inPath):
return path
time.sleep(0.5)
raise RuntimeError("ii server directory not found")
@staticmethod
def write_line(path, message):
with open(path, "w", encoding="utf-8", errors="ignore") as fileHandle:
fileHandle.write(message + "\n")
fileHandle.flush()
def ii_supports_i():
result = run_command(["ii", "-h"])
output = (result.stdout or "") + (result.stderr or "")
return "-i" in output
def list_subdirs(path):
try:
return [os.path.join(path, entry) for entry in os.listdir(path)]
except OSError:
return []
def shutil_which(command):
for path in os.environ.get("PATH", "").split(os.pathsep):
candidate = os.path.join(path, command)
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
return candidate
return None
def build_nick():
baseUser = os.environ.get("SUDO_USER") or os.environ.get("USER") or "sas"
return f"{baseUser}-{int(time.time())}"
def main():
say_or_print("Checking accessibility. Is your screen reader working? (y/n)", True)
answer = input().strip().lower()
useSpeech = answer in ("n", "no")
shouldRemoveUser = False
cleanupDone = False
tempDir = tempfile.mkdtemp(prefix="sas-ii-")
ircSession = None
sshProcess = None
def cleanup(exitMessage=None):
nonlocal cleanupDone
if cleanupDone:
return
cleanupDone = True
nonlocal sshProcess
if exitMessage:
say_or_print(exitMessage, useSpeech)
if sshProcess and sshProcess.poll() is None:
sshProcess.terminate()
try:
sshProcess.wait(timeout=5)
except subprocess.TimeoutExpired:
sshProcess.kill()
if ircSession:
ircSession.stop()
if shouldRemoveUser:
try:
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
time.sleep(1)
result = run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
if result.returncode != 0 and user_exists(sasUser):
say_or_print(
"Cleanup warning: failed to remove sas user. Please remove it manually.",
useSpeech,
)
except Exception:
pass
sudoKeepaliveStop.set()
if sudoKeepaliveThread:
sudoKeepaliveThread.join(timeout=2)
try:
remove_tree(tempDir)
except Exception:
pass
def handle_signal(signum, frame):
cleanup("Interrupted. Cleaning up.")
sys.exit(1)
signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)
try:
if not user_exists(sasUser):
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
else:
say_or_print(
"User 'sas' exists. Remove and recreate it? This will delete /home/sas. (y/n)",
useSpeech,
)
response = input().strip().lower()
if response not in ("y", "yes"):
cleanup("The sas user is unavailable. Remove it manually and try again.")
return 1
run_privileged(["pkill", "-u", sasUser], useSpeech, check=False)
run_privileged(["userdel", "-r", sasUser], useSpeech, check=False)
run_privileged(["rm", "-rf", f"/home/{sasUser}"], useSpeech, check=False)
run_privileged(
["useradd", "-m", "-d", f"/home/{sasUser}", "-s", "/bin/bash", "-G", "wheel", sasUser],
useSpeech,
)
shouldRemoveUser = True
ensure_wheel(sasUser, useSpeech)
password = generate_password()
set_password(sasUser, password, useSpeech)
privateKeyPath, publicKeyPath = generate_ssh_key(sasUser, useSpeech)
sasHome = get_user_home(sasUser)
knownHostsPath = os.path.join(sasHome, ".ssh", "known_hosts_sas")
run_privileged(["touch", knownHostsPath], useSpeech)
run_privileged(["chmod", "600", knownHostsPath], useSpeech)
run_privileged(["chown", f"{sasUser}:{sasUser}", knownHostsPath], useSpeech)
nick = build_nick()
ircSession = IrcSession(ircServer, ircPort, nick, ircChannel, tempDir)
ircSession.start()
ircSession.join_channel()
say_or_print("Waiting for assistance on IRC.", useSpeech)
startTime = time.monotonic()
nextPingTime = startTime
pingsSent = 0
confirmedAdmin = None
while time.monotonic() - startTime < pingIntervalSeconds * pingCount:
now = time.monotonic()
if pingsSent < pingCount and now >= nextPingTime:
ircSession.send_channel_message(f"{nick} is requesting assistance.")
pingsSent += 1
nextPingTime = startTime + (pingsSent * pingIntervalSeconds)
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
if messageText.strip().lower() in ("yes", "accept"):
confirmedAdmin = adminNick
break
if confirmedAdmin:
break
time.sleep(1)
if not confirmedAdmin:
cleanup("No one was available to help, please try again later.")
return 1
ircSession.send_private_message(
confirmedAdmin,
f'password: "{password}" please send wormhole ssh invite code',
)
failures = 0
while failures < maxWormholeFailures:
inviteCode = None
while inviteCode is None:
for adminNick, messageText in ircSession.get_private_messages(stormuxAdmin):
inviteCode = find_wormhole_code(messageText)
if inviteCode:
break
if inviteCode:
break
time.sleep(1)
if not path_exists_for_user(publicKeyPath, sasUser, useSpeech):
raise RuntimeError(f"Public key missing: {publicKeyPath}")
wormholeCommand = [
"wormhole",
"ssh",
"accept",
"--yes",
inviteCode,
]
result = run_as_user(sasUser, wormholeCommand, useSpeech, check=False)
if result.returncode == 0:
say_or_print("Wormhole key transfer succeeded.", useSpeech)
break
failures += 1
errorTextFull = (result.stderr or result.stdout or "").strip()
if errorTextFull and not useSpeech:
print("Wormhole ssh accept error:", flush=True)
print(errorTextFull, flush=True)
errorText = errorTextFull
if errorText:
errorText = " ".join(errorText.split())
if len(errorText) > 400:
errorText = errorText[:400] + "..."
ircSession.send_private_message(
confirmedAdmin,
f"Wormhole ssh accept failed: {errorText}",
)
ircSession.send_private_message(
confirmedAdmin,
"Wormhole ssh accept failed. Please send a new invite code.",
)
if failures >= maxWormholeFailures:
cleanup("Wormhole failed too many times. Exiting.")
return 1
say_or_print("Starting reverse SSH tunnel. Press Ctrl+C to stop.", useSpeech)
sshCommand = [
"ssh",
"-N",
"-R",
"localhost:2232:localhost:22",
"-o",
"ExitOnForwardFailure=yes",
"-o",
"BatchMode=yes",
"-o",
"ServerAliveInterval=30",
"-o",
"ServerAliveCountMax=3",
"-o",
"StrictHostKeyChecking=accept-new",
"-o",
f"UserKnownHostsFile={knownHostsPath}",
"-i",
privateKeyPath,
f"{sasUser}@{remoteHost}",
]
sshCommand = ["sudo", "-u", sasUser, "-H"] + sshCommand
sshProcess = subprocess.Popen(sshCommand)
sshProcess.wait()
except Exception as exc:
cleanup(f"Error: {exc}")
return 1
finally:
cleanup()
return 0
def remove_tree(path):
if not os.path.exists(path):
return
for rootDir, dirNames, fileNames in os.walk(path, topdown=False):
for fileName in fileNames:
try:
os.unlink(os.path.join(rootDir, fileName))
except OSError:
pass
for dirName in dirNames:
try:
os.rmdir(os.path.join(rootDir, dirName))
except OSError:
pass
try:
os.rmdir(path)
except OSError:
pass
if __name__ == "__main__":
sys.exit(main())
+1
View File
@@ -18,6 +18,7 @@ btrfs-progs
clonezilla clonezilla
cloud-init cloud-init
cryptsetup cryptsetup
curl
darkhttpd darkhttpd
ddrescue ddrescue
dhcpcd dhcpcd
-3
View File
@@ -19,7 +19,6 @@ file_permissions=(
["/etc/shadow"]="0:0:400" ["/etc/shadow"]="0:0:400"
["/root"]="0:0:750" ["/root"]="0:0:750"
["/root/.automated_script.sh"]="0:0:755" ["/root/.automated_script.sh"]="0:0:755"
["/usr/local/bin/sas"]="0:0:755"
["/root/customize_airootfs.sh"]="0:0:755" ["/root/customize_airootfs.sh"]="0:0:755"
["/root/.gnupg"]="0:0:700" ["/root/.gnupg"]="0:0:700"
["/root/.config"]="0:0:700" ["/root/.config"]="0:0:700"
@@ -30,8 +29,6 @@ file_permissions=(
["/usr/local/bin/install-stormux"]="0:0:755" ["/usr/local/bin/install-stormux"]="0:0:755"
["/usr/local/bin/stormux-setup.sh"]="0:0:755" ["/usr/local/bin/stormux-setup.sh"]="0:0:755"
["/usr/local/bin/init-pipewire-sound.sh"]="0:0:755" ["/usr/local/bin/init-pipewire-sound.sh"]="0:0:755"
["/usr/local/bin/sas.sh"]="0:0:755"
["/usr/local/bin/sas"]="0:0:755"
["/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh"]="0:0:755" ["/usr/share/fenrirscreenreader/scripts/ssh-login-monitor.sh"]="0:0:755"
["/etc/stormux-assist"]="0:0:755" ["/etc/stormux-assist"]="0:0:755"
["/etc/stormux-assist/client.conf"]="0:0:644" ["/etc/stormux-assist/client.conf"]="0:0:644"