Skip to content

Instantly share code, notes, and snippets.

@krisutofu
Last active June 20, 2024 13:30
Show Gist options
  • Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
Script that safely updates with Pacman without crashes when using btrfs and snapper
#!/bin/bash
# This bash script bypasses the crashing problem when using BTRFS and Snapper with Pacman. Useful as long as the Pacman
# space computation bug is not fixed.
# This script expects only a single mountpoint to be updated, and only the root config of snapper to be used.
# `bc` (basic calculator) needs to be installed which did NOT come by default with my Garuda Plasma installation.
if [ $(id -u) != 0 ]; then
exec sudo -s "$0" "$@";
fi
# does not work, it will not show the dialog for unknown reason, same with send-notify
true || {
notificationCommand='
if (( $exitCode )); then
kdialog --warningyesno "System Update: Snapshot cleanup aborted"'\''!'\'' "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." --yes-label "pacman -Scc" --no-label "cancel"
case $? in
0 ) pacman -Scc; exec sudo -s "'$0'" "'$@'" ;;
1 ) ;;
* ) ;;
esac
else
kdialog --passivepopup "System Update ready"'\''!'\''
fi
'
trap "$notificationCommand" EXIT # notify user when script finished to take action for pacman
}
# debugCommand=echo; # if you want to test this script without applying changes
updatedMountpoint="/";
snapperconfig="root";
syncronizationPace=$(( 3 )); # each synchronization takes long, this tells the script how many snapshots to delete at once before syncronization of BTRFS space
maxSnapshotPercentToRemove=$(( 50 )); # set here, how many oldest snapshots from the entire `snapper list` may be deleted at most by this script
minSnapshotsPreserved=$(( 12 )); # do not delete more snapshots if the number is less equal to this limit
maxSnapshotPercentToRemove=$(( $maxSnapshotPercentToRemove >= 100 ? 100 : $maxSnapshotPercentToRemove ));
computeSpaceExpression() {
echo "$*" | sed -E -e 's/GiB/*1024^3/g' -e 's/GB?/*1000^3/g' -e 's/MiB/*1024^2/g' -e 's/MB?/*1000^2/g' -e 's/KiB/*1024/g' -e 's/KB?/*1000/g' | bc -q;
}
if [ -n "${debugCommand+used}" ]; then
requiredSpaceThreshold='0';
else
requiredSpaceThreshold="$(computeSpaceExpression "200MiB")"; # safety gap margin in Bytes; minimum additional required space that must be available
fi
# min x, minimum x, at least x, require x, required x, > x, >= x is accepted as argument
if minArgument=$(echo "${*}" | pcre2grep -i -o1 '(?<=^|\s)(?:min(?:imum)|at least|required?|>=?) (\S+)' ) \
&& minArgument=$(computeSpaceExpression "$minArgument") \
&& minArgument=${minArgument%.*} \
&& (( $minArgument > ${requiredSpaceThreshold} ));
then
requiredSpaceThreshold=$minArgument;
fi
computeAvailableSpace() {
if [ -n "${debugCommand+used}" ]; then echo $(( $RANDOM + ${requiredSpaceThreshold} )); return 0; fi
# Using grep and cut on program output is fragile in general but there is no easy usable alternative in shell languages.
computeSpaceExpression "$(btrfs filesystem df "$updatedMountpoint" | grep 'Data, single:' | cut -d' ' -f3- | sed -E 's/total=(.*?),.*? used=(.*?)/\1-\2/')";
}
computeRequiredSpace() {
if [ -n "${debugCommand+used}" ]; then echo $(( $RANDOM + ${requiredSpaceThreshold} )); return 0; fi
# alternative to pacman -Qu: checkupdates (slow!)
computeSpaceExpression "$(pacman -Qu | cut -d' ' -f1 | xargs pacman -Si | grep 'Installed Size' | cut -d':' -f2 | tr '\n' '+' | tr ',' '.') 0";
}
isMoreThanSnapshotLimit() (( $(wc -w <<< "$*") >= ${minSnapshotsPreserved} ))
pacman -Sy > /dev/null; # should be automatically called when the system is updated
${debugCommand} btrfs subvolume sync "$updatedMountpoint"; # force update of BTRFS storage info
availableSpace=$(computeAvailableSpace);
availableSpace=${availableSpace%.*}; # availableSpace converted to int
requiredSpace=$(computeRequiredSpace);
requiredSpace=$((${requiredSpace%.*} + $requiredSpaceThreshold));
if (( $availableSpace >= $requiredSpace )); then
echo "enough space available ${availableSpace} = $(bc -q <<<"${availableSpace} / 1024^2") MiB > required space ${requiredSpace} = $(bc -q <<<"${requiredSpace} / 1024^2") MiB";
# all set, go to exit
else
snapshotNumbers=$(snapper list | grep '^ \?[[:digit:]]' | cut -d' ' -f1 | tr '\n' ' ');
toRemove=$(( $(wc -w <<< "$snapshotNumbers") * $maxSnapshotPercentToRemove / 100 ));
toRemove=$(( ($toRemove <= 0 && $maxSnapshotPercentToRemove > 0) ? 1 : $toRemove )); # as long as maxSnapshotPercentToRemove is set, do remove at least one snapshot
# remove groups of contiguous snapshots in steps until sufficient memory is available
while (( $availableSpace < $requiredSpace )) && (( --toRemove >= 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers";
do
echo "removing snapshot ${snapshotNumbers%% *}";
${debugCommand} snapper -c "$snapperconfig" delete ${snapshotNumbers%% *};
snapshotNumbers=${snapshotNumbers#* };
if (( ++removedCount % $syncronizationPace != 0 )) && (( toRemove >= 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers"; then
continue
fi
${debugCommand} btrfs subvolume sync "$updatedMountpoint";
availableSpace=$(computeAvailableSpace);
availableSpace=${availableSpace%.*};
done
if (( $availableSpace < $requiredSpace )); then
echo -e "Not enough space ${availableSpace} for system update ${requiredSpace}"'!!'
if (( $maxSnapshotPercentToRemove > 0 )) && isMoreThanSnapshotLimit "$snapshotNumbers"; then
echo "Run this script again to remove more snapshots";
elif (( isPaccacheCleanupAllowed )); then
echo "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." 2>&1
# kdialog --warningyesno "System Update: Snapshot cleanup aborted"'!' "Still require $(bc -q <<< "$(( requiredSpace - availableSpace )) / 1024^2" ) MiB." --yes-label "pacman -Scc" --no-label "cancel" # not working
# case $? in
# 0 ) pacman -Scc; exec sudo -s "'$0'" "'$@'" ;;
# 1 ) ;;
# * ) ;;
# esac
fi
exit ${exitCode:=1};
fi
echo -e "Enough space ${availableSpace} = $(bc -q <<<"${availableSpace} / 1024^2") MiB for system update ${requiredSpace} = $(bc -q <<<"${requiredSpace} / 1024^2") MiB available"'!'
fi
echo -e "Manually check the total size as depicted by Pacman"'!'"\n--------------------";
# kdialog --passivepopup "System Update ready"'!' # does not show anything and stops execution
exit ${exitCode:=0};
@krisutofu
Copy link
Author

Update: BTRFS sync was bypassed when leaving the loop, due to the post-decrement operator in the loop condition. Defect is fixed.

@krisutofu
Copy link
Author

Update: removed the garuda-update call at the end. Instead, an exit code is returned. Also added an argument which provides a minimum margin for the required space and integrated a debug variable which makes testing with random values very easy.

@krisutofu
Copy link
Author

krisutofu commented Mar 28, 2024

Space-related pacman crashes are still the same problem but 100MB of margin apparently do not suffice, Google fonts crashed during unpacking the download while being only little over the threshold for the total update.

Now, the minimum space argument is used for the threshold (margin), requiring at least that amount of free space also after the update (not just before it).

@krisutofu
Copy link
Author

Update, I reduced the default batch size of snapshots to be removed in one step. This number depends on how large snapshots are which depends on how often snapshots are made.

I also added a setting "minSnapshotsPreserved" which won't remove snapshots when there are equal or less snapshots available. This number also depends on how big the snapshots are.

I tried to add some notification when the script finished but it seems, kdialog or notify-send do not work here while it works in other smaller scripts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment