fix: keep Steam out of an endless update loop

Steam compares its installed files against its manifest at every start and
restores whatever differs, which is why its launcher entry carries
-noverifyfiles. The entry in ~/.config/autostart never got it: it points at
/usr/bin/steam, a shell script, so it fell through the Chromium filter in
mca_autostart_apply and was left alone.

A Steam started at login therefore verified, found the web helper script 46
bytes larger than the manifest says, restored it, and the path unit patched
it straight back - an update dialog that begins again every few seconds and
never finishes. The same client started from the menu was fine, which is
what made it look like a problem of Steam's own.

The autostart entry now carries the switch as well. It follows the Steam
setting rather than the autostart one, because leaving it out while Steam is
patched is exactly what causes the loop.

That covers the entry that exists; a terminal or a script still starts Steam
without the switch. So the patch no longer fights back either: a script that
was patched before, is not patched now, and belongs to a client that is
still running has just been restored by Steam, and doing it again would only
have the two of them undoing each other. It waits for the next apply with
Steam closed - the helper is started once, at the start, so patching it now
would not have helped that session anyway.

Also: Steam does not checksum that script, it compares sizes - the log says
"Verifying file sizes only" - while the comments and the documentation
claimed otherwise. And the status screen now tells a patch that is waiting
from one that is missing, instead of reporting both as not patched yet.
This commit is contained in:
Felitendo committed 2026-08-24 10:01:27 +02:00
1 parent 4004023297
commit f42d80598f
9 files changed
+135 -23

No files matched your search

