diff --git a/.gitignore b/.gitignore index da3509e3f..1ede2b5c7 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ raspbian_boot ncp-web/wizard.cfg ncp-web/ncp-web.cfg docker-armhf/qemu-arm-static -.vagrant/ +.vagrant/ \ No newline at end of file diff --git a/bin/ncp/CONFIG/nc-autoupdate-nc.sh b/bin/ncp/CONFIG/nc-autoupdate-nc.sh index 8cfa1375b..079740d24 100644 --- a/bin/ncp/CONFIG/nc-autoupdate-nc.sh +++ b/bin/ncp/CONFIG/nc-autoupdate-nc.sh @@ -23,8 +23,8 @@ configure() cat > /etc/cron.daily/ncp-autoupdate-nc <> /var/log/ncp.log -/usr/local/bin/ncp-update-nc "$VERSION" 2>&1 | tee -a /var/log/ncp.log +echo -e "[ncp-update-nc]" >> /var/log/ncp/ncp.log +/usr/local/bin/ncp-update-nc "$VERSION" 2>&1 | tee -a /var/log/ncp/ncp.log if [[ \${PIPESTATUS[0]} -eq 0 ]]; then @@ -33,7 +33,7 @@ if [[ \${PIPESTATUS[0]} -eq 0 ]]; then sudo -u www-data php /var/www/nextcloud/occ notification:generate \ "$NOTIFYUSER" "NextCloudPi" -l "Nextcloud was updated to \$VER" fi -echo "" >> /var/log/ncp.log +echo "" >> /var/log/ncp/ncp.log EOF chmod 755 /etc/cron.daily/ncp-autoupdate-nc echo "automatic Nextcloud updates enabled" diff --git a/bin/ncp/CONFIG/nc-update-nc-apps-auto.sh b/bin/ncp/CONFIG/nc-update-nc-apps-auto.sh index 95466161f..c08aa8cd2 100644 --- a/bin/ncp/CONFIG/nc-update-nc-apps-auto.sh +++ b/bin/ncp/CONFIG/nc-update-nc-apps-auto.sh @@ -20,9 +20,9 @@ configure() cat > "$cronfile" <<'EOF' #!/bin/bash -echo "[ nc-update-nc-apps-auto ]" >> /var/log/ncp.log -echo "checking for updates..." >> /var/log/ncp.log -ncc app:update --all -n >> /var/log/ncp.log +echo "[ nc-update-nc-apps-auto ]" >> /var/log/ncp/ncp.log +echo "checking for updates..." >> /var/log/ncp/ncp.log +ncc app:update --all -n >> /var/log/ncp/ncp.log EOF chmod 755 "$cronfile" echo "automatic app updates enabled" diff --git a/etc/library.sh b/etc/library.sh index 1b41fe2cc..d4c1abc6a 100644 --- a/etc/library.sh +++ b/etc/library.sh @@ -10,6 +10,8 @@ CFGDIR=/usr/local/etc/ncp-config.d BINDIR=/usr/local/bin/ncp +LOCK_FILE=/run/ncp.lock +LOG=/var/log/ncp/ncp.log function configure_app() { @@ -22,14 +24,16 @@ function configure_app() type dialog &>/dev/null || { echo "please, install dialog for interactive configuration"; return 1; } [[ -f "$cfg_file" ]] || return 0; - local cfg="$( cat "$cfg_file" )" - local len="$(jq '.params | length' <<<"$cfg")" + local cfg len + cfg="$( cat "$cfg_file" )" + len="$(jq '.params | length' <<<"$cfg")" [[ $len -eq 0 ]] && return # read cfg parameters for (( i = 0 ; i < len ; i++ )); do - local var="$(jq -r ".params[$i].id" <<<"$cfg")" - local val="$(jq -r ".params[$i].value" <<<"$cfg")" + local var val + var="$(jq -r ".params[$i].id" <<<"$cfg")" + val="$(jq -r ".params[$i].value" <<<"$cfg")" local vars+=("$var") local vals+=("$val") local idx=$((i+1)) @@ -91,8 +95,9 @@ function configure_app() function run_app() { - local ncp_app=$1 - local script="$(find "$BINDIR" -name $ncp_app.sh)" + local script ncp_app + ncp_app=$1 + script="$(find "$BINDIR" -name "$ncp_app.sh")" [[ -f "$script" ]] || { echo "file $script not found"; return 1; } @@ -102,42 +107,153 @@ function run_app() # receives a script file, no security checks function run_app_unsafe() { - local script=$1 - local ncp_app="$(basename "$script" .sh)" + local script ncp_app + script=$1 + ncp_app="$(basename "$script" .sh)" local cfg_file="$CFGDIR/$ncp_app.cfg" - local log=/var/log/ncp.log [[ -f "$script" ]] || { echo "file $script not found"; return 1; } - touch $log - chmod 640 $log - chown root:www-data $log + touch "$LOG" + chmod 640 "$LOG" + chown root:www-data "$LOG" echo "Running $ncp_app" - echo "[ $ncp_app ]" >> $log - # read script + attach_and_exit_if_running + unset configure - source "$script" + ( + # read cfg parameters + [[ -f "$cfg_file" ]] && { + local len cfg + cfg="$( cat "$cfg_file" )" + jq -e '.tmux' <<<"$cfg" &>/dev/null + local use_tmux="$?" + len="$(jq '.params | length' <<<"$cfg")" + for (( i = 0 ; i < len ; i++ )); do + local var val + var="$(jq -r ".params[$i].id" <<<"$cfg")" + val="$(jq -r ".params[$i].value" <<<"$cfg")" + eval "export $var=$val" + done + } + + echo "$ncp_app" > "$LOCK_FILE" + if which tmux > /dev/null && [[ $use_tmux == 0 ]] + then + run_app_in_tmux "$ncp_app" "$script" + else + # shellcheck disable=SC2064 + trap "rm '$LOCK_FILE'" EXIT SIGHUP SIGINT SIGQUIT SIGILL SIGABRT SIGSEV SIGTERM SIGIO + echo "[ $ncp_app ]" | tee -a "$LOG" + echo "Closing the browser/terminal session will interrupt $ncp_app!" | tee -a "$LOG" + # read script + # shellcheck source=/dev/null + source "$script" + # run + configure 2>&1 | tee -a "$LOG" + local ret="${PIPESTATUS[0]}" + exit "$ret" + fi + ) + ret="$?" + echo "" >> "$LOG" - # read cfg parameters - [[ -f "$cfg_file" ]] && { - local cfg="$( cat "$cfg_file" )" - local len="$(jq '.params | length' <<<"$cfg")" - for (( i = 0 ; i < len ; i++ )); do - local var="$(jq -r ".params[$i].id" <<<"$cfg")" - local val="$(jq -r ".params[$i].value" <<<"$cfg")" - eval "$var=$val" - done + return "$ret" +} + +function run_app_in_tmux() +{ + local ncp_app="$1" + local script="$2" + + echo "You can safely exit. $ncp_app will keep running until it's done." | tee -a "$LOG" + echo "Reattach at any time by running the app again." | tee -a "$LOG" + # Run app in tmux + local tmux_log_file tmux_status_file LIBPATH + tmux_log_file="/var/log/ncp/tmux.${ncp_app}.log" + tmux_status_file="/var/log/ncp/tmux.${ncp_app}.status" + LIBPATH="$(dirname "$CFGDIR")/library.sh" + + # Reset tmux output + echo "[ $ncp_app ]" >> "$LOG" + echo "[ $ncp_app ]" > "$tmux_log_file" + echo "" > "$tmux_status_file" + chmod 640 "$tmux_log_file" "$tmux_status_file" + chown root:www-data "$tmux_log_file" "$tmux_status_file" + + tmux new-session -d -s "$ncp_app" "bash -c '( + trap \"echo \\\$? > $tmux_status_file && rm $LOCK_FILE\" 1 2 3 4 6 9 11 15 19 29 + source \"$LIBPATH\" + source \"$script\" + configure 2>&1 | tee -a \"$LOG\" + echo \"\${PIPESTATUS[0]}\" > $tmux_status_file + rm $LOCK_FILE + )' 2>&1 | tee -a $tmux_log_file" + + attach_to_app "$ncp_app" + return $? +} + +function attach_and_exit_if_running() +{ + # Check if app is already running in tmux + local running_app + running_app=$( [[ -f "$LOCK_FILE" ]] && cat "$LOCK_FILE" || echo "" ) + [[ ! -z $running_app ]] && which tmux >/dev/null && tmux has-session -t="$running_app" &>/dev/null && { + + local choice question + [[ $ATTACH_TO_RUNNING == "1" ]] && choice="y" + [[ $ATTACH_TO_RUNNING == "0" ]] && choice="n" + question="An app ($running_app) is already running. Do you want to attach to it's output? " + if [[ $choice == "y" ]] || [[ $choice == "n" ]] + then + echo "$question" + echo "Choice: <$choice>" + else + read -rp "$question" choice + while [[ "$choice" != "y" ]] && [[ "$choice" != "n" ]] + do + echo "choice was '$choice'" + read -rp "Invalid choice. y or n expected." choice + done + fi + + if [[ "$choice" == "y" ]] + then + attach_to_app "$running_app" + fi + exit $? } + return 0 +} - # run - configure 2>&1 | tee -a $log - local ret="${PIPESTATUS[0]}" +function attach_to_app() +{ + local tmux_log_file tmux_status_file + tmux_log_file="/var/log/ncp/tmux.${ncp_app}.log" + tmux_status_file="/var/log/ncp/tmux.${ncp_app}.status" + + if [[ "$ATTACH_NO_FOLLOW" == "1" ]] + then + cat "$tmux_log_file" + return 0 + else + (while tmux has-session -t="$ncp_app" > /dev/null 2>&1 + do + sleep 1 + done) & - echo "" >> $log + # Follow log file until tmux session has terminated + tail --lines=+0 -f "$tmux_log_file" --pid="$!" + fi - return "$ret" + # Read return value from tmux log file + ret="$(tail -n 1 "$tmux_status_file")" + + [[ $ret =~ ^[0-9]+$ ]] && return $ret + return 1 } function is_active_app() @@ -147,18 +263,20 @@ function is_active_app() local script="$bin_dir/$ncp_app.sh" local cfg_file="$CFGDIR/$ncp_app.cfg" - [[ -f "$script" ]] || local script="$(find "$BINDIR" -name $ncp_app.sh)" + [[ -f "$script" ]] || script="$(find "$BINDIR" -name $ncp_app.sh)" [[ -f "$script" ]] || { echo "file $script not found"; return 1; } # function unset is_active + # shellcheck source=/dev/null source "$script" [[ $( type -t is_active ) == function ]] && { is_active; return $?; } # config [[ -f "$cfg_file" ]] || return 1 - local cfg="$( cat "$cfg_file" )" + local cfg + cfg="$( cat "$cfg_file" )" [[ "$(jq -r ".params[0].id" <<<"$cfg")" == "ACTIVE" ]] && \ [[ "$(jq -r ".params[0].value" <<<"$cfg")" == "yes" ]] && \ return 0 @@ -170,9 +288,10 @@ function info_app() local ncp_app=$1 local cfg_file="$CFGDIR/$ncp_app.cfg" - local cfg="$( cat "$cfg_file" 2>/dev/null )" - local info=$( jq -r .info <<<"$cfg" ) - local infotitle=$( jq -r .infotitle <<<"$cfg" ) + local cfg info infotitle + cfg="$( cat "$cfg_file" 2>/dev/null )" + info=$( jq -r .info <<<"$cfg" ) + infotitle=$( jq -r .infotitle <<<"$cfg" ) [[ "$info" == "" ]] || [[ "$info" == "null" ]] && return 0 [[ "$infotitle" == "" ]] || [[ "$infotitle" == "null" ]] && infotitle="Info" @@ -187,18 +306,20 @@ function info_app() function install_app() { + local script local ncp_app=$1 # $1 can be either an installed app name or an app script if [[ -f "$ncp_app" ]]; then - local script="$ncp_app" - local ncp_app="$(basename "$script" .sh)" + script="$ncp_app" + ncp_app="$(basename "$script" .sh)" else - local script="$(find "$BINDIR" -name $ncp_app.sh)" + script="$(find "$BINDIR" -name $ncp_app.sh)" fi # do it unset install + # shellcheck source=/dev/null source "$script" echo "Installing $ncp_app" (install) @@ -208,6 +329,7 @@ function cleanup_script() { local script=$1 unset cleanup + # shellcheck source=/dev/null source "$script" if [[ $( type -t cleanup ) == function ]]; then cleanup diff --git a/ncp-web/js/ncp.js b/ncp-web/js/ncp.js index 556bc90e6..33a372c37 100644 --- a/ncp-web/js/ncp.js +++ b/ncp-web/js/ncp.js @@ -25,8 +25,8 @@ window.onpopstate = function(event) { click_app($('#' + selectedID)); }; -function errorMsg() -{ +function errorMsg(e) +{ $('#config-box').fill( "Something went wrong. Try refreshing the page" ); } @@ -268,21 +268,36 @@ $(function() var ret = $.parseJSON( result ); if ( ret.token ) $('#csrf-token').set( { value: ret.token } ); - if ( ret.ret ) // means that the process was launched + if ( "ret" in ret ) // means that the process was launched { - if ( ret.ret == '0' ) + if ( ret.ret == 0 ) { if( ret.ref && ret.ref == 'nc-update' ) window.location.reload( true ); reload_sidebar(); $('.circle-retstatus').set('+icon-green-circle'); } - else + else if ( ret.ret === -1 ) // means we attach to an already running app + { + if( ret.output ) + { + var box_l = $('#' + selectedID + '-details-box'); + var box = box_l[0]; + var lines = ret.output.split("\n"); + lines.forEach(line => { + box_l.ht( box.innerHTML + escapeHTML(line) + '
' ); + }); + box.scrollTop = box.scrollHeight; + } $('.circle-retstatus').set('-icon-green-circle'); + } + else + { + $('.circle-retstatus').set('-icon-green-circle'); + } } else // print error from server instead { - $('.details-box').fill(ret.output); $('.circle-retstatus').set('-icon-green-circle'); } $( 'input' , '#config-box-wrapper' ).set('@disabled', null); diff --git a/ncp-web/ncp-launcher.php b/ncp-web/ncp-launcher.php index 332450188..5e068b4ac 100644 --- a/ncp-web/ncp-launcher.php +++ b/ncp-web/ncp-launcher.php @@ -80,13 +80,30 @@ } // launch - echo '{ "token": "' . getCSRFToken() . '",'; // Get new token - echo ' "ref": "' . $ncp_app . '",'; - echo ' "output": "" , '; - echo ' "ret": '; - exec( 'bash -c "sudo /home/www/ncp-launcher.sh ' . $ncp_app . '"' , $output , $ret ); - echo '"' . $ret . '" }'; + $lock_file = "/run/ncp.lock"; + if( file_exists($lock_file) ) + { + $running = trim(file_get_contents($lock_file)); + $output = "An app ($running) is already running...".PHP_EOL; + if ( file_exists("/var/log/ncp/tmux.$running.log") ) + { + $output .= "Attaching to its output:".PHP_EOL; + $output .= file_get_contents("/var/log/ncp/tmux.$running.log"); + } + $ret = -1; + } + else + { + exec( 'sudo /home/www/ncp-launcher.sh ' . $ncp_app , $cmd_out , $ret ); + } + + echo json_encode(array( + "token" => getCSRFToken(), + "ref" => $ncp_app, + "output" => $output, + "ret" => $ret + )); } // diff --git a/ncp-web/ncp-output.php b/ncp-web/ncp-output.php index 704b0a581..f7083f529 100644 --- a/ncp-web/ncp-output.php +++ b/ncp-web/ncp-output.php @@ -79,7 +79,7 @@ function follow($file) session_write_close(); echo str_pad('',1024*1024*4); // make sure the browser buffer becomes full -$ncp_log = '/var/log/ncp.log'; +$ncp_log = '/var/log/ncp/ncp.log'; if (!file_exists($ncp_log)) touch($ncp_log); follow($ncp_log); diff --git a/ncp.sh b/ncp.sh index 71872fa35..533b960e7 100644 --- a/ncp.sh +++ b/ncp.sh @@ -14,6 +14,7 @@ BRANCH=master BINDIR=/usr/local/bin/ncp CONFDIR=/usr/local/etc/ncp-config.d/ +LOGFILE=/var/log/ncp/ncp.log APTINSTALL="apt-get install -y --no-install-recommends" export DEBIAN_FRONTEND=noninteractive @@ -22,8 +23,13 @@ install() { # NCP-CONFIG apt-get update - $APTINSTALL git dialog whiptail jq - mkdir -p "$CONFDIR" "$BINDIR" + $APTINSTALL git dialog whiptail jq tmux locale + # Install UTF-8 locale required by tmux + grep -v '#' /etc/locale.gen | grep -ie "en_US.UTF-8[[:blank:]]*UTF-8" &> /dev/null || { + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + } + mkdir -p "$CONFDIR" "$BINDIR" "$(dirname LOGFILE)" # include option in raspi-config (only Raspbian) test -f /usr/bin/raspi-config && { @@ -131,8 +137,9 @@ EOF cat > /home/www/ncp-launcher.sh <<'EOF' #!/bin/bash + source /usr/local/etc/library.sh -run_app $1 +run_app "$1" EOF chmod 700 /home/www/ncp-launcher.sh echo "www-data ALL = NOPASSWD: /home/www/ncp-launcher.sh , /sbin/halt, /sbin/reboot" >> /etc/sudoers @@ -188,7 +195,7 @@ EOF # LIMIT LOG SIZE grep -q maxsize /etc/logrotate.d/apache2 || sed -i /weekly/amaxsize2M /etc/logrotate.d/apache2 cat >> /etc/logrotate.d/ncp <<'EOF' -/var/log/ncp.log +/var/log/ncp/ncp.log { rotate 4 size 500K @@ -200,7 +207,7 @@ EOF # ONLY FOR IMAGE BUILDS if [[ -f /.ncp-image ]]; then - rm -rf /var/log/ncp.log + rm -rf "$LOGFILE" ## NEXTCLOUDPI MOTD rm -rf /etc/update-motd.d diff --git a/update.sh b/update.sh index 761c4c3ac..def75198e 100755 --- a/update.sh +++ b/update.sh @@ -11,6 +11,7 @@ set -e CONFDIR=/usr/local/etc/ncp-config.d/ +LOGFILE=/var/log/ncp/ncp.log # don't make sense in a docker container EXCL_DOCKER=" @@ -47,8 +48,26 @@ pgrep apt &>/dev/null && { echo "apt is currently running. Try again later"; ex type jq &>/dev/null || { apt-get update apt-get install -y --no-install-recommends jq + +} + +which tmux || { + apt install tmux locale +} + +# Install UTF-8 locale required by tmux +grep -v '#' /etc/locale.gen | grep -ie "en_US.UTF-8[[:blank:]]*UTF-8" &> /dev/null || { + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen } +# Migrate to new log location +[[ -d /var/log/ncp ]] || { + mkdir -p "$(dirname "$LOGFILE")" + [[ -f /var/log/ncp.log ]] && mv /var/log/ncp.log "$LOGFILE" +} + + # migrate to the new cfg format [[ -f "$CONFDIR"/dnsmasq.sh ]] && { @@ -98,8 +117,9 @@ type jq &>/dev/null || { cat > /home/www/ncp-launcher.sh <<'EOF' #!/bin/bash + source /usr/local/etc/library.sh -run_app $1 +run_app "$1" EOF chmod 700 /home/www/ncp-launcher.sh }