Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change manage daemon to allow retrying failed actions #2708

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 32 additions & 50 deletions api
Original file line number Diff line number Diff line change
Expand Up @@ -1081,51 +1081,24 @@ terminal_manage() { # wrapper for the original terminal_manage function to termi
terminal_manage_multi "$action $app"
}

terminal_manage_multi() { #function to install/uninstall/update/refresh multiple apps (or filelists) - uses a terminal and refreshes the app list
queue="$1" #one or multiple actions and app names (or filelists of the format 'filelist:path/to/file:path/to/another/file')
terminal_manage_multi() { #function to install/uninstall/update/refresh multiple apps - refreshes the app list if applicable
local queue="$1" #one or multiple actions and app names

#To prevent multiple simultaneous manage instances, use the 'daemon' mode. This will create a queue of actions that are executed concurrently.
#The first daemon instance is the 'master' process. Subsequent processes will add the action to the queue and then exit.
#The first daemon instance is the 'master' process, which opens a terminal. Subsequent processes will add the action to the queue and then exit.
#All output is generated by the 'master' daemon process. Subsequent processes shouldn't open a terminal, because the master one is already open.
if [ -f "${DIRECTORY}/data/manage-daemon/pid" ] && process_exists $(cat "${DIRECTORY}/data/manage-daemon/pid") ;then
#The 'master' daemon is already running. Avoid launching a second terminal.
#daemon already running, send queue to it and exit
"${DIRECTORY}/manage" daemon "$queue"

else
#in a terminal, first get the api functions, display the pi-apps logo, run the manage script, and refresh the app list if the $pipe variable is set
"${DIRECTORY}/etc/terminal-run" '
DIRECTORY="'"$DIRECTORY"'"
export geometry2="'"$geometry2"'"
source "${DIRECTORY}/api"
generate_logo

refresh_list() { #Refresh the current list of apps in the event of a change
if [ ! -z "'"$pipe"'" ] && [ -p "'"$pipe"'" ];then
echo -e "\f" > "'"$pipe"'"
"${DIRECTORY}/preload" "$(cat "${DIRECTORY}/data/settings/App List Style")" "'"$prefix"'" > "'"$pipe"'" 2>/dev/null
fi
}

"${DIRECTORY}/manage" daemon "'"$queue"'"

#refresh app list
refresh_list

for i in {30..1} ;do
echo -en "You can close this window now. Auto-closing in $i seconds.\e[0K\r"
sleep 1
done
' "Terminal Output"

# Check if terminal-run failed to launch. GUI users don't see any terminal output if it fails (since there is no terminal open) so we need to prompt them with a GUI window
if [ "$?" != 0 ]; then
echo -e "Unable to open a terminal.\nDebug output below.\n$(DEBUG=1 "${DIRECTORY}/etc/terminal-run" 2>&1)" | yad --center --window-icon="${DIRECTORY}/icons/logo.png" \
--width=700 --height=300 --text-info --title="Error occured when calling terminal-run" \
--image="${DIRECTORY}/icons/error.png" --image-on-top --fontname=12 \
--button='OK'
if echo "$queue" | grep -q "^update filelist:" ;then #list of files separated by :
updatable_apps='' updatable_files="$(echo "$queue" | grep "^update filelist:" | sed 's/^update filelist://g' | tr ':' '\n')" no_status=true update_now_cli
fi
#this function is running the daemon, so once it exits refresh the pi-apps list
"${DIRECTORY}/manage" daemon "$queue"

#refresh app list
if [ ! -z "$pipe" ] && [ -p "$pipe" ];then
echo -e '\f' > "$pipe"
"${DIRECTORY}/preload" "$(cat "${DIRECTORY}/data/settings/App List Style")" "$prefix" > "$pipe" 2>/dev/null
fi
fi
}
Expand Down Expand Up @@ -1595,9 +1568,11 @@ will_reinstall() { #return 0 if $1 app will be reinstalled during an update, oth
[ -z "$app" ] && error 'will_reinstall(): requires an argument'

#exit immediately if app is not installed.
if [ "$(app_status "$app")" != 'installed' ];then
return 1
fi
#if [ "$(app_status "$app")" != 'installed' ];then
# return 1
#fi
#it seems that the above code was added for speed alone https://github.com/Botspot/pi-apps/commit/d8bfc3f5fc9b42060bfbd68a11b24f66246d61bc
#commenting it out allows retrying failed app update where uninstall succeeds but install fails

#detect which installation script exists - both for local install and for update directory
local local_scriptname="$(script_name_cpu "$app")"
Expand Down Expand Up @@ -2919,16 +2894,19 @@ send_error_report() { #non-interactively send a Pi-Apps error log file to the Bo

}

diagnose_apps() { #Given a list of apps that failed to install/uninstall, loop through each error log, diagnose it, and provide a "Send report" button if applicable.
local failed_apps="$1"
diagnose_apps() { #Given a list of action;app that failed to install/uninstall, loop through each error log, diagnose it, and provide a "Send report" button if applicable.
local failure_list="$1"
local IFS=$'\n'

local num_lines="$(grep . <<<"$failed_apps" | wc -l)"
local i=1 #counter to track which failed_app in the list is current
local app
local num_lines="$(grep . <<<"$failure_list" | wc -l)"
local i=1 #counter to track which line in the list is current
local line
local button

for app in $failed_apps ;do
for line in $failure_list ;do
local action="$(echo "$line" | awk -F';' '{print $1}')"
local app="$(echo "$line" | awk -F';' '{print $2}')"

local logfile="$(get_logfile "$app")"
#given the app's logfile, categorize the error and set the error_type variable
local diagnosis="$(log_diagnose "$logfile" "allowwrite")"
Expand Down Expand Up @@ -2958,7 +2936,7 @@ diagnose_apps() { #Given a list of apps that failed to install/uninstall, loop t
text+=$'\n'"Error report cannot be sent because your system is unsupported."
else
#if all of the above checks evaluate to FALSE, then display the "Send report" button.
local send_button=(--button='Send report'!"${DIRECTORY}/icons/send-error-report.png":2)
local send_button=(--button='Send report'!"${DIRECTORY}/icons/send-error-report.png"!"Send this log file to Pi-Apps developers for review":2)
fi

#display support links, depending on if this was a package-app or a script-app
Expand All @@ -2981,7 +2959,7 @@ diagnose_apps() { #Given a list of apps that failed to install/uninstall, loop t
error_caption="$(echo "$unsupported_message" | sed "s/\x1b\[[0-9;]*[a-zA-Z]//g")"$'\n'$'\n'"$error_caption"
fi

#this dialog may be one in a series of failed_app dialogs. Name the window-close button accordingly.
#this dialog may be one in a series of diagnosis dialogs. Name the window-close button accordingly.
if [ $i -lt $num_lines ];then
local close_button=(--button="Next error!${DIRECTORY}/icons/forward.png":1)
else
Expand All @@ -2993,12 +2971,16 @@ diagnose_apps() { #Given a list of apps that failed to install/uninstall, loop t
--text="$text" --wrap --fontname=12 \
--button='View log'!"${DIRECTORY}/icons/log-file.png"!"Review the output from when <b>$app</b> tried to $action.":"bash -c 'view_file "\""$logfile"\""'" \
"${send_button[@]}" \
"${close_button[@]}"
--button="Retry!${DIRECTORY}/icons/refresh.png"!"Try $(echo "${action}ing" | sed 's/updateing/updating/g') $app again.":3 \
"${close_button[@]}" >/dev/null
button="${PIPESTATUS[1]}"

if [ $button == 2 ];then
#send error log
send_error_report "$logfile"
elif [ $button == 3 ];then
#retry button clicked, return new queue line(s) back to manage daemon
echo "$line"
fi

i=$((i+1))
Expand Down
98 changes: 69 additions & 29 deletions manage
Original file line number Diff line number Diff line change
Expand Up @@ -417,9 +417,9 @@ $app"
fi
fi

[ -z "$geometry2" ] && geometry2='--center'
[ -z "$geometry2" ] && geometry2='--center --width=330 --height=400'

tail -f --retry "${DIRECTORY}/data/manage-daemon/yadlist" 2>/dev/null | yad --class Pi-Apps --name "Pi-Apps" --width=330 --height=400 "$geometry2" --title='Monitor Progress' \
tail -f --retry "${DIRECTORY}/data/manage-daemon/yadlist" 2>/dev/null | yad --class Pi-Apps --name "Pi-Apps" "$geometry2" --title='Monitor Progress' \
--list --tail --no-headers --column=:IMG --column=:IMG --column=Text --column=:IMG --column=Text \
--separator='\n' --window-icon="${DIRECTORY}/icons/logo.png" \
--dclick-action=true --select-action=true \
Expand All @@ -428,66 +428,92 @@ $app"

trap "kill $yadpid 2>/dev/null" EXIT

"${DIRECTORY}/etc/terminal-run" '
DIRECTORY="'"$DIRECTORY"'"
source "${DIRECTORY}/api"
generate_logo

queue=""

#Used to track which line of $queue is currently being dealt with.
current_line_num=1

sourced_updater=0 #updater script needs to be sourced if files are updated. This allows it to only be sourced once.

queue=''
IFS=$'\n'
IFS=$'\''\n'\''
while true;do #repeat until nothing is left in the queue

#check for new actions to be executed
echo -n > "${DIRECTORY}/data/manage-daemon/queue" & #ensure that the pipe is in write mode to prevent cat from hanging if the queue file is empty
echo -n > "${DIRECTORY}/data/manage-daemon/queue" & #ensure that the pipe is in write mode to prevent tac from hanging if the queue file is empty
new_lines="$(tac "${DIRECTORY}/data/manage-daemon/queue")" # tac reverses the order of the list. a plain cat of the file will give the newest item in the queue first.

#keep track of all actions for this session with the $queue variable
if [ -z "$queue" ];then
queue="$new_lines"
elif [ ! -z "$new_lines" ];then # add new_lines to queue if new_lines is not empty
queue+=$'\n'"$new_lines"
queue+=$'\''\n'\''"$new_lines"
fi


#check if queue is complete - diagnose apps and provide the opportunity to retry failed actions, otherwise exit loop
if [ "${current_line_num}" -gt "$(echo "$queue" | wc -l)" ];then
#diagnose every failed apps logfile - list item format is $action;$app;$exitcode
failed_apps="$(echo "$queue" | grep '\'';1$'\'' | sed '\''s/;1$//g'\'')"
retry_apps="$(diagnose_apps "$failed_apps")"
if [ -z "$retry_apps" ];then
#user chose to not retry the failed action(s).
#edge case: user may have added more actions while the diagnosis window was open. Check for this, and if nothing found, exit loop
echo -n > "${DIRECTORY}/data/manage-daemon/queue" & #ensure that the pipe is in write mode to prevent tac from hanging if the queue file is empty
new_lines="$(tac "${DIRECTORY}/data/manage-daemon/queue")"
if [ -z "$new_lines" ];then
break
else
#Replace past '1' exit codes with "diagnosed" to avoid repeated diagnosis
queue="$(echo "$queue" | sed '\''s/;1$/;diagnosed/g'\'')"
#user added more actions while the diagnosis window was open
queue+=$'\''\n'\''"$new_lines"
fi
else
#user chose to retry action(s). Replace '1' exit codes with "diagnosed" to avoid repeated diagnosis
queue="$(echo "$queue" | sed '\''s/;1$/;diagnosed/g'\'')"
#and now add to queue the actions we want to retry.
queue+=$'\''\n'\''"$retry_apps"
fi
fi

# reorder queue list to prioritize app refresh and file update actions
queue="$(reorder_list "$queue")"

if [ ! -z "$new_lines" ] && [ "$sourced_updater" == 0 ] && grep -q "update\|refresh\|update-file" <<<"$new_lines" ;then #source updater if necessary
source "${DIRECTORY}/updater" source
sourced_updater=1
fi
#echo "length of queue is $(echo "$queue" | wc -l)"

#exit loop if queue is complete and no new actions were added to the queue
if [ "${current_line_num}" -gt "$(echo "$queue" | wc -l)" ];then
break
fi

#echo "current position in queue is ${current_line_num}"
#echo "queue is '$queue'"
#echo "queue is $queue"

line="$(echo "$queue" | sed -n "${current_line_num}"p)"
#echo "Now handling request: '$line'"
#echo "Now handling request: $line"

#indicate current action in current line of $queue
queue="$(echo "$queue" | sed "${current_line_num}s/$/;in-progress/")"

#get first word of this line - the action. Subsequent words are the name of the app.
action="$(echo "$line" | awk -F';' '{print $1}')"
app="$(echo "$line" | awk -F';' '{print $2}')"
action="$(echo "$line" | awk -F'\'';'\'' '\''{print $1}'\'')"
app="$(echo "$line" | awk -F'\'';'\'' '\''{print $2}'\'')"

#refresh the list in queue-viewer window as a background process - skip it if the list is still refreshing from last loop iteration; in game dev this is 'dropped input'
#refresh the list in queue-viewer window as a background process - skip it if the list is still refreshing from last loop iteration; in game dev this is "dropped input"
if [ -z "$write_list_pid" ] || ! process_exists "$write_list_pid" ;then
write_list "$queue" &
write_list_pid=$!

#secondary list-writing background process - kill it if it exists because write_list just sent a newer version of the list to yad
[ ! -z "$secondary_write_list_pid" ] && process_exists "$secondary_write_list_pid" && kill "$secondary_write_list_pid"
[ ! -z "$secondary_write_list_pid" ] && kill "$secondary_write_list_pid" 2>/dev/null
else
#if app1 is refreshed and app2 is then reinstalled, the list would only say app1 is bring refreshed for the entirety of app2's reinstallation, due to the process-skipping.
#if app1 is refreshed and app2 is then reinstalled, the list would only say app1 is bring refreshed for the entirety of app2s reinstallation, due to the process-skipping.
#launch a secondary background process that waits for $write_list_pid to finish

#only allow one secondary background process to run; kill previous jobs and start a new one
[ ! -z "$secondary_write_list_pid" ] && process_exists "$secondary_write_list_pid" && kill "$secondary_write_list_pid"
[ ! -z "$secondary_write_list_pid" ] && kill "$secondary_write_list_pid" 2>/dev/null
(while process_exists $write_list_pid ;do sleep 1 ;done ; write_list "$queue") &
secondary_write_list_pid=$!
fi
Expand All @@ -500,25 +526,31 @@ $app"
exitcode=$?
elif [ "$action" == refresh ];then
#Set terminal title
echo -ne "\e]0;${action^}ing ${app}\a"
echo -ne "\e]0;Refreshing ${app}\a"
refresh_app "$app"
exitcode=$?
elif [ "$action" == update ];then #manage can handle this action, but avoid spawning subprocess
#Set terminal title
echo -ne "\e]0;Updating ${app}\a"
update_app "$app"
exitcode=$?
else
#Set terminal title
echo -ne "\e]0;${action^}ing ${app}\a" | sed 's/Updateing/Updating/g'
echo -ne "\e]0;${action^}ing ${app}\a"
"${DIRECTORY}/manage" "$action" "$app"
exitcode=$?
fi

#record exit code in current line of $queue
[ "$exitcode" != 0 ] && exitcode=1 #for easier regex and sed lines, force non-zero exit codes to be 1
queue="$(echo "$queue" | sed "${current_line_num}s/;in-progress$/;$exitcode/")"

#one more line of $queue has been completed.
current_line_num=$((current_line_num+1))
done

#refresh the list in queue-viewer window for the final time with all actions complete
[ ! -z "$secondary_write_list_pid" ] && process_exists "$secondary_write_list_pid" && kill "$secondary_write_list_pid" #kill secondary list writer
[ ! -z "$secondary_write_list_pid" ] && kill "$secondary_write_list_pid" 2>/dev/null #kill secondary list writer
wait $write_list_pid
write_list "$queue"

Expand All @@ -533,12 +565,20 @@ ${DIRECTORY}/icons/none-1.png
rm -f "${DIRECTORY}/data/manage-daemon/pid"

#close the queue-viewer window in a few seconds
(sleep 5; kill $yadpid 2>/dev/null) &
(sleep 5; kill $yadpid 2>/dev/null) &' "Terminal Output"

# Check if terminal-run failed to launch. GUI users don't see any terminal output if it fails (since there is no terminal open) so we need to prompt them with a GUI window
if [ "$?" != 0 ]; then
echo -e "Unable to open a terminal.\nDebug output below.\n$(DEBUG=1 "${DIRECTORY}/etc/terminal-run" 2>&1)" | yad --class Pi-Apps --name "Pi-Apps" --center --window-icon="${DIRECTORY}/icons/logo.png" \
--width=700 --height=300 --text-info --title="Error occured when calling terminal-run" \
--image="${DIRECTORY}/icons/error.png" --image-on-top --fontname=12 \
--button='OK'
if echo "$queue" | grep -q "^update-file;" ;then
typeset -f update_now_cli &>/dev/null || source "${DIRECTORY}/updater" source
updatable_apps='' updatable_files="$(echo "$queue" | grep "^update-file;" | sed 's/^update-file;//g')" update_now_cli
fi
fi

#diagnose every failed app's logfile - list item format is $action;$app;$exitcode
failed_apps="$(echo "$queue" | grep ';1$' | awk -F';' '{print $2}')"
diagnose_apps "$failed_apps"

# if update refresh or update-file actions were run then update the .git folder
if [ "$sourced_updater" == 1 ]; then
update_git
Expand Down
14 changes: 8 additions & 6 deletions updater
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ update_app() { #first arg is app name

#Set terminal title
echo -ne "\e]0;Updating ${app}\a"

# check if update app folder exists before doing anything
# it can happen that executing the updater from the pi-apps GUI the update folder is missing
# if the user has no internet or internet issues the update folder will be removed due to the failed git pull and then wait in the loop for a new git clone
Expand All @@ -452,15 +452,17 @@ update_app() { #first arg is app name
if will_reinstall "$app";then
installback=yes
status "$app's install script has been updated. Reinstalling $app..."
#uninstall it
"${DIRECTORY}/manage" uninstall "$app" update #report to the app uninstall script that this is an uninstall for the purpose of updating by passing "update"
#uninstall it - if retrying an update with successful uninstall and failed install, don't uninstall again
if [ "$(app_status "$app")" != uninstalled ];then
"${DIRECTORY}/manage" uninstall "$app" update #report to the app uninstall script that this is an uninstall for the purpose of updating by passing "update"
fi

#fix edge case: if app is installed but uninstall script doesn't exist somehow, then pretend app was uninstalled so that the reinstall later will happen noninteractively
if [ "$(app_status "$app")" == installed ];then
echo 'uninstalled' > "${DIRECTORY}/data/status/${app}"
fi
fi

no_status=true refresh_app "$app"

failed=false
Expand Down Expand Up @@ -558,7 +560,7 @@ update_now_gui_apps() { # deprecated function that is only here so old updater s
#load the app list now to reduce launch time
refresh_app_list &

action=update diagnose_apps "$failed_apps"
diagnose_apps "$(echo "$failed_apps" | sed 's/^/update;/g')"
}

update_now_gui() { #input: updatable_files and updatable_apps variables
Expand Down Expand Up @@ -620,7 +622,7 @@ update_now_background() { #input: updatable_apps and updatable_files variables
elif [ -f "${DIRECTORY}/update/pi-apps/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/install-${arch}" ] && [ ! -f "${DIRECTORY}/apps/${app}/install" ] && [ ! -f "${DIRECTORY}/apps/${app}/packages" ]; then
continue
# if app will be reinstalled then don't try to reinstall it in the background
elif will_reinstall "$app"; then
elif [ "$(app_status "${app}")" == 'installed' ];then
continue
# if app failed to install last time, show this app refresh to the user.
elif [ "$(app_status "${app}")" == 'corrupted' ];then
Expand Down
Loading