diff --git a/etc/systemd/system/stormux-user-dirs.service b/etc/systemd/system/stormux-user-dirs.service new file mode 100644 index 0000000..a4ffe01 --- /dev/null +++ b/etc/systemd/system/stormux-user-dirs.service @@ -0,0 +1,13 @@ +[Unit] +Description=Create Stormux user directories +RequiresMountsFor=/home/stormux +After=local-fs.target user@1000.service + +[Service] +Type=oneshot +User=stormux +Environment=HOME=/home/stormux +ExecStart=/usr/bin/xdg-user-dirs-update + +[Install] +WantedBy=multi-user.target diff --git a/home/stormux/.local/.functions/download.sh b/home/stormux/.local/.functions/download.sh index 75dd19a..6734eaa 100755 --- a/home/stormux/.local/.functions/download.sh +++ b/home/stormux/.local/.functions/download.sh @@ -5,31 +5,47 @@ download_with_fallback() { local outputPath="$1" local sourceUrl="$2" local errorFile="" + local curlArgs=( + --fail + --show-error + --location + --connect-timeout 20 + --speed-limit 1024 + --speed-time 30 + --retry 3 + --retry-connrefused + --retry-delay 2 + ) local curlExitCode=0 local retryExitCode=0 local fallbackExitCode=0 local wgetExitCode=0 + local totalBytes=0 downloadAttemptExitCodes="" downloadErrorLog="" errorFile="$(mktemp)" || return 1 + totalBytes="$(get_remote_file_size "${sourceUrl}")" - if curl -L4 -C - --retry 10 --output "${outputPath}" "${sourceUrl}" 2> "${errorFile}"; then + printf 'Trying curl with resume support: %s\n' "${sourceUrl}" >&2 + if run_download_command "${outputPath}" "${totalBytes}" "${errorFile}" curl -4 -C - "${curlArgs[@]}" --output "${outputPath}" "${sourceUrl}"; then rm -f "${errorFile}" return 0 fi curlExitCode=$? rm -f "${outputPath}" - if curl -L4 --retry 10 --output "${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then + printf 'Trying fresh IPv4 curl download: %s\n' "${sourceUrl}" >&2 + if run_download_command "${outputPath}" "${totalBytes}" "${errorFile}" curl -4 "${curlArgs[@]}" --output "${outputPath}" "${sourceUrl}"; then rm -f "${errorFile}" return 0 fi retryExitCode=$? rm -f "${outputPath}" - if curl -L --retry 10 --output "${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then + printf 'Trying fresh curl download with any address family: %s\n' "${sourceUrl}" >&2 + if run_download_command "${outputPath}" "${totalBytes}" "${errorFile}" curl "${curlArgs[@]}" --output "${outputPath}" "${sourceUrl}"; then rm -f "${errorFile}" return 0 fi @@ -37,7 +53,8 @@ download_with_fallback() { if command -v wget > /dev/null 2>&1; then rm -f "${outputPath}" - if wget --tries=10 --output-document="${outputPath}" "${sourceUrl}" 2>> "${errorFile}"; then + printf 'Trying wget fallback: %s\n' "${sourceUrl}" >&2 + if run_download_command "${outputPath}" "${totalBytes}" "${errorFile}" wget --tries=3 --timeout=20 --read-timeout=30 --output-document="${outputPath}" "${sourceUrl}"; then rm -f "${errorFile}" return 0 fi @@ -58,6 +75,102 @@ download_with_fallback() { return 1 } +get_remote_file_size() { + local sourceUrl="$1" + local headerOutput + + headerOutput="$(curl --fail --silent --show-error --location --head --max-redirs 10 --max-time 20 "${sourceUrl}" 2> /dev/null || true)" + awk ' + BEGIN { IGNORECASE = 1 } + /^content-length:/ { + gsub("\r", "", $2) + if ($2 ~ /^[0-9]+$/) { + contentLength = $2 + } + } + END { + if (contentLength != "") { + print contentLength + } else { + print 0 + } + } + ' <<< "${headerOutput}" +} + +beep_download_progress() { + local percent="$1" + local frequency + + [[ "${STORMUX_DOWNLOAD_BEEPS:-1}" == "1" ]] || return 0 + command -v play > /dev/null 2>&1 || return 0 + + frequency="$((220 + (percent * 7)))" + play -q -n synth 0.06 sine "${frequency}" vol 0.12 > /dev/null 2>&1 || true +} + +monitor_download_progress() { + local outputPath="$1" + local totalBytes="$2" + local doneFile="$3" + local currentBytes=0 + local percent=0 + local lastAnnouncedPercent=-10 + local tickCount=0 + + while [[ ! -e "$doneFile" ]]; do + if [[ -f "$outputPath" ]]; then + currentBytes="$(stat -c '%s' "$outputPath" 2> /dev/null || printf '0')" + else + currentBytes=0 + fi + + if [[ "$totalBytes" -gt 0 ]]; then + percent="$(((currentBytes * 100) / totalBytes))" + if [[ "$percent" -gt 100 ]]; then + percent=100 + fi + if [[ "$percent" -ge $((lastAnnouncedPercent + 10)) ]]; then + printf 'Downloaded %s%% (%s/%s bytes)\n' "$percent" "$currentBytes" "$totalBytes" >&2 + beep_download_progress "$percent" + lastAnnouncedPercent="$percent" + fi + else + tickCount="$((tickCount + 1))" + if [[ "$tickCount" -ge 5 ]]; then + printf 'Downloaded %s bytes\n' "$currentBytes" >&2 + beep_download_progress 50 + tickCount=0 + fi + fi + sleep 2 + done +} + +run_download_command() { + local outputPath="$1" + local totalBytes="$2" + local errorFile="$3" + local doneFile + local monitorPid + local commandStatus=0 + shift 3 + + doneFile="$(mktemp)" || return 1 + rm -f "$doneFile" + monitor_download_progress "$outputPath" "$totalBytes" "$doneFile" & + monitorPid="$!" + + "$@" 2> >(tee -a "$errorFile" >&2) + commandStatus="$?" + + touch "$doneFile" + wait "$monitorPid" 2> /dev/null || true + rm -f "$doneFile" + + return "$commandStatus" +} + validate_downloaded_cache_file() { local dest="$1" local downloadError=0 diff --git a/home/stormux/.local/bin/game_launcher.py b/home/stormux/.local/bin/game_launcher.py index dba7567..7d15e30 100755 --- a/home/stormux/.local/bin/game_launcher.py +++ b/home/stormux/.local/bin/game_launcher.py @@ -583,11 +583,13 @@ class VoicedMenu: # Add the appropriate items based on current status for each service for friendlyName, serviceName in self.systemMenuServices.items(): isActive = self.check_service_status(serviceName) + startLabel = "Start" if friendlyName == "Fenrir Screen Reader" else "Enable" + stopLabel = "Stop" if friendlyName == "Fenrir Screen Reader" else "Disable" if isActive: - self.add_item("System", f"Disable {friendlyName}", + self.add_item("System", f"{stopLabel} {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn)) else: - self.add_item("System", f"Enable {friendlyName}", + self.add_item("System", f"{startLabel} {friendlyName}", lambda fn=friendlyName: self.toggle_service(fn)) def install_and_launch(self, executable_name, launch_mode="gui"):