fix: make the Steam patch survive Steam's own file check
Steam's autoscroll worked, except when it didn't. Every report of that came down to the same thing: Steam started in a way that carried no -noverifyfiles, noticed the patched web helper script, put its own copy back, and spent the session without the flag. There is no way to make every possible way of starting Steam carry an argument - a game launcher, a shortcut, a terminal, another program calling `steam steam://` - so the patch stops depending on it. The check is on size and timestamp, not on content, so the patch is now written to look untouched: the bytes the flag costs come back out of the script's own comments, and the timestamp of the untouched copy is restored afterwards. The file Steam finds is exactly as long and exactly as old as the one it wrote. A client that verifies its files finds nothing to repair. -noverifyfiles stays as the fallback for a script with no comments left to pay for the flag. Two things fall out of that. A patch that keeps the size no longer has to wait for Steam to close, because there is no size mismatch for the client to chase; only the growing one still defers. And an installation patched by the earlier version is quietly redone at the original size on the next apply, so the fix arrives without anyone having to know about it. Then the launch paths that were never covered: - Shortcuts on the desktop itself. Nothing in the XDG search path looks at that folder, so nothing had ever seen them - and Steam writes one there for every game somebody asks for a shortcut to. Its name is translated, so it is read from user-dirs.dirs and handed to the watcher in a drop-in. - Steam as a Flatpak was never recognised as Steam, so its entry got nothing while its script got patched - the worst of both. The switch goes after the application id there, where flatpak passes it on rather than reading it. - steam-native and steam-jupiter, which are ordinary Steam starts under another name, and Flatpak and snap entries in ~/.config/autostart. Two things found on the way: a client update left the undo copy holding the script from before the update, so undoing would have put an old version back; and a shortcut edited in place and later deleted would have been recreated by `disable`.
This commit is contained in:
1 parent
ced6f57dfd
commit
b7e0ff6fd1
9 files changed
+458
-115
No files matched your search
+56
-2
@@ -93,7 +93,7 @@ mca_apply() {
|
||||
mca_desktop_apply "$id" "$file"
|
||||
;;
|
||||
steam)
|
||||
mca_steam_desktop_apply "$id" "$file"
|
||||
mca_steam_desktop_apply "$id" "$file" "$packaging"
|
||||
if (( ! steam_done )); then
|
||||
mca_steam_apply
|
||||
steam_done=1
|
||||
@@ -115,7 +115,8 @@ mca_apply() {
|
||||
# autoscroll for the rest of the session.
|
||||
if [[ $CFG_STEAM == yes ]]; then
|
||||
for i in "${!MCA_STEAM_LINKS[@]}"; do
|
||||
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" "${MCA_STEAM_LINK_FILES[i]}"
|
||||
mca_steam_desktop_apply "${MCA_STEAM_LINKS[i]}" \
|
||||
"${MCA_STEAM_LINK_FILES[i]}" "${MCA_STEAM_LINK_PACK[i]}"
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -125,6 +126,14 @@ mca_apply() {
|
||||
if [[ $CFG_AUTOSTART == yes || $CFG_STEAM == yes ]]; then
|
||||
mca_autostart_apply
|
||||
fi
|
||||
|
||||
# Shortcuts on the desktop itself. Nothing above has seen them - the XDG
|
||||
# search path does not go there - and a game started from one starts Steam
|
||||
# without its switch, which is the difference between autoscroll working
|
||||
# and autoscroll working most of the time. Each entry is gated on its own,
|
||||
# so there is nothing to check out here.
|
||||
mca_shortcuts_apply
|
||||
|
||||
[[ $CFG_SPOTIFY == yes ]] && mca_spotify_apply
|
||||
|
||||
mca_prune_orphans
|
||||
@@ -161,6 +170,12 @@ mca_revert() {
|
||||
MCA_UNIT_PATH="middleclick-autoscroll.path"
|
||||
MCA_UNIT_SERVICE="middleclick-autoscroll.service"
|
||||
|
||||
# The desktop folder is watched like every other directory an entry can turn up
|
||||
# in, but its name is translated and the unit that ships with the package
|
||||
# cannot know it. So that one path is written here, into a drop-in, from the
|
||||
# name this system actually uses.
|
||||
MCA_UNIT_DROPIN="${MCA_XDG_CONFIG}/systemd/user/${MCA_UNIT_PATH}.d"
|
||||
|
||||
mca_watch_available() {
|
||||
mca_have systemctl && [[ -n ${XDG_RUNTIME_DIR:-} || -S "/run/user/$(id -u)/bus" ]]
|
||||
}
|
||||
@@ -170,17 +185,56 @@ mca_watch_enabled() {
|
||||
systemctl --user is-enabled --quiet "$MCA_UNIT_PATH" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_watch_dropin_write
|
||||
# Returns 0 when the drop-in changed and systemd has to be told, 1 when it was
|
||||
# already right - the same contract as mca_write_if_changed, and for the same
|
||||
# reason: the watcher runs after every desktop entry that appears anywhere on
|
||||
# the system, and a daemon-reload each time would be absurd.
|
||||
_mca_watch_dropin_write() {
|
||||
local dir file="$MCA_UNIT_DROPIN/desktop.conf" content
|
||||
|
||||
dir="$(mca_desktop_folder)"
|
||||
[[ -d $dir ]] || { _mca_watch_dropin_remove; return $?; }
|
||||
|
||||
# A per cent sign starts a specifier in a unit file and has to be doubled
|
||||
# to mean itself. Rare in a folder name, fatal when it happens.
|
||||
content="[Path]"$'\n'"PathModified=${dir//%/%%}"$'\n'
|
||||
|
||||
mca_write_if_changed "$file" "$content"
|
||||
}
|
||||
|
||||
_mca_watch_dropin_remove() {
|
||||
[[ -e "$MCA_UNIT_DROPIN/desktop.conf" ]] || return 1
|
||||
rm -f -- "$MCA_UNIT_DROPIN/desktop.conf"
|
||||
rmdir "$MCA_UNIT_DROPIN" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
mca_watch_enable() {
|
||||
mca_watch_available || return 1
|
||||
_mca_watch_dropin_write
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user enable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || return 1
|
||||
systemctl --user enable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_watch_refresh
|
||||
# Keeps a running watcher in step with a desktop folder that has been renamed,
|
||||
# which is what a change of session language does to it.
|
||||
mca_watch_refresh() {
|
||||
mca_watch_available || return 0
|
||||
_mca_watch_dropin_write || return 0
|
||||
systemctl --user daemon-reload > /dev/null 2>&1 || true
|
||||
systemctl --user restart "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
return 0
|
||||
}
|
||||
|
||||
mca_watch_disable() {
|
||||
mca_watch_available || return 0
|
||||
systemctl --user disable --now "$MCA_UNIT_PATH" > /dev/null 2>&1 || true
|
||||
systemctl --user disable "$MCA_UNIT_SERVICE" > /dev/null 2>&1 || true
|
||||
_mca_watch_dropin_remove && systemctl --user daemon-reload > /dev/null 2>&1
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,29 @@ MCA_BACKUPDIR="${MCA_STATEDIR}/backup"
|
||||
# else. See mca_ledger_add.
|
||||
MCA_LEDGER="${MCA_STATEDIR}/ledger"
|
||||
|
||||
# The folder the desktop itself shows. Nothing in the XDG search path looks at
|
||||
# it, so an entry that lives only there is invisible to a scan - and a game
|
||||
# shortcut dragged onto the desktop is exactly that.
|
||||
#
|
||||
# Its name is translated: Schreibtisch, Bureau, Escritorio. The name in use is
|
||||
# in the file xdg-user-dirs writes, so it is read rather than guessed, and
|
||||
# ~/Desktop is only the fallback for a system that has no such file.
|
||||
mca_desktop_folder() {
|
||||
local file="$MCA_XDG_CONFIG/user-dirs.dirs" line dir=''
|
||||
|
||||
if [[ -r $file ]]; then
|
||||
while IFS= read -r line; do
|
||||
[[ $line == XDG_DESKTOP_DIR=* ]] || continue
|
||||
dir="${line#XDG_DESKTOP_DIR=}"
|
||||
dir="${dir%\"}"; dir="${dir#\"}"
|
||||
done < "$file"
|
||||
dir="${dir/#\$HOME/$HOME}"
|
||||
fi
|
||||
|
||||
[[ $dir == /* ]] || dir="$HOME/Desktop"
|
||||
printf '%s\n' "${dir%/}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Translations
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
+32
-17
@@ -99,17 +99,6 @@ mca_desktop_dirs() {
|
||||
)
|
||||
}
|
||||
|
||||
# mca_desktop_get <file> <key>
|
||||
# A single value from the [Desktop Entry] group. Desktop Action groups repeat
|
||||
# the same keys, and reading past the first group would pick up the wrong one.
|
||||
mca_desktop_get() {
|
||||
local file="$1" key="$2"
|
||||
awk -v key="$key" '
|
||||
/^[[:space:]]*\[/ { inentry = ($0 ~ /^[[:space:]]*\[Desktop Entry\][[:space:]]*$/); next }
|
||||
inentry && index($0, key "=") == 1 { print substr($0, length(key) + 2); exit }
|
||||
' "$file" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_desktop_read <file>
|
||||
# Every key the scan needs, in one pass and without a single fork. There are a
|
||||
# couple of hundred desktop entries on an ordinary system, and doing this with
|
||||
@@ -231,8 +220,22 @@ mca_exec_is_steam_link() {
|
||||
local line="$1" prog="$2"
|
||||
|
||||
[[ $line == *steam://* ]] || return 1
|
||||
mca_prog_is_steam "$prog"
|
||||
}
|
||||
|
||||
# mca_prog_is_steam <program>
|
||||
# Whether running this program starts the Steam client. Every packaging is in
|
||||
# here and every name Valve and the distributions give the launcher, because
|
||||
# the answer decides whether an entry gets Steam's own switch - and an entry
|
||||
# that starts Steam without it undoes the web helper patch on the way up.
|
||||
#
|
||||
# The program is what a scan leaves behind: an absolute path for a native
|
||||
# install, flatpak:<id> or snap:<name> for the other two.
|
||||
mca_prog_is_steam() {
|
||||
local prog="$1"
|
||||
|
||||
case "${prog##*/}" in
|
||||
steam|steam-runtime) return 0 ;;
|
||||
steam|steam-runtime|steam-native|steam-jupiter) return 0 ;;
|
||||
esac
|
||||
[[ $prog == flatpak:com.valvesoftware.Steam || $prog == snap:steam ]]
|
||||
}
|
||||
@@ -580,6 +583,8 @@ MCA_PACKAGING=() # native | flatpak | snap
|
||||
# kept at all: the Steam module gives them Steam's own switch.
|
||||
MCA_STEAM_LINKS=() # desktop file id
|
||||
MCA_STEAM_LINK_FILES=() # the entry that is in effect for it
|
||||
MCA_STEAM_LINK_PACK=() # native | flatpak | snap, which decides where the
|
||||
# switch goes on the command line
|
||||
|
||||
# A scan reads every desktop entry on the system, so the menu does it once and
|
||||
# then redraws from what it found. Applying rescans on its own, so nothing else
|
||||
@@ -598,7 +603,7 @@ mca_scan() {
|
||||
|
||||
MCA_IDS=(); MCA_FILES=(); MCA_NAMES=(); MCA_PROGS=(); MCA_KINDS=()
|
||||
MCA_PACKAGING=()
|
||||
MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=()
|
||||
MCA_STEAM_LINKS=(); MCA_STEAM_LINK_FILES=(); MCA_STEAM_LINK_PACK=()
|
||||
|
||||
# Pass one: read the entries and work out what each of them starts. No
|
||||
# detection yet - that needs a stat per program, and those are collected so
|
||||
@@ -641,6 +646,13 @@ mca_scan() {
|
||||
if mca_exec_is_steam_link "$exec_line" "$prog"; then
|
||||
MCA_STEAM_LINKS+=("$id")
|
||||
MCA_STEAM_LINK_FILES+=("$file")
|
||||
if [[ $prog == flatpak:* ]]; then
|
||||
MCA_STEAM_LINK_PACK+=(flatpak)
|
||||
elif [[ $prog == snap:* ]]; then
|
||||
MCA_STEAM_LINK_PACK+=(snap)
|
||||
else
|
||||
MCA_STEAM_LINK_PACK+=(native)
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -664,10 +676,13 @@ mca_scan() {
|
||||
packaging=native
|
||||
|
||||
if [[ $prog == flatpak:* ]]; then
|
||||
prog="${prog#flatpak:}"
|
||||
packaging=flatpak
|
||||
mca_flatpak_is_chromium "$prog" \
|
||||
&& { (( c_browser[i] )) && kind=browser || kind=app; }
|
||||
if mca_prog_is_steam "$prog"; then
|
||||
kind=steam
|
||||
elif mca_flatpak_is_chromium "${prog#flatpak:}"; then
|
||||
(( c_browser[i] )) && kind=browser || kind=app
|
||||
fi
|
||||
prog="${prog#flatpak:}"
|
||||
elif [[ $prog == snap:* ]]; then
|
||||
prog="${prog#snap:}"
|
||||
packaging=snap
|
||||
@@ -676,7 +691,7 @@ mca_scan() {
|
||||
elif mca_snap_is_chromium "$prog"; then
|
||||
(( c_browser[i] )) && kind=browser || kind=app
|
||||
fi
|
||||
elif [[ ${prog##*/} == steam || ${prog##*/} == steam-runtime ]]; then
|
||||
elif mca_prog_is_steam "$prog"; then
|
||||
# Steam is Chromium inside, but nothing about it can be changed
|
||||
# from a command line argument; it has its own module.
|
||||
kind=steam
|
||||
|
||||
+114
-35
@@ -349,51 +349,117 @@ mca_desktop_apply() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Autostart entries
|
||||
# Entries outside the search path
|
||||
# ---------------------------------------------------------------------------
|
||||
# Applications that start themselves at login write their own entry into
|
||||
# ~/.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.
|
||||
# Two directories hold desktop entries that no part of the XDG search path
|
||||
# looks at, so nothing the scan does can reach them:
|
||||
#
|
||||
# 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.
|
||||
# ~/.config/autostart, where applications that start themselves at login
|
||||
# write their own entry, pointing straight at their binary. Those bypass the
|
||||
# entry in the menu completely, which is why Discord launched at login used
|
||||
# to behave differently from Discord launched from the menu.
|
||||
#
|
||||
# The desktop folder, where a shortcut somebody dragged out of the menu -
|
||||
# or asked Steam for - lives and nowhere else.
|
||||
#
|
||||
# Steam is in both, even though it is not a Chromium process itself: its
|
||||
# entries need -noverifyfiles exactly like the menu one. The autostart entry is
|
||||
# the one 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. The desktop
|
||||
# ones are the gap behind "autoscroll works, except sometimes": starting a game
|
||||
# from the desktop is a Steam start like any other, and a Steam start without
|
||||
# the switch costs the interface its autoscroll for the rest of the session.
|
||||
#
|
||||
# Neither can be shadowed from anywhere, so both are edited where they stand,
|
||||
# with the original kept.
|
||||
|
||||
# _mca_entry_patch_inplace <file> <gate: autostart|apps>
|
||||
# One desktop entry that lives outside the XDG search path, edited where it is
|
||||
# because there is nowhere to shadow it from.
|
||||
#
|
||||
# Anything that starts Steam gets Steam's own switch; a Chromium application
|
||||
# gets the flag. What decides whether a Chromium application here is in scope
|
||||
# differs by where the entry came from, which is what the gate says: an
|
||||
# autostart entry follows the autostart setting, a shortcut follows the same
|
||||
# rules as the application it is a shortcut to.
|
||||
_mca_entry_patch_inplace() {
|
||||
local file="$1" gate="$2"
|
||||
local id prog packaging=native content backup kind
|
||||
|
||||
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && return 0
|
||||
|
||||
_mca_desktop_read "$file"
|
||||
[[ -n $DE_EXEC ]] || return 0
|
||||
[[ $DE_HIDDEN == true ]] && return 0
|
||||
|
||||
mca_exec_program "$DE_EXEC" || return 0
|
||||
prog="$MCA_PROG"
|
||||
|
||||
# A Flatpak or a snap entry names the wrapper rather than the application.
|
||||
# The scan resolves those into an id; outside the scan the same has to
|
||||
# happen here, or every Flatpak looks like the flatpak program itself.
|
||||
if [[ ${prog##*/} == flatpak ]]; then
|
||||
mca_exec_flatpak_id "$DE_EXEC" || return 0
|
||||
prog="flatpak:$MCA_PROG"
|
||||
packaging=flatpak
|
||||
elif mca_snap_name "$prog"; then
|
||||
prog="snap:$MCA_PROG"
|
||||
packaging=snap
|
||||
fi
|
||||
|
||||
if mca_prog_is_steam "$prog"; then
|
||||
[[ $CFG_STEAM == yes ]] || return 0
|
||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE" \
|
||||
"$MCA_STEAM_LAUNCH_FLAG" "$(mca_steam_flag_position "$packaging")")"
|
||||
else
|
||||
case "$packaging" in
|
||||
flatpak) mca_flatpak_is_chromium "${prog#flatpak:}" || return 0 ;;
|
||||
snap) mca_snap_is_chromium "${prog#snap:}" || return 0 ;;
|
||||
*) mca_is_chromium "$prog" || return 0 ;;
|
||||
esac
|
||||
|
||||
if [[ $gate == autostart ]]; then
|
||||
[[ $CFG_AUTOSTART == yes ]] || return 0
|
||||
else
|
||||
id="${file##*/}"; id="${id%.desktop}"
|
||||
mca_desktop_is_browser && kind=browser || kind=app
|
||||
mca_kind_wanted "$kind" "$id" "$packaging" || return 0
|
||||
fi
|
||||
|
||||
content="$(_mca_desktop_transform "$file" "$MCA_MARK_INPLACE")"
|
||||
fi
|
||||
|
||||
[[ -n $content ]] || return 0
|
||||
[[ "$content" == "$(< "$file")" ]] && return 0
|
||||
|
||||
backup="$(mca_backup "$file")" || return 0
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "$backup"
|
||||
}
|
||||
|
||||
mca_autostart_apply() {
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file exec_line prog content backup
|
||||
local dir="$MCA_XDG_CONFIG/autostart" file
|
||||
|
||||
[[ -d $dir ]] || return 0
|
||||
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
grep -q "^$MCA_MARK_INPLACE=" "$file" 2>/dev/null && continue
|
||||
_mca_entry_patch_inplace "$file" autostart
|
||||
done
|
||||
}
|
||||
|
||||
exec_line="$(mca_desktop_get "$file" Exec)"
|
||||
[[ -n $exec_line ]] || continue
|
||||
mca_shortcuts_apply() {
|
||||
local dir file
|
||||
dir="$(mca_desktop_folder)"
|
||||
|
||||
mca_exec_program "$exec_line" || continue
|
||||
prog="$MCA_PROG"
|
||||
[[ -d $dir ]] || return 0
|
||||
|
||||
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
|
||||
|
||||
backup="$(mca_backup "$file")" || continue
|
||||
if mca_write_if_changed "$file" "$content"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
mca_ledger_add inplace "$file" "$backup"
|
||||
for file in "$dir"/*.desktop; do
|
||||
[[ -f $file ]] || continue
|
||||
_mca_entry_patch_inplace "$file" apps
|
||||
done
|
||||
}
|
||||
|
||||
@@ -446,13 +512,15 @@ mca_spotify_apply() {
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# mca_prune_orphans
|
||||
# Removes generated entries whose application is gone.
|
||||
# Forgets changes there is no longer anything to undo.
|
||||
#
|
||||
# Uninstalling something deletes its entry from /usr/share/applications, but the
|
||||
# copy shadowing it is in the user's home and pacman knows nothing about it. It
|
||||
# would sit in the menu forever, offering to start a program that is no longer
|
||||
# installed - and the watcher would not notice, because a plain apply only ever
|
||||
# looks at what is there now.
|
||||
# looks at what is there now. A file that was edited where it stood and has
|
||||
# since been deleted is the same problem from the other side: its record would
|
||||
# have `disable` put the file back.
|
||||
mca_prune_orphans() {
|
||||
local kind path source line
|
||||
local -a lines=()
|
||||
@@ -462,6 +530,17 @@ mca_prune_orphans() {
|
||||
|
||||
for line in "${lines[@]}"; do
|
||||
IFS=$'\t' read -r kind path source <<< "$line"
|
||||
|
||||
# An entry that was edited where it stood and has since been deleted -
|
||||
# a game shortcut dragged to the wastebasket, an autostart entry the
|
||||
# application removed. There is nothing left to put back, and keeping
|
||||
# the record would have `disable` recreate a file the user threw away.
|
||||
if [[ $kind == inplace && ! -e $path ]]; then
|
||||
[[ -n $source ]] && rm -f -- "$MCA_BACKUPDIR/$source"
|
||||
mca_ledger_forget "$path"
|
||||
continue
|
||||
fi
|
||||
|
||||
[[ $kind == shadow && -n $source ]] || continue
|
||||
[[ -e $source ]] && continue
|
||||
|
||||
|
||||
+175
-38
@@ -10,18 +10,27 @@
|
||||
# ~/.local/share/Steam/ubuntu12_64/steamwebhelper_sniper_wrap.sh
|
||||
#
|
||||
# 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.
|
||||
# size and timestamp rather than by content - and restores whatever differs.
|
||||
# So the patch is written to look untouched: the bytes the flag costs are taken
|
||||
# back out of the script's comments and the timestamp is put back afterwards,
|
||||
# which leaves a file that is byte-for-byte the size Steam recorded and as old
|
||||
# as Steam left it. A client that verifies its files finds nothing to repair
|
||||
# and the flag survives however Steam was started - from the menu, from a game
|
||||
# shortcut, from a launcher like Heroic, from a terminal.
|
||||
#
|
||||
# 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.
|
||||
# That is the part that has to work, because there is no way to make every
|
||||
# possible way of starting Steam carry an argument. -noverifyfiles on the
|
||||
# launcher entries is the second line rather than the first: it covers the case
|
||||
# where the script has no comments left to pay for the flag and the patch has
|
||||
# to grow the file. The trade-off is real and belongs to the user, so it 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.
|
||||
#
|
||||
# A patch that does change the size still has to stay out of the way of a
|
||||
# client that is already running, 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. A patch
|
||||
# that keeps the size is invisible to that check and goes in either way.
|
||||
#
|
||||
# The file comes back on every client update, and the watcher re-applies the
|
||||
# patch when that happens.
|
||||
@@ -145,12 +154,112 @@ _mca_steam_exec_line() {
|
||||
' "$1" 2>/dev/null
|
||||
}
|
||||
|
||||
# _mca_steam_build <script> <flags> <reclaim: 0|1>
|
||||
# The patched script on stdout: the flags appended to the line that starts the
|
||||
# web helper, and - when asked for - the same number of bytes taken back out of
|
||||
# the script's comments, so that the file Steam finds is the length Steam wrote
|
||||
# down. Everything else - setting up the container runtime, deciding whether
|
||||
# the sandbox can be used - is left alone.
|
||||
#
|
||||
# Comments are eaten from the bottom up, so the header that says what the
|
||||
# script is for is the last thing to lose anything, and the '#' itself always
|
||||
# stays: a line that loses it stops being a comment and starts being a command.
|
||||
# Fails when the comments are too short to pay for the flags.
|
||||
_mca_steam_build() {
|
||||
local script="$1" flags="$2" reclaim="$3" lineno need=0
|
||||
|
||||
lineno="$(_mca_steam_exec_line "$script")"
|
||||
[[ -n $lineno ]] || return 1
|
||||
|
||||
if (( reclaim )); then
|
||||
need="$(printf '%s' " $flags" | wc -c)"
|
||||
need="${need//[^0-9]/}"
|
||||
fi
|
||||
|
||||
# LC_ALL=C so awk counts bytes: a comment with an accent in it is shorter
|
||||
# in characters than it is on disk, and it is the disk that Steam measures.
|
||||
LC_ALL=C awk -v n="$lineno" -v f=" $flags" -v need="$need" '
|
||||
{ lines[NR] = (NR == n ? $0 f : $0) }
|
||||
END {
|
||||
for (i = NR; i >= 2 && need > 0; i--) {
|
||||
if (i == n) continue
|
||||
p = index(lines[i], "#")
|
||||
if (p == 0) continue
|
||||
# Only a line that is nothing but a comment; a trailing one
|
||||
# sits behind a command that has to stay intact.
|
||||
if (substr(lines[i], 1, p - 1) ~ /[^ \t]/) continue
|
||||
can = length(lines[i]) - p
|
||||
if (can <= 0) continue
|
||||
if (can > need) can = need
|
||||
lines[i] = substr(lines[i], 1, length(lines[i]) - can)
|
||||
need -= can
|
||||
}
|
||||
if (need > 0) exit 1
|
||||
for (i = 1; i <= NR; i++) print lines[i]
|
||||
}
|
||||
' "$script"
|
||||
}
|
||||
|
||||
# _mca_steam_same_size <script> <content>
|
||||
# Whether writing that content leaves the file at the size it has now. Measured
|
||||
# rather than worked out: a script without a final newline gains one on the way
|
||||
# through awk, and one byte is all it takes for Steam to notice.
|
||||
_mca_steam_same_size() {
|
||||
local script="$1" content="$2"
|
||||
(( $(wc -c < "$script") == $(printf '%s\n' "$content" | wc -c) ))
|
||||
}
|
||||
|
||||
# _mca_steam_refresh_backup <script>
|
||||
# A client update brings a new version of the script, and the copy kept from
|
||||
# before it is no longer what putting it back means. Only ever done while the
|
||||
# script is unpatched, so the copy that replaces it is Steam's own.
|
||||
_mca_steam_refresh_backup() {
|
||||
local script="$1" name
|
||||
name="$(mca_backup_name "$script")"
|
||||
|
||||
[[ -e "$MCA_BACKUPDIR/$name" ]] || return 0
|
||||
cmp -s -- "$script" "$MCA_BACKUPDIR/$name" && return 0
|
||||
cp -p -- "$script" "$MCA_BACKUPDIR/$name" 2>/dev/null || true
|
||||
}
|
||||
|
||||
# _mca_steam_reneutralise <script> <flags>
|
||||
# An earlier version of this program patched by appending and left the script
|
||||
# longer than Steam expects, which is a patch that only survives because
|
||||
# -noverifyfiles is there to stop Steam looking. If the same flags can be made
|
||||
# to fit, they are: the file goes back to the length and the timestamp Steam
|
||||
# wrote down and stops depending on that switch.
|
||||
#
|
||||
# The new copy is built from the untouched original that was kept when the
|
||||
# script was first patched, never from the patched file - patching a patched
|
||||
# file is how a flag ends up in there twice.
|
||||
_mca_steam_reneutralise() {
|
||||
local script="$1" flags="$2" name copy out
|
||||
|
||||
name="$(mca_backup_name "$script")"
|
||||
copy="$MCA_BACKUPDIR/$name"
|
||||
[[ -f $copy ]] || return 1
|
||||
|
||||
# Already the length Steam expects: there is nothing to gain.
|
||||
(( $(wc -c < "$script") == $(wc -c < "$copy") )) && return 1
|
||||
|
||||
out="$(_mca_steam_build "$copy" "$flags" 1)" || return 1
|
||||
_mca_steam_same_size "$copy" "$out" || return 1
|
||||
|
||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
touch -r "$copy" -- "$script" 2>/dev/null || true
|
||||
mca_ledger_add steam "$script" "$name"
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_steam_apply
|
||||
# Appends the flags to the line that starts the web helper. Everything else in
|
||||
# the script - setting up the container runtime, deciding whether the sandbox
|
||||
# can be used - is left alone.
|
||||
# Puts the flags into every Steam installation that has the script, preferring
|
||||
# the patch that keeps the file's size and falling back to the one that does
|
||||
# not. Returns 1 when there is no Steam to patch at all.
|
||||
mca_steam_apply() {
|
||||
local root script backup flags lineno found=0 out
|
||||
local root script backup flags found=0 out neutral
|
||||
|
||||
flags="$(mca_flags)"
|
||||
|
||||
@@ -158,34 +267,51 @@ mca_steam_apply() {
|
||||
script="$(mca_steam_script "$root")" || continue
|
||||
found=1
|
||||
|
||||
mca_steam_script_patched "$script" && continue
|
||||
if mca_steam_script_patched "$script"; then
|
||||
_mca_steam_reneutralise "$script" "$flags" || true
|
||||
continue
|
||||
fi
|
||||
|
||||
# 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_steam_refresh_backup "$script"
|
||||
|
||||
neutral=1
|
||||
if ! { out="$(_mca_steam_build "$script" "$flags" 1)" \
|
||||
&& _mca_steam_same_size "$script" "$out"; }
|
||||
then
|
||||
neutral=0
|
||||
out="$(_mca_steam_build "$script" "$flags" 0)" || {
|
||||
# A client update changed how the helper is started. Leaving
|
||||
# the file alone is the only safe answer: this script is what
|
||||
# starts Steam's entire interface.
|
||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||
continue
|
||||
}
|
||||
fi
|
||||
|
||||
# The patch has to grow the file, and it is not patched now but was
|
||||
# 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. A patch that keeps the size has
|
||||
# nothing to wait for.
|
||||
if (( ! neutral )) && 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
|
||||
# file alone is the only safe answer: this script is what starts
|
||||
# Steam's entire interface.
|
||||
mca_note "$(mca_msg "Steam starts its interface in a way this version does not recognise; leaving it alone.")"
|
||||
continue
|
||||
fi
|
||||
|
||||
out="$(awk -v n="$lineno" -v f=" $flags" 'NR == n { $0 = $0 f } { print }' "$script")" || continue
|
||||
|
||||
backup="$(mca_backup "$script")" || continue
|
||||
if mca_write_if_changed "$script" "$out"$'\n'; then
|
||||
MCA_CHANGES=$(( MCA_CHANGES + 1 ))
|
||||
fi
|
||||
chmod +x -- "$script" 2>/dev/null || true
|
||||
|
||||
# Steam looks at when the file was last written as well as at how big
|
||||
# it is, and the copy taken before the edit still carries the original
|
||||
# timestamp. Putting it back costs nothing and removes the other half
|
||||
# of what the client would notice.
|
||||
touch -r "$MCA_BACKUPDIR/$backup" -- "$script" 2>/dev/null || true
|
||||
|
||||
mca_ledger_add steam "$script" "$backup"
|
||||
done < <(mca_steam_roots)
|
||||
|
||||
@@ -214,11 +340,22 @@ mca_steam_revert() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# mca_steam_desktop_apply <id> <source entry>
|
||||
# mca_steam_flag_position <packaging>
|
||||
# Where Steam's own switch goes on a command line. Straight after the program
|
||||
# for a native or snap entry, because Steam reads its options before the
|
||||
# steam:// argument that the right-click actions pass. For a Flatpak the first
|
||||
# token is flatpak itself, which would take the switch for one of its own and
|
||||
# refuse to start, so there it goes at the end of the arguments - after the
|
||||
# application id, where flatpak hands everything on to Steam.
|
||||
mca_steam_flag_position() {
|
||||
[[ ${1:-native} == flatpak ]] && { printf 'before-fields\n'; return; }
|
||||
printf 'after-program\n'
|
||||
}
|
||||
|
||||
# mca_steam_desktop_apply <id> <source entry> [packaging]
|
||||
# Steam restores its own files at every start unless it is told not to verify
|
||||
# them, so the launcher entry carries that switch. It goes directly after the
|
||||
# program because Steam reads its options before the steam:// argument that the
|
||||
# right-click actions pass.
|
||||
# them, so the launcher entry carries that switch.
|
||||
mca_steam_desktop_apply() {
|
||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" after-program
|
||||
mca_desktop_apply "$1" "$2" "$MCA_STEAM_LAUNCH_FLAG" \
|
||||
"$(mca_steam_flag_position "${3:-native}")"
|
||||
}
|
||||
Reference in new issue
Block a user