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, 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