-
-
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+true}" ]; 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+true}" ]; 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\|Download 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}; |
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: to my delight, Garuda finally added an official memory check to garuda-update like my script is doing it. (It doesn't free snapshots however.) In addition to checking memory requirements, my script also removes some snapshots in multiples of n until the requirements are satisfied or until half the snapshots only remain. (In that case, it requires manual action to empty the pacman cache.)
I wondered in the past why 1GiB of extraspace could still lead to Pacman memory crashes during update. The official check provides the answer: I need to add package Download Size to the space requirement besides just buffer space.
Passing "at least 1GiB" as argument to this script before garuda-update will guarantee that garuda-update passes the official memory check.
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.