diff --git a/shell.md b/shell.md index 026f9769095..9150bdbbe9b 100644 --- a/shell.md +++ b/shell.md @@ -38,7 +38,7 @@ For each download the release key is fetched by its fingerprint from keys.openpg sh start.sh --stop ``` - `--run` records the process id in `.pid` (`FullNode.jar.pid` by default) next to `start.log`, and `--stop` reads it. Run `--stop` in the directory the node was started from, with the same `-j` name if one was given. After `--release` or `-cb` the node runs in `FullNode/`; `--stop` finds it there from the parent directory as well. A node started by an earlier version of the script (a `start.log` but no pid file) is still found by its jar name. + `--run` records the process id in `.pid` (`FullNode.jar.pid` by default) next to `start.log`, and `--stop` reads it. Run `--stop` in the directory the node was started from, with the same `-j` name if one was given. After `--release` or `-cb` the node runs in `FullNode/`; `--stop` finds it there from the parent directory as well. `--stop` stops a process only when it runs the jar from that directory, reading the working directory from `/proc` on Linux or with `lsof` on macOS, so nodes with the same jar name in other directories keep running; a pid file whose process now runs from another directory counts as a stopped node. This isolation needs a readable working directory: if the working directory of the process in the pid file cannot be read, the recorded id is trusted as it is, so it must be known to belong to this node, for example when it is written into the pid file by hand. A node started by an earlier version of the script (a `start.log` but no pid file) is found by its jar name; if the working directory of such a process cannot be read, `--stop` exits with an error: write the node's process id to the pid file and run `--stop` again. If the node cannot be stopped, for example because its process belongs to another user, `--stop` exits with an error and `--run` does not start another node. * Get the latest version of `FullNode.jar` and start it diff --git a/start.sh b/start.sh index 8244c0d7ecd..1ba68a4e36f 100644 --- a/start.sh +++ b/start.sh @@ -401,26 +401,75 @@ matchNodePid() { done } -# Sets $pid to the process id recorded in .pid; empty when that process is gone. A -# directory that has a start.log but no pid file was used by an earlier version of this script, -# which kept no pid file; there every process on the machine that runs the jar name is taken, -# as that version did, so $pid may hold several ids. +# Prints the working directory of process $1, or nothing when it cannot be read: from /proc on +# Linux and with lsof elsewhere (macOS). For another user's process both work only as root. +processDir() { + if [ -d /proc/self ]; then + readlink "/proc/$1/cwd" 2>/dev/null + else + lsof -a -p "$1" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' + fi +} + +# Prints the process ids on stdin whose working directory is the current directory. This script +# and its earlier versions start the node from its directory, and the JVM keeps that working +# directory, so this tells the node started here from nodes in other directories that run the +# same jar name. A process whose working directory cannot be read may be the node started here: +# if this user can signal it, it is reported on stderr and the function returns 1. One that this +# user cannot signal (another user's process, or one already gone) is left out. +matchDirPid() { + local here candidate dir unread='' + here=$(pwd -P) + while read -r candidate; do + dir=$(processDir "$candidate") + if [ "$dir" = "$here" ]; then + echo "$candidate" + elif [ -z "$dir" ]; then + if kill -0 "$candidate" 2>/dev/null; then + unread="$unread $candidate" + elif ps -p "$candidate" >/dev/null 2>&1; then + warn "process $candidate runs $JAR_NAME but cannot be inspected or signalled by this user, leaving it running" >&2 + fi + fi + done + if [ -n "$unread" ]; then + error "cannot read the working directory of process$unread running $JAR_NAME, so the node started from $PWD cannot be told apart; write its process id to $PWD/${JAR_NAME##*/}.pid and run again" + return 1 + fi + return 0 +} + +# Sets $pid to the process id recorded in .pid; empty when that process is gone or runs +# from another directory. A directory that has a start.log but no pid file was used by an earlier +# version of this script, which kept no pid file; there $pid holds the processes that run the jar +# name from this directory, and the script exits when one of them cannot be told apart. checkPid() { local pidFile="${JAR_NAME##*/}.pid" - local saved + local saved dir pid='' if [ -f "$pidFile" ]; then - # Only the recorded process counts, and only while it still runs the jar. Once it is found - # gone the file is emptied, so its id is not looked up again. + # Only the recorded process counts, and only while it still runs the jar from this directory. + # Once it is found gone the file is emptied, so its id is not looked up again. A working + # directory read as another directory means the id now belongs to another node; one that + # cannot be read proves nothing, so the recorded process is kept. saved=$(cat "$pidFile" 2>/dev/null) if [ -n "$saved" ]; then pid=$(ps -o pid=,args= -p "$saved" 2>/dev/null | matchNodePid) + if [ -n "$pid" ]; then + dir=$(processDir "$pid") + if [ -n "$dir" ] && [ "$dir" != "$(pwd -P)" ]; then + warn "process $pid in $pidFile now runs from $dir, the node started from $PWD has stopped" >&2 + pid='' + fi + fi if [ -z "$pid" ]; then : > "$pidFile" fi fi elif [ -f start.log ]; then - pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid) + if ! pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid | matchDirPid); then + exit 1 + fi fi } @@ -435,9 +484,11 @@ enterNodeDir() { } # Stops the running node: sends SIGTERM once per second for up to MAX_STOP_TIME seconds, -# then SIGKILL. Returns 1 when no node was ever started from this directory. +# then SIGKILL. Returns 1 when no node was ever started from this directory, and 2 when the node +# cannot be stopped: this user cannot signal one of its processes, or it still runs after SIGKILL. stopService() { local pidFile="${JAR_NAME##*/}.pid" + local p denied enterNodeDir if [ ! -f "$pidFile" ] && [ ! -f start.log ]; then info "no node was started from $PWD" @@ -446,20 +497,38 @@ stopService() { count=1 while [ $count -le $MAX_STOP_TIME ]; do checkPid - if [ -n "$pid" ]; then - kill -15 $pid - sleep 1 - else + if [ -z "$pid" ]; then info "java-tron stop" - return + return 0 + fi + # A failed kill counts only while the process still exists: it may have exited after + # checkPid found it. + denied='' + for p in $pid; do + if ! kill -15 "$p" 2>/dev/null && ps -p "$p" > /dev/null 2>&1; then + denied="$denied $p" + fi + done + if [ -n "$denied" ]; then + error "cannot signal process$denied of the node started from $PWD, stop it as its owner" + return 2 fi + sleep 1 count=$(($count + 1)) if [ $count -eq $MAX_STOP_TIME ]; then - kill -9 $pid + for p in $pid; do + kill -9 "$p" 2>/dev/null + done sleep 1 fi done sleep 5 + checkPid + if [ -n "$pid" ]; then + error "java-tron still runs with pid ${pid//$'\n'/ } after SIGKILL" + return 2 + fi + info "java-tron stop" } # Linux only: refuses to start with less than ALLOW_MIN_MEMORY MB of memory, or when -mem @@ -742,7 +811,11 @@ restart() { # running, so its download and key-server lookup do not add to the stop-to-start downtime. enterNodeDir ensureArchiveJar + # 1 only means that no node was started from this directory before, so this is its first start. stopService + if [ $? -eq 2 ]; then + exit 1 + fi if [[ $IS_BACKUP_GC_LOG = true ]]; then backupGCLog fi