+6 -1
View File
@@ -107,7 +107,12 @@ mca_apply() {
mca_steam_apply
fi
[[ $CFG_AUTOSTART == yes ]] && mca_autostart_apply
# Steam's autostart entry carries Steam's own switch and follows the Steam
# setting, not this one - leaving it out while Steam is patched is what puts
# the client in an update loop - so both are checked inside.
if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then
mca_autostart_apply
fi
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
mca_prune_orphans
+9 -1
View File
@@ -239,6 +239,14 @@ mca_ledger_forget() {
}
# mca_backup_name <file>
# The name a backup copy of that file is stored under. Derived from the path
# rather than remembered, so a caller can ask whether a file has been backed up
# before without reading the ledger.
mca_backup_name() {
printf '%s' "$1" | sed 's|/|%|g'
}
# mca_backup <file>
# Copies a file aside before it is edited in place, and prints the name the
# copy was stored under. Existing backups are never overwritten: the first copy
@@ -246,7 +254,7 @@ mca_ledger_forget() {
# patched version.
mca_backup() {
local file="$1" name
name="$(printf '%s' "$file" | sed 's|/|%|g')"
name="$(mca_backup_name "$file")"
mkdir -p "$MCA_BACKUPDIR" 2>/dev/null || return 1
if [[ ! -e "$MCA_BACKUPDIR/$name" ]]; then
+3
View File
@@ -178,6 +178,9 @@ mca_ui_status() {
if mca_steam_installed; then
if mca_steam_patched; then
_mca_row "$(mca_msg "Steam")" "$(_mca_onoff yes)"
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]] && mca_steam_waiting; then
_mca_row "$(mca_msg "Steam")" \
"${MCA_C_DIM}$(mca_msg "waiting until Steam is closed")${MCA_C_RESET}"
elif [[ $CFG_STEAM == yes && $CFG_ENABLED == yes ]]; then
_mca_row "$(mca_msg "Steam")" \
"${MCA_C_YELLOW}$(mca_msg "not patched yet")${MCA_C_RESET}"
+15 -2
View File
@@ -345,6 +345,12 @@ mca_desktop_apply() {
# ~/.config/autostart, pointing straight at their binary. Those bypass the
# desktop entry in the menu completely, which is why Discord launched at login
# used to behave differently from Discord launched from the menu.
#
# Steam is here too, even though it is not a Chromium process itself: its entry
# needs -noverifyfiles exactly like the menu one, and it is the entry most
# likely to exist, because Steam writes it as soon as "run at startup" is
# ticked. Without it, a Steam started at login restores the patched web helper
# script and the watcher patches it back, over and over.
mca_autostart_apply() {
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
@@ -360,9 +366,16 @@ mca_autostart_apply() {
mca_exec_program "$exec_line" || continue
prog="$MCA_PROG"
mca_is_chromium "$prog" || continue
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
if [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
[[ $CFG_STEAM == yes ]] || continue
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
"$MCA_STEAM_LAUNCH_FLAG" after-program)"
else
[[ $CFG_AUTOSTART == yes ]] || continue
mca_is_chromium "$prog" || continue
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
fi
[[ -n $content ]] || continue
[[ "$content" == "$(< "$file")" ]] && continue
+60 -5
View File
@@ -9,11 +9,19 @@
#
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
#
# Steam checksums that script at every start and restores it when it differs,
# which is why the launcher gets -noverifyfiles as well. The trade-off is real
# and belongs to the user, so this is a switch of its own rather than part of
# the general application handling: with verification off, Steam no longer
# repairs a damaged installation by itself.
# Steam compares the installed files against its manifest at every start - by
# size, not by content - and restores whatever differs, which is why the
# launcher gets -noverifyfiles as well. The trade-off is real and belongs to
# the user, so this is a switch of its own rather than part of the general
# application handling: with verification off, Steam no longer repairs a
# damaged installation by itself.
#
# Every way of starting Steam has to carry that switch, or the two programs
# spend the session undoing each other: Steam restores the script, the watcher
# patches it again, Steam restores it again, and the client never gets past its
# update dialog. So the autostart entry gets it as well as the menu one (see
# mca_autostart_apply), and the patch below stays out of the way of a client
# that is already running.
#
# The file comes back on every client update, and the watcher re-applies the
# patch when that happens.
@@ -64,6 +72,23 @@ mca_steam_installed() {
return 1
}
# mca_steam_running
# Steam records its own process id beside its installation while it runs. The
# file outlives a crash, so the id is checked rather than believed.
mca_steam_running() {
local f pid
for f in \
"$HOME/.steam/steam.pid" \
"$HOME/.var/app/com.valvesoftware.Steam/.steam/steam.pid"
do
[[ -r $f ]] || continue
pid="$(< "$f")"
[[ $pid =~ ^[0-9]+$ ]] || continue
kill -0 "$pid" 2>/dev/null && return 0
done
return 1
}
# mca_steam_script_patched <script>
mca_steam_script_patched() {
grep -q -- "$MCA_FEATURE" "$1" 2>/dev/null
@@ -78,6 +103,26 @@ mca_steam_patched() {
return 1
}
# mca_steam_deferred <script>
# Whether the patch is being held back rather than simply missing: the script
# was patched before, its own copy is back, and Steam is still running. See
# mca_steam_apply for why that is left alone.
mca_steam_deferred() {
[[ -e "$MCA_BACKUPDIR/$(mca_backup_name "$1")" ]] && mca_steam_running
}
# mca_steam_waiting
# The same question for the status screen, which has no script in hand.
mca_steam_waiting() {
local root script
while IFS= read -r root; do
script="$(mca_steam_script "$root")" || continue
mca_steam_script_patched "$script" && continue
mca_steam_deferred "$script" && return 0
done < <(mca_steam_roots)
return 1
}
# _mca_steam_exec_line <script>
# The line number of the command that starts the web helper: the last
# uncommented line that both names steamwebhelper and forwards "$@". Both the
@@ -107,6 +152,16 @@ mca_steam_apply() {
mca_steam_script_patched "$script" && continue
# Not patched now, but patched before: Steam has just put its own copy
# back. Patching it again while the client watches is what turns one
# size mismatch into an endless update dialog, and it would not help
# this session anyway - the helper is started once, at the start. The
# patch waits for the next apply with Steam closed.
if mca_steam_deferred "$script"; then
mca_note "$(mca_msg "Steam is running and has put its own file back; the change waits until Steam is closed.")"
continue
fi
lineno="$(_mca_steam_exec_line "$script")"
if [[ -z $lineno ]]; then
# A client update changed how the helper is started. Leaving the