From c8e81568f7e6ed65819f1e6842f8954fe8e84c48 Mon Sep 17 00:00:00 2001 From: GrapeS Date: Thu, 29 Oct 2026 09:25:04 +0800 Subject: [PATCH 1/3] fix: make start.sh --stop skip nodes in other directories A directory with a start.log but no pid file was used by an earlier version of the script, and --stop found its node by the jar name alone, so it also stopped nodes with the same jar name in other directories. Pick the node whose working directory is the current directory, read from /proc on Linux or with lsof on macOS. When neither is available, exit with an error that asks for the process id in the pid file. --- shell.md | 2 +- start.sh | 31 ++++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/shell.md b/shell.md index 026f9769095..286a128c398 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. A node started by an earlier version of the script (a `start.log` but no pid file) is found by its jar name among the processes running from that directory, read from `/proc` on Linux or with `lsof` on macOS, so nodes with the same jar name in other directories keep running. Where neither is available, `--stop` exits with an error; write the node's process id to the pid file and run it again. * Get the latest version of `FullNode.jar` and start it diff --git a/start.sh b/start.sh index 8244c0d7ecd..1ed2d455269 100644 --- a/start.sh +++ b/start.sh @@ -401,10 +401,31 @@ matchNodePid() { done } +# 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. The working directory is read from /proc on Linux and with lsof on macOS; for +# another user's process both work only as root, as does stopping it. +matchDirPid() { + local here candidate dir + here=$(pwd -P) + while read -r candidate; do + if [ -d /proc/self ]; then + dir=$(readlink "/proc/$candidate/cwd" 2>/dev/null) + else + dir=$(lsof -a -p "$candidate" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p') + fi + if [ "$dir" = "$here" ]; then + echo "$candidate" + fi + 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. +# which kept no pid file; there $pid holds the processes that run the jar name from this +# directory. Without /proc or lsof they cannot be told from nodes in other directories, so the +# script exits and asks for the node's process id in the pid file. checkPid() { local pidFile="${JAR_NAME##*/}.pid" local saved @@ -420,7 +441,11 @@ checkPid() { fi fi elif [ -f start.log ]; then - pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid) + if [ ! -d /proc/self ] && ! command -v lsof >/dev/null 2>&1; then + error "no $pidFile, and without /proc or lsof the node started here cannot be told from other $JAR_NAME processes; write its process id to $PWD/$pidFile and run again" + exit 1 + fi + pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid | matchDirPid) fi } From 9ea420df5deb0ad54058f11176a8eef3411652ea Mon Sep 17 00:00:00 2001 From: GrapeS Date: Fri, 30 Oct 2026 15:10:40 +0800 Subject: [PATCH 2/3] fix: check process directories on both start.sh --stop paths The pid file path matched only the jar name, so a recorded id reused by a node that runs the same jar from another directory would have stopped that node. A recorded process whose working directory reads as another directory is now taken as a stopped node and the pid file is emptied; one whose directory cannot be read keeps the pid file and is stopped as before. Without a pid file, a process whose working directory could not be read was skipped silently, so --stop could report the node stopped while it was still running. If this user can signal such a process, --stop and restart now end with an error on stderr; a process this user cannot signal is left running with a warning. --- shell.md | 2 +- start.sh | 62 +++++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/shell.md b/shell.md index 286a128c398..22b09643531 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 found by its jar name among the processes running from that directory, read from `/proc` on Linux or with `lsof` on macOS, so nodes with the same jar name in other directories keep running. Where neither is available, `--stop` exits with an error; write the node's process id to the pid file and run it again. + `--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` only stops a process that runs the jar from that directory, read 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. 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. * Get the latest version of `FullNode.jar` and start it diff --git a/start.sh b/start.sh index 1ed2d455269..f2f774891fc 100644 --- a/start.sh +++ b/start.sh @@ -401,51 +401,75 @@ matchNodePid() { done } +# 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. The working directory is read from /proc on Linux and with lsof on macOS; for -# another user's process both work only as root, as does stopping it. +# 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 + local here candidate dir unread='' here=$(pwd -P) while read -r candidate; do - if [ -d /proc/self ]; then - dir=$(readlink "/proc/$candidate/cwd" 2>/dev/null) - else - dir=$(lsof -a -p "$candidate" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p') - fi + 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. 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. Without /proc or lsof they cannot be told from nodes in other directories, so the -# script exits and asks for the node's process id in the pid file. +# 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 - if [ ! -d /proc/self ] && ! command -v lsof >/dev/null 2>&1; then - error "no $pidFile, and without /proc or lsof the node started here cannot be told from other $JAR_NAME processes; write its process id to $PWD/$pidFile and run again" + if ! pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid | matchDirPid); then exit 1 fi - pid=$(ps -A -o pid=,args= 2>/dev/null | matchNodePid | matchDirPid) fi } From b2a4b675810e2624115a64cdcbc33cf8f1e562df Mon Sep 17 00:00:00 2001 From: GrapeS Date: Fri, 30 Oct 2026 17:02:39 +0800 Subject: [PATCH 3/3] fix: fail start.sh --stop when the node cannot be stopped stopService ended with sleep 5 and returned success even when the node was still running: a process this user cannot signal made every kill fail until the timeout, and a restart then started another node and wrote its id over the pid file of the running one. A failed signal now counts while the process still exists, and a node still running after SIGKILL fails too; both report an error on stderr and return 2, which ends --stop with that status and stops a restart before it starts a node. A return of 1 still means no node was ever started from the directory, so --run starts the node as before. shell.md now states that the directory check needs a readable working directory, and that a pid file whose process cannot be inspected is trusted as recorded. --- shell.md | 2 +- start.sh | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/shell.md b/shell.md index 22b09643531..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. `--stop` only stops a process that runs the jar from that directory, read 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. 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. + `--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 f2f774891fc..1ba68a4e36f 100644 --- a/start.sh +++ b/start.sh @@ -484,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" @@ -495,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 @@ -791,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