-
-
Save krisutofu/ca252cc4732acb9ae6b7e0f6a1c11b52 to your computer and use it in GitHub Desktop.
#!/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=$(( 8 )); # 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:]]' | sed -E -e 's;^\s*;;g' | 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}; |
Update: I added two options. 1) the fraction of snapshots (in %) to be removed at most. 2) the interval of simultaneously removed snapshots (in this default case, 3 snapshots are deleted at once before synchronizing).
I also enhanced the echoed messages at the end of the script.
I tested the new script with $RANDOM
values.
Update: Prevent the loop from deleting the last snapshot. I also ensured that at least one snapshot is removed.
It now avoids useless recomputations of available space.
It won't tell you the available space twice anymore and only tell you to re-run when it would have an effect.
Changes have been tested with $RANDOM
computed space.
Update: BTRFS sync was bypassed when leaving the loop, due to the post-decrement operator in the loop condition. Defect is fixed.
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.
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).
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.
Fetching the snapshotnumbers has not worked properly anymore. Either it's because of reaching snapshot 1000 or bash was updated with breaking changes. Therefore, the initial whitespace of each line in the table output is removed in the extraction of the snapshot numbers.
Update: fixed the bug which I introduced when I added the btrfs sync command recently using wrong syntax. "btrfs filesystem sync " (initiates removal) and "btrfs subvolume sync " (also waits for removal according to docs) are permitted.