From 163e8369c3c451c1c29a7492293fcc27b1c2d722 Mon Sep 17 00:00:00 2001 From: Botspot <54716352+Botspot@users.noreply.github.com> Date: Sun, 19 Jan 2025 17:23:50 -0600 Subject: [PATCH 1/3] everything should be functional --- api | 33 +++++++++++++++++++++------------ manage | 56 +++++++++++++++++++++++++++++++++++++++----------------- updater | 14 ++++++++------ 3 files changed, 68 insertions(+), 35 deletions(-) diff --git a/api b/api index da0a306428..c0c2fac45a 100755 --- a/api +++ b/api @@ -1595,9 +1595,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")" @@ -2919,16 +2921,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")" @@ -2958,7 +2963,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 logfile to Pi-Apps developers for review":2) fi #display support links, depending on if this was a package-app or a script-app @@ -2981,7 +2986,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 @@ -2993,12 +2998,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 $app 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)) diff --git a/manage b/manage index 7d76da7a7c..f985cf7ed4 100755 --- a/manage +++ b/manage @@ -438,7 +438,7 @@ $app" 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 @@ -447,7 +447,33 @@ $app" elif [ ! -z "$new_lines" ];then # add new_lines to queue if new_lines is not empty 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 app's 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")" @@ -455,12 +481,6 @@ $app" 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'" @@ -481,13 +501,13 @@ $app" 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. #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 @@ -500,17 +520,23 @@ $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. @@ -518,7 +544,7 @@ $app" 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" @@ -535,10 +561,6 @@ ${DIRECTORY}/icons/none-1.png #close the queue-viewer window in a few seconds (sleep 5; kill $yadpid 2>/dev/null) & - #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 diff --git a/updater b/updater index 1e1591be89..867b273810 100755 --- a/updater +++ b/updater @@ -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 @@ -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 @@ -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 @@ -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 From 1e6485d711f5a8d000eabf58def90c9907c9412a Mon Sep 17 00:00:00 2001 From: Botspot <54716352+Botspot@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:08:31 -0600 Subject: [PATCH 2/3] manage daemon run terminal to change window order This has bothered me ever since bookworm dropped. Now terminal appears after queueviewer window, thus, on top of it. No more half occluded terminal. This also prepares for a summary window from "Contributor update" to appear after the terminal closes. --- api | 51 ++++++++++++--------------------------------------- manage | 54 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/api b/api index c0c2fac45a..c56fd1a4ae 100755 --- a/api +++ b/api @@ -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 (or filelists) - refreshes the app list + 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 } @@ -2963,7 +2936,7 @@ diagnose_apps() { #Given a list of action;app that failed to install/uninstall, 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"!"Send this logfile to Pi-Apps developers for review":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 diff --git a/manage b/manage index f985cf7ed4..c2e0cd245f 100755 --- a/manage +++ b/manage @@ -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 \ @@ -428,13 +428,19 @@ $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 @@ -445,13 +451,13 @@ $app" 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 app's logfile - list item format is $action;$app;$exitcode - failed_apps="$(echo "$queue" | grep ';1$' | sed 's/;1$//g')" + #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). @@ -462,15 +468,15 @@ $app" break else #Replace past '1' exit codes with "diagnosed" to avoid repeated diagnosis - queue="$(echo "$queue" | sed 's/;1$/;diagnosed/g')" + queue="$(echo "$queue" | sed '\''s/;1$/;diagnosed/g'\'')" #user added more actions while the diagnosis window was open - queue+=$'\n'"$new_lines" + 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')" + queue="$(echo "$queue" | sed '\''s/;1$/;diagnosed/g'\'')" #and now add to queue the actions we want to retry. - queue+=$'\n'"$retry_apps" + queue+=$'\''\n'\''"$retry_apps" fi fi @@ -483,19 +489,19 @@ $app" 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=$! @@ -503,7 +509,7 @@ $app" #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" ] && 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 @@ -559,7 +565,19 @@ ${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 # if update refresh or update-file actions were run then update the .git folder if [ "$sourced_updater" == 1 ]; then From d22300a5638ff6f48f248163754fe16d98969772 Mon Sep 17 00:00:00 2001 From: Botspot <54716352+Botspot@users.noreply.github.com> Date: Sun, 19 Jan 2025 22:10:21 -0600 Subject: [PATCH 3/3] remove last mention of filelist since we don't use it --- api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api b/api index c56fd1a4ae..a85a141224 100755 --- a/api +++ b/api @@ -1081,7 +1081,7 @@ 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) - refreshes the app list +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.