From 362e27eefdeafe9b487f92fd8bea3d7196cb4435 Mon Sep 17 00:00:00 2001 From: Avanish Shukla Date: Tue, 29 Sep 2026 16:48:35 +0530 Subject: [PATCH 1/4] feat(dist): make conf/logs/plugins/pid paths overridable and fix log4j2 configs resolution ( #3236 ) --- .../assembly/static/bin/start-hugegraph-pd.sh | 15 ++++--- .../assembly/static/bin/stop-hugegraph-pd.sh | 9 +++- .../src/assembly/static/conf/log4j2.xml | 3 +- .../assembly/static/bin/hugegraph-server.sh | 8 ++-- .../assembly/static/bin/start-hugegraph.sh | 43 +++++++++++-------- .../src/assembly/static/bin/stop-hugegraph.sh | 9 ++-- .../src/assembly/static/bin/util.sh | 2 +- .../src/assembly/static/conf/log4j2.xml | 3 +- .../static/bin/start-hugegraph-store.sh | 22 ++++++---- .../static/bin/stop-hugegraph-store.sh | 9 +++- .../src/assembly/static/conf/log4j2.xml | 3 +- 11 files changed, 79 insertions(+), 47 deletions(-) diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh index 82a92dc9f9..b334d2c9ee 100755 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh @@ -30,14 +30,17 @@ if [ -z "$DAEMON" ]; then DAEMON="true" fi -while getopts "d:g:j:y:" arg; do +while getopts "c:d:g:i:j:l:y:" arg; do case ${arg} in + c) CONF_OVERRIDE="$OPTARG" ;; g) GC_OPTION="$OPTARG" ;; + i) PID_FILE_OVERRIDE="$OPTARG" ;; j) USER_OPTION="$OPTARG" ;; + l) LOGS_OVERRIDE="$OPTARG" ;; # Telemetry is used to collect metrics, traces and logs d) DAEMON="$OPTARG" ;; y) OPEN_TELEMETRY="$OPTARG" ;; - ?) echo "USAGE: $0 [-d true|false] [-g g1] [-j xxx] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-y true|false]" && exit 1 ;; esac done @@ -53,13 +56,13 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" -CONF="$TOP/conf" +CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" PLUGINS="$TOP/plugins" -LOGS="$TOP/logs" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" OUTPUT=${LOGS}/hugegraph-pd-stdout.log GITHUB="https://github.com" -PID_FILE="$BIN/pid" +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" . "$BIN"/util.sh @@ -171,7 +174,7 @@ if [ $(ps -ef|grep -v grep| grep java|grep -cE ${CONF}) -ne 0 ]; then exit 0 fi -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" +JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" # Turn on security check if [[ $DAEMON == "true" ]]; then diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh index 2220dd678f..1f7ccf076a 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh @@ -27,12 +27,19 @@ abs_path() { echo "$( cd -P "$( dirname "$SOURCE" )" && pwd )" } +while getopts "i:" arg; do + case ${arg} in + i) PID_FILE_OVERRIDE="$OPTARG" ;; + ?) echo "USAGE: $0 [-i pid_file]" && exit 1 ;; + esac +done + BIN=$(abs_path) TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh -PID_FILE=$BIN/pid +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=30 if [ ! -f ${PID_FILE} ]; then diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/log4j2.xml b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/log4j2.xml index e0b5d9dc98..9c8c8f0ff4 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/log4j2.xml +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/conf/log4j2.xml @@ -20,7 +20,8 @@ - logs + + ${sys:LOG_PATH:-logs} hugegraph-pd diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh index 2c93aa085c..de5796a4b9 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh @@ -34,11 +34,11 @@ fi BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" -CONF="$TOP/conf" +CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" EXT="$TOP/ext" PLUGINS="$TOP/plugins" -LOGS="$TOP/logs" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" OUTPUT=${LOGS}/hugegraph-server.log GITHUB="https://github.com" @@ -155,7 +155,8 @@ case "$GC_OPTION" in exit 1 esac -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml" +JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS}" + SECURITY_MANAGER_OPTION="" if [[ ${OPEN_SECURITY_CHECK} == "true" ]]; then if [[ ${JAVA_VERSION} -gt ${MAX_SECURITY_JAVA_VERSION} ]]; then @@ -225,7 +226,6 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then fi fi - # Note: remember update it if we change the jar expected_md5="e3bcbbe8ed9b6d840fa4c333b36f369f" actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh index 46044e932b..710c65db3b 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh @@ -39,29 +39,36 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" -CONF="$TOP/conf" -LOGS="$TOP/logs" SCRIPTS="$TOP/scripts" -PID_FILE="$BIN/pid" . "$BIN"/util.sh # Note: keep ':' in the end of the string to indicate the option needs a value -while getopts "d:g:m:p:s:j:t:y:" arg; do - case ${arg} in - d) DAEMON="$OPTARG" ;; - g) GC_OPTION="$OPTARG" ;; - m) OPEN_MONITOR="$OPTARG" ;; - p) PRELOAD="$OPTARG" ;; - s) OPEN_SECURITY_CHECK="$OPTARG" ;; - j) USER_OPTION="$OPTARG" ;; - t) SERVER_STARTUP_TIMEOUT_S="$OPTARG" ;; - # Telemetry is used to collect metrics, traces and logs - y) OPEN_TELEMETRY="$OPTARG" ;; - # Note: update usage info when the params changed - ?) exit_with_usage_help ;; - esac -done +while getopts "c:d:g:i:j:l:m:p:s:t:y:" arg; do + case ${arg} in + c) CONF_OVERRIDE="$OPTARG" ;; + d) DAEMON="$OPTARG" ;; + g) GC_OPTION="$OPTARG" ;; + i) PID_FILE_OVERRIDE="$OPTARG" ;; + j) USER_OPTION="$OPTARG" ;; + l) LOGS_OVERRIDE="$OPTARG" ;; + m) OPEN_MONITOR="$OPTARG" ;; + p) PRELOAD="$OPTARG" ;; + s) OPEN_SECURITY_CHECK="$OPTARG" ;; + t) SERVER_STARTUP_TIMEOUT_S="$OPTARG" ;; + # Telemetry is used to collect metrics, traces and logs + y) OPEN_TELEMETRY="$OPTARG" ;; + # Note: update usage info when the params changed + ?) exit_with_usage_help ;; + esac + done + +CONF="${CONF_OVERRIDE:-$TOP/conf}" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" + +. "$BIN"/util.sh +export CONF_OVERRIDE LOGS_OVERRIDE if [[ "$OPEN_MONITOR" != "true" && "$OPEN_MONITOR" != "false" ]]; then exit_with_usage_help diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh index c63035e072..c29ea79e51 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh @@ -17,15 +17,16 @@ # CLOSE_MONITOR="true" -while getopts "m:" arg; do +while getopts "i:m:" arg; do case ${arg} in + i) PID_FILE_OVERRIDE="$OPTARG" ;; m) CLOSE_MONITOR="$OPTARG" ;; - ?) echo "USAGE: $0 [-m true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-i pid_file] [-m true|false]" && exit 1 ;; esac done if [[ "$CLOSE_MONITOR" != "true" && "$CLOSE_MONITOR" != "false" ]]; then - echo "USAGE: $0 [-m true|false]" + echo "USAGE: $0 [-i pid_file] [-m true|false]" exit 1 fi @@ -44,7 +45,7 @@ TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh -PID_FILE=$BIN/pid +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=10 if [ "$CLOSE_MONITOR" == "true" ]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index 8ae46e88a4..3fb6d0b442 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -655,6 +655,6 @@ function kill_process_and_wait() { } function exit_with_usage_help(){ - echo "USAGE: $0 [-d true|false] [-g g1] [-m true|false] [-p true|false] [-s true|false] [-j java_options] [-t timeout] [-y true|false]" + echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-l logs_dir] [-m true|false] [-p true|false] [-s true|false] [-j java_options] [-t timeout] [-y true|false]" exit 1 } diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml index 6286314d7b..36109db288 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/conf/log4j2.xml @@ -18,7 +18,8 @@ - logs + + ${sys:LOG_PATH:-logs} hugegraph-server diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh index 15006bb777..ca674b316a 100755 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh @@ -29,13 +29,8 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" -CONF="$TOP/conf" LIB="$TOP/lib" -PLUGINS="$TOP/plugins" -LOGS="$TOP/logs" -OUTPUT=${LOGS}/hugegraph-store-server.log GITHUB="https://github.com" -PID_FILE="$BIN/pid" . "$BIN"/util.sh @@ -105,17 +100,26 @@ if [ -z "$DAEMON" ]; then DAEMON="true" fi -while getopts "d:g:j:y:" arg; do +while getopts "d:c:g:i:j:l:y:" arg; do case ${arg} in + c) CONF_OVERRIDE="$OPTARG" ;; g) GC_OPTION="$OPTARG" ;; + i) PID_FILE_OVERRIDE="$OPTARG" ;; j) USER_OPTION="$OPTARG" ;; + l) LOGS_OVERRIDE="$OPTARG" ;; # Telemetry is used to collect metrics, traces and logs y) OPEN_TELEMETRY="$OPTARG" ;; d) DAEMON="$OPTARG" ;; - ?) echo "USAGE: $0 [-d true|false] [-g g1] [-j xxx] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-d true|false] [-c conf_dir] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-y true|false]" && exit 1 ;; esac done +CONF="${CONF_OVERRIDE:-$TOP/conf}" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" +PLUGINS="$TOP/plugins" +OUTPUT=${LOGS}/hugegraph-store-server.log +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" + ensure_path_writable "$LOGS" ensure_path_writable "$PLUGINS" @@ -156,7 +160,7 @@ if [ "$JAVA_OPTIONS" = "" ]; then # JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" # Rolling out detailed GC logs - JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=./logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" + JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=./logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" fi # Using G1GC as the default garbage collector (Recommended for large memory machines) @@ -178,7 +182,7 @@ case "$GC_OPTION" in exit 1 esac -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dfastjson.parser.safeMode=true -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" +JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" if [ "${OPEN_TELEMETRY}" == "true" ]; then OT_JAR="opentelemetry-javaagent.jar" diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh index 8f898df7da..0acf989f07 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh @@ -27,12 +27,19 @@ abs_path() { echo "$( cd -P "$( dirname "$SOURCE" )" && pwd )" } +while getopts "i:" arg; do + case ${arg} in + i) PID_FILE_OVERRIDE="$OPTARG" ;; + ?) echo "USAGE: $0 [-i pid_file]" && exit 1 ;; + esac +done + BIN=$(abs_path) TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh -PID_FILE=$BIN/pid +PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=30 if [ ! -f ${PID_FILE} ]; then diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/conf/log4j2.xml b/hugegraph-store/hg-store-dist/src/assembly/static/conf/log4j2.xml index f0cf11fc3d..5e9ba2f002 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/conf/log4j2.xml +++ b/hugegraph-store/hg-store-dist/src/assembly/static/conf/log4j2.xml @@ -20,7 +20,8 @@ - logs + + ${sys:LOG_PATH:-logs} hugegraph-store raft-hugegraph-store audit-hugegraph-store From 526bf2ef6342119391c9bd64c21cb696aba6df94 Mon Sep 17 00:00:00 2001 From: Avanish Shukla Date: Sat, 3 Oct 2026 00:37:32 +0530 Subject: [PATCH 2/4] feat(scripts): Added plugins override, fix store restart, monitor, canonicalise overridden paths & fix store GC paths - Add plugins directory override with -o flag - Pass conf and plugins overrides to init-store.sh - Route GC log output to ${LOGS} (fallback ./logs) instead of hardcoded path - Persist path overrides into monitor-hugegraph.sh cron env so -m true restarts reuse the correct conf/logs/pid paths - Enable Fastjson safe mode (-Dfastjson.parser.safeMode=true) in Store JVM - Run restart-hugegraph-store.sh steps as separate subprocesses, passing -i to stop and all flags to start to avoid shared getopts/OPTIND clashes --- .../assembly/static/bin/start-hugegraph-pd.sh | 18 +++++--- .../src/assembly/static/bin/util.sh | 26 +++++++++++ .../assembly/static/bin/hugegraph-server.sh | 2 +- .../assembly/static/bin/monitor-hugegraph.sh | 21 ++++++--- .../assembly/static/bin/start-hugegraph.sh | 15 +++++-- .../src/assembly/static/bin/start-monitor.sh | 17 ++++++- .../src/assembly/static/bin/util.sh | 27 +++++++++++ .../static/bin/restart-hugegraph-store.sh | 45 ++++++++++++++++++- .../static/bin/start-hugegraph-store.sh | 17 ++++--- .../src/assembly/static/bin/util.sh | 26 +++++++++++ 10 files changed, 189 insertions(+), 25 deletions(-) diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh index b334d2c9ee..f165209b97 100755 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh @@ -30,17 +30,18 @@ if [ -z "$DAEMON" ]; then DAEMON="true" fi -while getopts "c:d:g:i:j:l:y:" arg; do +while getopts "c:d:g:i:j:l:o:y:" arg; do case ${arg} in c) CONF_OVERRIDE="$OPTARG" ;; g) GC_OPTION="$OPTARG" ;; i) PID_FILE_OVERRIDE="$OPTARG" ;; j) USER_OPTION="$OPTARG" ;; l) LOGS_OVERRIDE="$OPTARG" ;; + o) PLUGINS_OVERRIDE="$OPTARG" ;; # Telemetry is used to collect metrics, traces and logs d) DAEMON="$OPTARG" ;; y) OPEN_TELEMETRY="$OPTARG" ;; - ?) echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-o plugins_dir] [-y true|false]" && exit 1 ;; esac done @@ -56,16 +57,23 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" + +. "$BIN"/util.sh + +# Canonicalize relative path overrides to absolute paths +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" + CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" -PLUGINS="$TOP/plugins" +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" OUTPUT=${LOGS}/hugegraph-pd-stdout.log GITHUB="https://github.com" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" -. "$BIN"/util.sh - ensure_path_writable "$LOGS" ensure_path_writable "$PLUGINS" diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh index b476d7935b..3d46b06139 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh @@ -229,6 +229,32 @@ function ensure_path_writable() { fi } +# Canonicalize a (possibly relative) directory override into an absolute path. +function canonicalize_dir() { + local path="$1" + [ -z "$path" ] && return 0 + mkdir -p "$path" 2>/dev/null + (cd "$path" 2>/dev/null && pwd) || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } +} + +# Canonicalize a (possibly relative) file override (e.g. the pid file) into an absolute path +function canonicalize_file() { + local path="$1" + [ -z "$path" ] && return 0 + local dir file abs_dir + dir="$(dirname "$path")" + file="$(basename "$path")" + mkdir -p "$dir" 2>/dev/null + abs_dir="$(cd "$dir" 2>/dev/null && pwd)" || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } + echo "$abs_dir/$file" +} + function get_ip() { local os=$(uname) local loopback="127.0.0.1" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh index de5796a4b9..6921a26f5c 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh @@ -37,7 +37,7 @@ TOP="$(cd "$BIN"/../ && pwd)" CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" EXT="$TOP/ext" -PLUGINS="$TOP/plugins" +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" OUTPUT=${LOGS}/hugegraph-server.log GITHUB="https://github.com" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh index 11be239fe1..2a43b0829c 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh @@ -30,15 +30,18 @@ TOP="$(cd $BIN/../ && pwd)" . $BIN/util.sh -SERVER_URL=`read_property "$TOP/conf/rest-server.properties" "restserver.url"` +CONF="${CONF_OVERRIDE:-$TOP/conf}" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" + +SERVER_URL=`read_property "$CONF/rest-server.properties" "restserver.url"` DETECT_URL="$SERVER_URL/versions" PROC_NAME="HugeGraphServer" -LOG_DIR="$TOP/logs" +LOG_DIR="$LOGS" MONITOR_LOG="$LOG_DIR/monitor.log" if [ ! -d $LOG_DIR ]; then - mkdir $LOG_DIR + mkdir -p $LOG_DIR fi function record_monitor_log() { @@ -48,12 +51,16 @@ function record_monitor_log() { function restart_server() { local stop_old=$1 if [ "$stop_old" == "true" ]; then - # Don't remove monitor - $BIN/stop-hugegraph.sh false + # Don't remove monitor; stop the pid file the caller used. + $BIN/stop-hugegraph.sh ${PID_FILE_OVERRIDE:+-i "$PID_FILE_OVERRIDE"} false fi record_monitor_log "Ready to restart $PROC_NAME" - # Don't add monitor again - $BIN/start-hugegraph.sh -m false + # Don't add monitor again, but restart with path overrides + $BIN/start-hugegraph.sh -m false \ + ${CONF_OVERRIDE:+-c "$CONF_OVERRIDE"} \ + ${LOGS_OVERRIDE:+-l "$LOGS_OVERRIDE"} \ + ${PLUGINS_OVERRIDE:+-o "$PLUGINS_OVERRIDE"} \ + ${PID_FILE_OVERRIDE:+-i "$PID_FILE_OVERRIDE"} if [ $? -ne 0 ]; then record_monitor_log "Failed to restart $PROC_NAME" exit 1 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh index 710c65db3b..987a920c1b 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh @@ -44,7 +44,7 @@ SCRIPTS="$TOP/scripts" . "$BIN"/util.sh # Note: keep ':' in the end of the string to indicate the option needs a value -while getopts "c:d:g:i:j:l:m:p:s:t:y:" arg; do +while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do case ${arg} in c) CONF_OVERRIDE="$OPTARG" ;; d) DAEMON="$OPTARG" ;; @@ -53,6 +53,7 @@ while getopts "c:d:g:i:j:l:m:p:s:t:y:" arg; do j) USER_OPTION="$OPTARG" ;; l) LOGS_OVERRIDE="$OPTARG" ;; m) OPEN_MONITOR="$OPTARG" ;; + o) PLUGINS_OVERRIDE="$OPTARG" ;; p) PRELOAD="$OPTARG" ;; s) OPEN_SECURITY_CHECK="$OPTARG" ;; t) SERVER_STARTUP_TIMEOUT_S="$OPTARG" ;; @@ -61,14 +62,20 @@ while getopts "c:d:g:i:j:l:m:p:s:t:y:" arg; do # Note: update usage info when the params changed ?) exit_with_usage_help ;; esac - done +done + +# Canonicalize relative path overrides to absolute paths +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" CONF="${CONF_OVERRIDE:-$TOP/conf}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" -. "$BIN"/util.sh -export CONF_OVERRIDE LOGS_OVERRIDE +export CONF_OVERRIDE LOGS_OVERRIDE PID_FILE_OVERRIDE PLUGINS_OVERRIDE if [[ "$OPEN_MONITOR" != "true" && "$OPEN_MONITOR" != "false" ]]; then exit_with_usage_help diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh index ffe1a2243d..eca9d6c8e7 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh @@ -37,6 +37,21 @@ fi # Monitor HugeGraphServer every minute, if the server crashes then restart it. # Modify the frequency according to actual needs carefully. -CRONTAB_JOB="*/1 * * * * export JAVA_HOME=$JAVA_HOME && $TOP/bin/monitor-hugegraph.sh" + +# Persist any path overrides (-c/-l/-i/-o) that the caller (start-hugegraph.sh) exported +CRONTAB_JOB="*/1 * * * * export JAVA_HOME=$JAVA_HOME &&" +if [ -n "$CONF_OVERRIDE" ]; then + CRONTAB_JOB="$CRONTAB_JOB export CONF_OVERRIDE='$CONF_OVERRIDE' &&" +fi +if [ -n "$LOGS_OVERRIDE" ]; then + CRONTAB_JOB="$CRONTAB_JOB export LOGS_OVERRIDE='$LOGS_OVERRIDE' &&" +fi +if [ -n "$PID_FILE_OVERRIDE" ]; then + CRONTAB_JOB="$CRONTAB_JOB export PID_FILE_OVERRIDE='$PID_FILE_OVERRIDE' &&" +fi +if [ -n "$PLUGINS_OVERRIDE" ]; then + CRONTAB_JOB="$CRONTAB_JOB export PLUGINS_OVERRIDE='$PLUGINS_OVERRIDE' &&" +fi +CRONTAB_JOB="$CRONTAB_JOB $TOP/bin/monitor-hugegraph.sh" crontab_append "$CRONTAB_JOB" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index 3fb6d0b442..e380ba266d 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -484,6 +484,33 @@ function ensure_path_writable() { fi } +# Canonicalize a (possibly relative) directory override into an absolute path. +function canonicalize_dir() { + local path="$1" + [ -z "$path" ] && return 0 + mkdir -p "$path" 2>/dev/null + (cd "$path" 2>/dev/null && pwd) || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } +} + +# Canonicalize a (possibly relative) file override (e.g. the pid file) into an +# absolute path . +function canonicalize_file() { + local path="$1" + [ -z "$path" ] && return 0 + local dir file abs_dir + dir="$(dirname "$path")" + file="$(basename "$path")" + mkdir -p "$dir" 2>/dev/null + abs_dir="$(cd "$dir" 2>/dev/null && pwd)" || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } + echo "$abs_dir/$file" +} + function get_ip() { local os=$(uname) local loopback="127.0.0.1" diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/restart-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/restart-hugegraph-store.sh index d1655353be..35d56f9a4c 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/restart-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/restart-hugegraph-store.sh @@ -28,5 +28,46 @@ function abs_path() { } BIN=$(abs_path) -. "$BIN"/stop-hugegraph-store.sh -. "$BIN"/start-hugegraph-store.sh + +DAEMON_ARG="" +CONF_ARG="" +GC_ARG="" +PID_ARG="" +USER_ARG="" +LOGS_ARG="" +PLUGINS_ARG="" +TELEMETRY_ARG="" + +while getopts "d:c:g:i:j:l:o:y:" arg; do + case ${arg} in + d) DAEMON_ARG="$OPTARG" ;; + c) CONF_ARG="$OPTARG" ;; + g) GC_ARG="$OPTARG" ;; + i) PID_ARG="$OPTARG" ;; + j) USER_ARG="$OPTARG" ;; + l) LOGS_ARG="$OPTARG" ;; + o) PLUGINS_ARG="$OPTARG" ;; + y) TELEMETRY_ARG="$OPTARG" ;; + ?) echo "USAGE: $0 [-d true|false] [-c conf_dir] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-o plugins_dir] [-y true|false]" && exit 1 ;; + esac +done + +# stop-hugegraph-store.sh only understands -i (the pid file).Forward the -i flag to stop script +STOP_ARGS=() +[ -n "$PID_ARG" ] && STOP_ARGS+=(-i "$PID_ARG") + +# Forward every override to start so the restarted instance also keeps the overriden +# conf/logs/pid/plugins/daemon/gc/telemetry settings +START_ARGS=() +[ -n "$DAEMON_ARG" ] && START_ARGS+=(-d "$DAEMON_ARG") +[ -n "$CONF_ARG" ] && START_ARGS+=(-c "$CONF_ARG") +[ -n "$GC_ARG" ] && START_ARGS+=(-g "$GC_ARG") +[ -n "$PID_ARG" ] && START_ARGS+=(-i "$PID_ARG") +[ -n "$USER_ARG" ] && START_ARGS+=(-j "$USER_ARG") +[ -n "$LOGS_ARG" ] && START_ARGS+=(-l "$LOGS_ARG") +[ -n "$PLUGINS_ARG" ] && START_ARGS+=(-o "$PLUGINS_ARG") +[ -n "$TELEMETRY_ARG" ] && START_ARGS+=(-y "$TELEMETRY_ARG") + +# Run stop and start as separate processes so that: +bash "$BIN"/stop-hugegraph-store.sh "${STOP_ARGS[@]}" +bash "$BIN"/start-hugegraph-store.sh "${START_ARGS[@]}" diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh index ca674b316a..8ccdd184b6 100755 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh @@ -100,23 +100,30 @@ if [ -z "$DAEMON" ]; then DAEMON="true" fi -while getopts "d:c:g:i:j:l:y:" arg; do +while getopts "d:c:g:i:j:l:o:y:" arg; do case ${arg} in c) CONF_OVERRIDE="$OPTARG" ;; g) GC_OPTION="$OPTARG" ;; i) PID_FILE_OVERRIDE="$OPTARG" ;; j) USER_OPTION="$OPTARG" ;; l) LOGS_OVERRIDE="$OPTARG" ;; + o) PLUGINS_OVERRIDE="$OPTARG" ;; # Telemetry is used to collect metrics, traces and logs y) OPEN_TELEMETRY="$OPTARG" ;; d) DAEMON="$OPTARG" ;; - ?) echo "USAGE: $0 [-d true|false] [-c conf_dir] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-d true|false] [-c conf_dir] [-g g1] [-i pid_file] [-j opts] [-l logs_dir] [-o plugins_dir] [-y true|false]" && exit 1 ;; esac done +# Canonicalize relative path overrides to absolute paths +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" + CONF="${CONF_OVERRIDE:-$TOP/conf}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" -PLUGINS="$TOP/plugins" +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" OUTPUT=${LOGS}/hugegraph-store-server.log PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" @@ -160,7 +167,7 @@ if [ "$JAVA_OPTIONS" = "" ]; then # JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" # Rolling out detailed GC logs - JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=./logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" + JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=${LOGS}/logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" fi # Using G1GC as the default garbage collector (Recommended for large memory machines) @@ -182,7 +189,7 @@ case "$GC_OPTION" in exit 1 esac -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" +JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dfastjson.parser.safeMode=true -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" if [ "${OPEN_TELEMETRY}" == "true" ]; then OT_JAR="opentelemetry-javaagent.jar" diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh index e45249a04b..58f761c706 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh @@ -228,6 +228,32 @@ function ensure_path_writable() { fi } +# Canonicalize a (possibly relative) directory override into an absolute path. +function canonicalize_dir() { + local path="$1" + [ -z "$path" ] && return 0 + mkdir -p "$path" 2>/dev/null + (cd "$path" 2>/dev/null && pwd) || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } +} + +# Canonicalize a (possibly relative) file override (e.g. the pid file) into an absolute path +function canonicalize_file() { + local path="$1" + [ -z "$path" ] && return 0 + local dir file abs_dir + dir="$(dirname "$path")" + file="$(basename "$path")" + mkdir -p "$dir" 2>/dev/null + abs_dir="$(cd "$dir" 2>/dev/null && pwd)" || { + echo "Error: cannot resolve path '$path'" >&2 + exit 1 + } + echo "$abs_dir/$file" +} + function get_ip() { local os=$(uname) local loopback="127.0.0.1" From e272e27c96f733f88f6b2e1163be0b10a7b8a7d4 Mon Sep 17 00:00:00 2001 From: Avanish Shukla Date: Thu, 8 Oct 2026 11:21:51 +0530 Subject: [PATCH 3/4] fix(scripts): harden path overrides and quote cron monitor command - Exit when canonicalize_dir/canonicalize_file fails - Create and check the PID file's parent dir before launch (server, PD, store) so the PID write cannot fail after startup - Quote PID_FILE/CONF/LOGS (and OUTPUT, derived from LOGS) in tests, reads and redirects in the start and stop scripts - Quote BIN/TOP in start-monitor.sh, stop-monitor.sh and monitor-hugegraph.sh - Serialize JAVA_HOME, the four overrides and the monitor path into the cron line as one POSIX shell word - Reject CR/LF before canonicalization, and % when -m true - Write the cron entry with printf '%s\n'; match add/remove on the same serialized key so apostrophe paths stay removable - Add -o plugins_dir to the usage text in util.sh - Store: write gc.log under ${LOGS} - Add test-cron-quote.sh (spaces, apostrophes, backslashes, shell metacharacters, dash, duplicate add/remove, rejected input) - Add test-canonicalize.sh (absolute, relative and nested paths, unresolvable parents, nocreate mode for the stop scripts) --- .../assembly/static/bin/start-hugegraph-pd.sh | 33 +- .../assembly/static/bin/stop-hugegraph-pd.sh | 4 +- .../src/assembly/static/bin/util.sh | 49 ++- .../assembly/static/bin/hugegraph-server.sh | 11 +- .../src/assembly/static/bin/init-store.sh | 23 +- .../assembly/static/bin/monitor-hugegraph.sh | 16 +- .../assembly/static/bin/start-hugegraph.sh | 34 +- .../src/assembly/static/bin/start-monitor.sh | 29 +- .../src/assembly/static/bin/stop-hugegraph.sh | 2 +- .../src/assembly/static/bin/stop-monitor.sh | 12 +- .../assembly/static/bin/test-canonicalize.sh | 143 +++++++++ .../assembly/static/bin/test-cron-quote.sh | 292 ++++++++++++++++++ .../src/assembly/static/bin/util.sh | 51 ++- .../static/bin/start-hugegraph-store.sh | 37 +-- .../static/bin/stop-hugegraph-store.sh | 4 +- .../src/assembly/static/bin/util.sh | 49 ++- 16 files changed, 691 insertions(+), 98 deletions(-) create mode 100644 hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh create mode 100755 hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh index f165209b97..5a34a14c87 100755 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh @@ -60,11 +60,11 @@ TOP="$(cd "$BIN"/../ && pwd)" . "$BIN"/util.sh -# Canonicalize relative path overrides to absolute paths -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" -LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" -PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" -PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" +# Canonicalize relative path overrides to absolute paths. +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" || exit 1 +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" || exit 1 CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" @@ -76,6 +76,7 @@ PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" ensure_path_writable "$LOGS" ensure_path_writable "$PLUGINS" +ensure_path_writable "$(dirname "$PID_FILE")" # The maximum and minimum heap memory that service can use MAX_MEM=$((32 * 1024)) @@ -98,7 +99,7 @@ JAVA_VERSION=$($JAVA -version 2>&1 | sed 's/^1\.//' | cut -d'.' -f1) JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then - echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> ${OUTPUT} + echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> "${OUTPUT}" exit 1 fi @@ -107,7 +108,7 @@ if [ "$JAVA_OPTIONS" = "" ]; then XMX=$(calc_xmx $MIN_MEM $MAX_MEM) if [ $? -ne 0 ]; then echo "Failed to start HugeGraphPDServer, requires at least ${MIN_MEM}m free memory" \ - >> ${OUTPUT} + >> "${OUTPUT}" exit 1 fi JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" @@ -132,7 +133,7 @@ case "$GC_OPTION" in -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" ;; *) - echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> ${OUTPUT} + echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> "${OUTPUT}" exit 1 esac @@ -146,7 +147,7 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then "${GITHUB}/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.1.0/${OT_JAR}" if [[ ! -e "${OT_JAR_PATH}" ]]; then - echo "## Error: Failed to download ${OT_JAR}." >>${OUTPUT} + echo "## Error: Failed to download ${OT_JAR}." >>"${OUTPUT}" exit 1 fi fi @@ -156,8 +157,8 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') if [[ "${expected_md5}" != "${actual_md5}" ]]; then - echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>${OUTPUT} - echo "## Tips: Remove the file and try again." >>${OUTPUT} + echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>"${OUTPUT}" + echo "## Tips: Remove the file and try again." >>"${OUTPUT}" exit 1 fi @@ -177,7 +178,7 @@ fi #if [ "${JMX_EXPORT_PORT}" != "" ] && [ ${JMX_EXPORT_PORT} -ne 0 ] ; then # JAVA_OPTIONS="${JAVA_OPTIONS} -javaagent:${LIB}/jmx_prometheus_javaagent-0.16.1.jar=${JMX_EXPORT_PORT}:${CONF}/jmx_exporter.yml" #fi -if [ $(ps -ef|grep -v grep| grep java|grep -cE ${CONF}) -ne 0 ]; then +if [ "$(ps -ef | grep -v grep | grep java | grep -cE "${CONF}")" -ne 0 ]; then echo "HugeGraphPDServer is already running..." exit 0 fi @@ -189,10 +190,10 @@ if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphPDServer in daemon mode..." if [[ "${STDOUT_MODE:-false}" == "true" ]]; then exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar & + -Dspring.config.location="${CONF}"/application.yml ${LIB}/hg-pd-service-*.jar & else exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar >> ${OUTPUT} 2>&1 & + -Dspring.config.location="${CONF}"/application.yml ${LIB}/hg-pd-service-*.jar >> "${OUTPUT}" 2>&1 & fi PID="$!" # Write pid to file @@ -205,9 +206,9 @@ else echo "[+pid] $$" if [[ "${STDOUT_MODE:-false}" == "true" ]]; then exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar + -Dspring.config.location="${CONF}"/application.yml ${LIB}/hg-pd-service-*.jar else exec ${JAVA} -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml ${LIB}/hg-pd-service-*.jar >> ${OUTPUT} 2>&1 + -Dspring.config.location="${CONF}"/application.yml ${LIB}/hg-pd-service-*.jar >> "${OUTPUT}" 2>&1 fi fi diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh index 1f7ccf076a..c00d2f76e5 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh @@ -42,12 +42,12 @@ TOP="$(cd $BIN/../ && pwd)" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=30 -if [ ! -f ${PID_FILE} ]; then +if [ ! -f "${PID_FILE}" ]; then echo "The pid file $PID_FILE doesn't exist" exit 0 fi -PID=$(cat $PID_FILE) +PID=$(cat "$PID_FILE") kill_process_and_wait "HugeGraphPDServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S" if [ $? -eq 0 ]; then diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh index 3d46b06139..377b74675f 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh @@ -82,27 +82,64 @@ function process_id() { return "$pid" } +# Emit $1 as ONE complete POSIX shell word, INCLUDING the surrounding quotes. +# Callers should NOT add another pair of quotes: +# /data/team's-logs -> '/data/team'\''s-logs' +# +function shell_quote() { + local sq="'" repl="'\\''" + printf "'%s'" "${1//$sq/$repl}" +} + +# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron +# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE +# canonicalize_dir/canonicalize_file. +# cron : crlf + reject '%'. +function reject_unsafe_path() { + case "$1" in + *[$'\n\r']*) + echo "ERROR: $2 must not contain CR/LF characters" >&2 + return 1 ;; + esac + + [ "$3" = "cron" ] || return 0 + + case "$1" in + *%*) + echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 + return 1 ;; + esac + return 0 +} + +# Validate (cron rules) then quote. $1=value $2=label. Prints the quoted word. +function cron_quote() { + reject_unsafe_path "$1" "$2" cron || return 1 + shell_quote "$1" +} + function crontab_append() { local job="$1" - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 fi - (crontab -l ; echo "$job") | crontab - + # printf, not echo: echo may interpret backslashes or swallow a leading -n/-e + { crontab -l 2>/dev/null; printf '%s\n' "$job"; } | crontab - } function crontab_remove() { local job="$1" # check exist before remove - crontab -l | grep -F "$job" >/dev/null 2>&1 - if [ $? -eq 1 ]; then + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 + if [ $? -ne 0 ]; then return 0 fi - crontab -l | grep -Fv "$job" | crontab - + crontab -l 2>/dev/null | grep -Fv -- "$job" | crontab - # Check exist after remove - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 else diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh index 6921a26f5c..48defe856b 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh @@ -81,7 +81,7 @@ CP="$CP":$(find -L $LIB -name '*.jar' \ # Add the jars in ext (at any subdirectory depth) CP="$CP":$(find -L $EXT -name '*.jar' | sort | tr '\n' ':') # Add the jars in plugins (at any subdirectory depth), check "javaagent" related jars carefully -CP="$CP":$(find -L $PLUGINS -name '*.jar' | sort | tr '\n' ':') +CP="$CP":$(find -L "$PLUGINS" -name '*.jar' | sort | tr '\n' ':') # (Cygwin only) Use ; classpath separator and reformat paths for Windows ("C:\foo") [[ $(uname) = CYGWIN* ]] && CP="$(cygpath -p -w "$CP")" @@ -221,17 +221,18 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then "${GITHUB}/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.1.0/${OT_JAR}" if [[ ! -e "${OT_JAR_PATH}" ]]; then - echo "## Error: Failed to download ${OT_JAR}." >>${OUTPUT} + echo "## Error: Failed to download ${OT_JAR}." >>"${OUTPUT}" exit 1 fi fi + # Note: remember update it if we change the jar expected_md5="e3bcbbe8ed9b6d840fa4c333b36f369f" actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') if [[ "${expected_md5}" != "${actual_md5}" ]]; then - echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>${OUTPUT} - echo "## Tips: Remove the file and try again." >>${OUTPUT} + echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>"${OUTPUT}" + echo "## Tips: Remove the file and try again." >>"${OUTPUT}" exit 1 fi @@ -267,5 +268,5 @@ else ${SECURITY_MANAGER_OPTION} -cp ${CLASSPATH}: \ org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \ ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} \ - >> ${LOGS}/hugegraph-server-stdout.log 2>&1 + >> "${LOGS}/hugegraph-server-stdout.log" 2>&1 fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh index d934192fad..b0a5e7a379 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh @@ -27,13 +27,28 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "${BIN}"/../ && pwd)" -CONF="$TOP/conf" -LIB="$TOP/lib" -PLUGINS="$TOP/plugins" -JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" + +# Support overrides via flags (for standalone invocation) or env vars (inherited +# when launched from start-hugegraph.sh). +while getopts "c:o:" arg; do + case ${arg} in + c) CONF_OVERRIDE="$OPTARG" ;; + o) PLUGINS_OVERRIDE="$OPTARG" ;; + ?) echo "USAGE: $0 [-c conf_dir] [-o plugins_dir]" && exit 1 ;; + esac +done . "${BIN}"/util.sh +# Canonicalize relative path overrides to absolute paths +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 + +CONF="${CONF_OVERRIDE:-$TOP/conf}" +LIB="$TOP/lib" +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" +JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" + configure_riscv64_libatomic || exit 1 ensure_path_writable "${PLUGINS}" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh index 2a43b0829c..da854293f3 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh @@ -25,10 +25,10 @@ function abs_path() { echo "$( cd -P "$( dirname "$SOURCE" )" && pwd )" } -BIN=`abs_path` -TOP="$(cd $BIN/../ && pwd)" +BIN="$(abs_path)" +TOP="$(cd "$BIN"/../ && pwd)" -. $BIN/util.sh +. "$BIN"/util.sh CONF="${CONF_OVERRIDE:-$TOP/conf}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" @@ -40,23 +40,23 @@ PROC_NAME="HugeGraphServer" LOG_DIR="$LOGS" MONITOR_LOG="$LOG_DIR/monitor.log" -if [ ! -d $LOG_DIR ]; then - mkdir -p $LOG_DIR +if [ ! -d "$LOG_DIR" ]; then + mkdir -p "$LOG_DIR" fi function record_monitor_log() { - echo `date '+%Y-%m-%d %H:%M:%S'`, $1 >> $MONITOR_LOG + echo `date '+%Y-%m-%d %H:%M:%S'`, $1 >> "$MONITOR_LOG" } function restart_server() { local stop_old=$1 if [ "$stop_old" == "true" ]; then # Don't remove monitor; stop the pid file the caller used. - $BIN/stop-hugegraph.sh ${PID_FILE_OVERRIDE:+-i "$PID_FILE_OVERRIDE"} false + "$BIN"/stop-hugegraph.sh ${PID_FILE_OVERRIDE:+-i "$PID_FILE_OVERRIDE"} false fi record_monitor_log "Ready to restart $PROC_NAME" # Don't add monitor again, but restart with path overrides - $BIN/start-hugegraph.sh -m false \ + "$BIN"/start-hugegraph.sh -m false \ ${CONF_OVERRIDE:+-c "$CONF_OVERRIDE"} \ ${LOGS_OVERRIDE:+-l "$LOGS_OVERRIDE"} \ ${PLUGINS_OVERRIDE:+-o "$PLUGINS_OVERRIDE"} \ diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh index 987a920c1b..b8b85cd99c 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh @@ -46,14 +46,18 @@ SCRIPTS="$TOP/scripts" # Note: keep ':' in the end of the string to indicate the option needs a value while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do case ${arg} in - c) CONF_OVERRIDE="$OPTARG" ;; + c) reject_unsafe_path "$OPTARG" "-c conf dir" crlf || exit 1 + CONF_OVERRIDE="$OPTARG" ;; d) DAEMON="$OPTARG" ;; g) GC_OPTION="$OPTARG" ;; - i) PID_FILE_OVERRIDE="$OPTARG" ;; + i) reject_unsafe_path "$OPTARG" "-i pid file" crlf || exit 1 + PID_FILE_OVERRIDE="$OPTARG" ;; j) USER_OPTION="$OPTARG" ;; - l) LOGS_OVERRIDE="$OPTARG" ;; + l) reject_unsafe_path "$OPTARG" "-l logs dir" crlf || exit 1 + LOGS_OVERRIDE="$OPTARG" ;; m) OPEN_MONITOR="$OPTARG" ;; - o) PLUGINS_OVERRIDE="$OPTARG" ;; + o) reject_unsafe_path "$OPTARG" "-o plugins dir" crlf || exit 1 + PLUGINS_OVERRIDE="$OPTARG" ;; p) PRELOAD="$OPTARG" ;; s) OPEN_SECURITY_CHECK="$OPTARG" ;; t) SERVER_STARTUP_TIMEOUT_S="$OPTARG" ;; @@ -64,17 +68,29 @@ while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do esac done -# Canonicalize relative path overrides to absolute paths -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" -LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" -PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" -PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" +# Canonicalize relative path overrides to absolute paths. +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" || exit 1 +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" || exit 1 + +# The monitor persists these values into a crontab line, where '%' is special to cron +if [ "$OPEN_MONITOR" == "true" ]; then + reject_unsafe_path "$CONF_OVERRIDE" "conf dir (-c)" cron || exit 1 + reject_unsafe_path "$LOGS_OVERRIDE" "logs dir (-l)" cron || exit 1 + reject_unsafe_path "$PLUGINS_OVERRIDE" "plugins dir (-o)" cron || exit 1 + reject_unsafe_path "$PID_FILE_OVERRIDE" "pid file (-i)" cron || exit 1 + reject_unsafe_path "$JAVA_HOME" "JAVA_HOME" cron || exit 1 + reject_unsafe_path "$TOP" "install directory" cron || exit 1 +fi CONF="${CONF_OVERRIDE:-$TOP/conf}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" +ensure_path_writable "$(dirname "$PID_FILE")" + export CONF_OVERRIDE LOGS_OVERRIDE PID_FILE_OVERRIDE PLUGINS_OVERRIDE if [[ "$OPEN_MONITOR" != "true" && "$OPEN_MONITOR" != "false" ]]; then diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh index eca9d6c8e7..a9b10ba497 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh @@ -25,10 +25,10 @@ function abs_path() { echo "$( cd -P "$( dirname "$SOURCE" )" && pwd )" } -BIN=`abs_path` -TOP="$(cd $BIN/../ && pwd)" +BIN="$(abs_path)" +TOP="$(cd "$BIN"/../ && pwd)" -. $BIN/util.sh +. "$BIN"/util.sh if [ "$JAVA_HOME" == "" ]; then echo "Must set JAVA_HOME environment variable and install JDK >= 17" @@ -38,20 +38,29 @@ fi # Monitor HugeGraphServer every minute, if the server crashes then restart it. # Modify the frequency according to actual needs carefully. -# Persist any path overrides (-c/-l/-i/-o) that the caller (start-hugegraph.sh) exported -CRONTAB_JOB="*/1 * * * * export JAVA_HOME=$JAVA_HOME &&" +# Persist any path overrides (-c/-l/-i/-o) that the caller (start-hugegraph.sh) exported. +# Every value goes through cron_quote: it validates (CR/LF, '%') and emits ONE complete +# POSIX shell word. +JAVA_HOME_Q="$(cron_quote "$JAVA_HOME" "JAVA_HOME")" || exit 1 +MONITOR_Q="$(cron_quote "$TOP/bin/monitor-hugegraph.sh" "monitor script path")" || exit 1 + +CRONTAB_JOB="*/1 * * * * export JAVA_HOME=$JAVA_HOME_Q &&" if [ -n "$CONF_OVERRIDE" ]; then - CRONTAB_JOB="$CRONTAB_JOB export CONF_OVERRIDE='$CONF_OVERRIDE' &&" + CONF_Q="$(cron_quote "$CONF_OVERRIDE" "CONF_OVERRIDE")" || exit 1 + CRONTAB_JOB="$CRONTAB_JOB export CONF_OVERRIDE=$CONF_Q &&" fi if [ -n "$LOGS_OVERRIDE" ]; then - CRONTAB_JOB="$CRONTAB_JOB export LOGS_OVERRIDE='$LOGS_OVERRIDE' &&" + LOGS_Q="$(cron_quote "$LOGS_OVERRIDE" "LOGS_OVERRIDE")" || exit 1 + CRONTAB_JOB="$CRONTAB_JOB export LOGS_OVERRIDE=$LOGS_Q &&" fi if [ -n "$PID_FILE_OVERRIDE" ]; then - CRONTAB_JOB="$CRONTAB_JOB export PID_FILE_OVERRIDE='$PID_FILE_OVERRIDE' &&" + PID_Q="$(cron_quote "$PID_FILE_OVERRIDE" "PID_FILE_OVERRIDE")" || exit 1 + CRONTAB_JOB="$CRONTAB_JOB export PID_FILE_OVERRIDE=$PID_Q &&" fi if [ -n "$PLUGINS_OVERRIDE" ]; then - CRONTAB_JOB="$CRONTAB_JOB export PLUGINS_OVERRIDE='$PLUGINS_OVERRIDE' &&" + PLUGINS_Q="$(cron_quote "$PLUGINS_OVERRIDE" "PLUGINS_OVERRIDE")" || exit 1 + CRONTAB_JOB="$CRONTAB_JOB export PLUGINS_OVERRIDE=$PLUGINS_Q &&" fi -CRONTAB_JOB="$CRONTAB_JOB $TOP/bin/monitor-hugegraph.sh" +CRONTAB_JOB="$CRONTAB_JOB $MONITOR_Q" crontab_append "$CRONTAB_JOB" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh index c29ea79e51..89eaf80da8 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh @@ -62,7 +62,7 @@ if [ ! -f "${PID_FILE}" ]; then exit 1 fi -PID=$(cat $PID_FILE) +PID=$(cat "$PID_FILE") if ! kill_process_and_wait "HugeGraphServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S"; then exit 1 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-monitor.sh index 0888eccb2d..57323dac40 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-monitor.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-monitor.sh @@ -25,11 +25,15 @@ function abs_path() { echo "$( cd -P "$( dirname "$SOURCE" )" && pwd )" } -BIN=`abs_path` -TOP="$(cd $BIN/../ && pwd)" +BIN="$(abs_path)" +TOP="$(cd "$BIN"/../ && pwd)" -. $BIN/util.sh +. "$BIN"/util.sh -CRONTAB_JOB="$TOP/bin/monitor-hugegraph.sh" +# Must equal the serialized form start-monitor.sh saved (one quoted shell word), +# otherwise grep -F cannot match a path containing an apostrophe or space and the +# job could never be removed. shell_quote (not cron_quote) so removal is not +# blocked by validation. +CRONTAB_JOB="$(shell_quote "$TOP/bin/monitor-hugegraph.sh")" crontab_remove "$CRONTAB_JOB" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh new file mode 100644 index 0000000000..7677e453f3 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh @@ -0,0 +1,143 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Local unit tests for canonicalize_dir / canonicalize_file in util.sh. +# No cluster, Java, or CM needed. Run: bash test-canonicalize.sh + +set -u + +UTIL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/util.sh" + +# We only want the helpers; source util.sh in a guarded way. util.sh has no +# top-level side effects for these functions, so a plain source is fine. +# shellcheck disable=SC1090 +. "$UTIL" + +PASS=0 +FAIL=0 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +ok() { echo "PASS: $1"; PASS=$((PASS+1)); } +bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } + +# Run a function in a subshell so its `exit 1` paths don't kill the harness. +run_capture() { # usage: run_capture FUNC ARG ; sets OUT, RC + OUT="$("$@" 2>/tmp/_cz_err)"; RC=$? + ERR="$(cat /tmp/_cz_err)" +} + +echo "== canonicalize_dir ==" + +# 1. empty input -> empty output, rc 0 +run_capture canonicalize_dir "" +[ $RC -eq 0 ] && [ -z "$OUT" ] && ok "dir: empty input returns empty, rc=0" \ + || bad "dir: empty input (rc=$RC out='$OUT')" + +# 2. absolute existing dir -> same absolute path +run_capture canonicalize_dir "$TMP" +[ "$OUT" = "$TMP" ] && ok "dir: absolute existing path echoes itself" \ + || bad "dir: absolute path (out='$OUT' want='$TMP')" + +# 3. relative dir -> absolute, and dir is created +( cd "$TMP" && run_capture canonicalize_dir "sub/rel" \ + && [ "$OUT" = "$TMP/sub/rel" ] && [ -d "$TMP/sub/rel" ] ) \ + && ok "dir: relative path canonicalized + created" \ + || bad "dir: relative path (out='$OUT')" + +# 4. nested non-existent absolute -> created + echoed +run_capture canonicalize_dir "$TMP/a/b/c" +[ "$OUT" = "$TMP/a/b/c" ] && [ -d "$TMP/a/b/c" ] \ + && ok "dir: nested absolute created" || bad "dir: nested (out='$OUT')" + +# 5. unresolvable (parent not writable) -> error + exit 1 +RO="$TMP/ro"; mkdir -p "$RO"; chmod 500 "$RO" +run_capture canonicalize_dir "$RO/child" +if [ "$(id -u)" -eq 0 ]; then + echo "SKIP: dir unwritable test (running as root bypasses perms)" +else + [ $RC -eq 1 ] && echo "$ERR" | grep -q "cannot resolve path" \ + && ok "dir: unwritable parent -> exit 1 + error msg" \ + || bad "dir: unwritable parent (rc=$RC err='$ERR')" +fi +chmod 700 "$RO" + +echo "== canonicalize_file ==" + +# 6. empty input -> empty, rc 0 +run_capture canonicalize_file "" +[ $RC -eq 0 ] && [ -z "$OUT" ] && ok "file: empty input returns empty, rc=0" \ + || bad "file: empty input (rc=$RC out='$OUT')" + +# 7. absolute file path -> abs parent + filename, parent created, file NOT created +run_capture canonicalize_file "$TMP/pids/server.pid" +[ "$OUT" = "$TMP/pids/server.pid" ] && [ -d "$TMP/pids" ] && [ ! -e "$TMP/pids/server.pid" ] \ + && ok "file: abs path resolves, parent created, file not created" \ + || bad "file: abs path (out='$OUT')" + +# 8. relative file path -> absolute +( cd "$TMP" && run_capture canonicalize_file "relpids/x.pid" \ + && [ "$OUT" = "$TMP/relpids/x.pid" ] && [ -d "$TMP/relpids" ] ) \ + && ok "file: relative path canonicalized" || bad "file: relative (out='$OUT')" + +# 9. unresolvable parent -> error + exit 1 +# Use a NEVER-created subpath under the read-only dir so mkdir -p must fail. +chmod 500 "$RO" +run_capture canonicalize_file "$RO/fresh_sub/x.pid" +if [ "$(id -u)" -eq 0 ]; then + echo "SKIP: file unwritable test (running as root)" +else + [ $RC -eq 1 ] && echo "$ERR" | grep -q "cannot resolve path" \ + && ok "file: unwritable parent -> exit 1 + error msg" \ + || bad "file: unwritable parent (rc=$RC err='$ERR')" +fi +chmod 700 "$RO" + +echo "== canonicalize_file nocreate (stop scripts) ==" + +# 10. existing parent -> absolute path, nothing created +mkdir -p "$TMP/existing" +run_capture canonicalize_file "$TMP/existing/s.pid" nocreate +[ $RC -eq 0 ] && [ "$OUT" = "$TMP/existing/s.pid" ] && [ ! -e "$TMP/existing/s.pid" ] \ + && ok "nocreate: existing parent resolves, file not created" \ + || bad "nocreate: existing parent (rc=$RC out='$OUT')" + +# 11. relative path with existing parent -> absolute +( cd "$TMP/existing" && run_capture canonicalize_file "r.pid" nocreate \ + && [ "$OUT" = "$TMP/existing/r.pid" ] ) \ + && ok "nocreate: relative path resolves" || bad "nocreate: relative (out='$OUT')" + +# 12. missing parent -> fails with the same error, and the parent is NOT created +run_capture canonicalize_file "$TMP/missing/deeper/s.pid" nocreate +[ $RC -eq 1 ] && echo "$ERR" | grep -q "cannot resolve path" && [ ! -e "$TMP/missing" ] \ + && ok "nocreate: missing parent -> exit 1 + error, nothing created" \ + || bad "nocreate: missing parent (rc=$RC err='$ERR' created=$([ -e "$TMP/missing" ] && echo yes || echo no))" + +# 13. default mode still creates the missing parent (start scripts unchanged) +run_capture canonicalize_file "$TMP/startcreates/s.pid" +[ $RC -eq 0 ] && [ -d "$TMP/startcreates" ] \ + && ok "default: missing parent is still created" \ + || bad "default: parent not created (rc=$RC)" + +# 14. empty input stays a no-op in nocreate mode +run_capture canonicalize_file "" nocreate +[ $RC -eq 0 ] && [ -z "$OUT" ] && ok "nocreate: empty input returns empty, rc=0" \ + || bad "nocreate: empty input (rc=$RC out='$OUT')" + +echo "-----------------------------" +echo "PASS=$PASS FAIL=$FAIL" +[ $FAIL -eq 0 ] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh new file mode 100755 index 0000000000..2689f0cf12 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh @@ -0,0 +1,292 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Local regression tests for the cron serialization helpers in util.sh: +# shell_quote, reject_unsafe_path, cron_quote, +# crontab_append, crontab_remove. +# No cluster, Java, or real crontab needed: `crontab` is replaced by a stub that +# reads/writes a temp file, so the invoking user's crontab is not touched. +# Run: bash test-cron-quote.sh + +set -u + +UTIL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/util.sh" +# shellcheck disable=SC1090 +. "$UTIL" + +PASS=0 +FAIL=0 +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +ok() { echo "PASS: $1"; PASS=$((PASS+1)); } +bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } + +# ---- stub crontab --------------------------------------------------------- +STUB_DIR="$TMP/stubbin" +CRON_FILE="$TMP/crontab.txt" +mkdir -p "$STUB_DIR" +cat > "$STUB_DIR/crontab" < "$CRON_FILE.new" && mv "$CRON_FILE.new" "$CRON_FILE"; exit 0 ;; +esac +exit 1 +EOF +chmod +x "$STUB_DIR/crontab" +export PATH="$STUB_DIR:$PATH" +: > "$CRON_FILE" + +# Which POSIX shells to execute the serialized values through. +SHELLS="/bin/sh" +command -v dash >/dev/null 2>&1 && SHELLS="$SHELLS $(command -v dash)" +[ -x /bin/bash ] && SHELLS="$SHELLS /bin/bash" + +# --------------------------------------------------------------------------- +echo "== A. shell_quote round-trips through each shell ==" + +# Values that must survive byte-for-byte. Built with $'..' so the odd ones are exact. +VALUES=( + "/opt/hugegraph" + "/var/log/Huge Graph" + "/data/team's-logs" + "/data/o''neill" + "'leading-and-trailing'" + "'" + "''" + '/data/a\b' + '/data/a\'"'"'b' + '/data/a\\b' + '/tmp/$(touch PWNED)' + '/tmp/`touch PWNED`' + '/tmp/$HOME' + '/a;touch PWNED;b' + '/a&b|c>d&1)" + if [ "$got" = "$v" ]; then + ok "$(basename "$sh_bin") #$n round-trips: [$v]" + else + bad "$(basename "$sh_bin") #$n: want [$v] got [$got]" + fi + done +done +[ -e "$TMP/PWNED" ] && bad "a metacharacter value executed a command" \ + || ok "no metacharacter value was executed" + +# An apostrophe is closed, escaped and reopened. +expected="'/data/team'\\''s-logs'" +got="$(shell_quote "/data/team's-logs")" +[ "$got" = "$expected" ] && ok "apostrophe is escaped -> $expected" || bad "apostrophe escaping: want $expected got $got" + +# Callers must not need extra quotes: output is already one complete word. +case "$(shell_quote "a b")" in "'a b'") ok "output carries its own quotes" ;; *) bad "own quotes" ;; esac + +# A trailing newline must be preserved by the quoting itself (no $( ) strip). +q="$(shell_quote $'x\n')"; [ "$q" = $'\'x\n\'' ] && ok "shell_quote keeps a trailing newline" \ + || bad "shell_quote lost trailing newline" +# (command substitution above strips only OUR capture; compare via printf -v to be exact) +printf -v q '%s' "$(shell_quote $'x\n'; printf .)"; q="${q%.}" +[ "$q" = $'\'x\n\'' ] && ok "shell_quote emits the newline inside the quotes" || bad "newline inside quotes" + +# Should not emit bash-only ANSI-C quoting (dash cannot parse it). +for v in '/data/a\b' $'/tmp/nl\nx' "/data/team's"; do + label="${v//$'\n'/}" + case "$(shell_quote "$v")" in + *\$\'*) bad "emitted \$'...' for [$label]" ;; + *) ok "no \$'...' emitted for [$label]" ;; + esac +done + +# --------------------------------------------------------------------------- +echo "== B. reject_unsafe_path ==" + +expect_rc() { # desc want_rc value label mode + local out rc + out="$(reject_unsafe_path "$3" "$4" "$5" 2>&1 >/dev/null)"; rc=$? + if [ "$rc" -eq "$2" ]; then ok "$1"; else bad "$1 (rc=$rc want=$2)"; fi + LAST_ERR="$out" +} + +expect_rc "crlf: plain path accepted" 0 "/opt/x" L crlf +expect_rc "crlf: LF rejected" 1 $'/opt/x\ny' L crlf +expect_rc "crlf: CR rejected" 1 $'/opt/x\ry' L crlf +expect_rc "crlf: trailing LF rejected" 1 $'/opt/x\n' L crlf +expect_rc "crlf: '%' still accepted (non-cron)" 0 "/opt/100%/x" L crlf +expect_rc "cron: plain path accepted" 0 "/opt/x" L cron +expect_rc "cron: '%' rejected" 1 "/opt/100%/x" L cron +expect_rc "cron: LF rejected" 1 $'/opt/x\ny' L cron +expect_rc "cron: apostrophe/space accepted" 0 "/data/team's logs" L cron +expect_rc "cron: empty accepted" 0 "" L cron + +expect_rc "message names the label" 1 "a%b" "CONF_OVERRIDE" cron +case "$LAST_ERR" in *CONF_OVERRIDE*"%"*) ok "error mentions label and %" ;; *) bad "error text: $LAST_ERR" ;; esac +expect_rc "message for CR/LF" 1 $'a\nb' "LOGS_OVERRIDE" crlf +case "$LAST_ERR" in *LOGS_OVERRIDE*CR/LF*) ok "error mentions label and CR/LF" ;; *) bad "error text: $LAST_ERR" ;; esac + +# Errors go to stderr, not stdout. +out="$(reject_unsafe_path $'a\nb' L crlf 2>/dev/null)" +[ -z "$out" ] && ok "diagnostics go to stderr only" || bad "diagnostic leaked to stdout" + +# CR/LF must be checked on the raw value, before canonicalization: $( ) strips a +# trailing newline, so a check made afterwards would not see it. +raw=$'/tmp/x\n' +stripped="$(printf '%s' "$raw")" +reject_unsafe_path "$stripped" L crlf 2>/dev/null \ + && ok "a trailing LF is already gone after command substitution" \ + || bad "stripped value unexpectedly rejected" +reject_unsafe_path "$raw" L crlf 2>/dev/null \ + && bad "raw trailing-LF value must be rejected" \ + || ok "raw value with trailing LF is rejected" + +# --------------------------------------------------------------------------- +echo "== C. cron_quote ==" +q="$(cron_quote "/data/team's-logs" L)"; rc=$? +[ $rc -eq 0 ] && [ "$q" = "'/data/team'\\''s-logs'" ] && ok "cron_quote quotes valid input" || bad "cron_quote valid ($q)" +out="$(cron_quote "/a%b" L 2>/dev/null)"; rc=$? +[ $rc -ne 0 ] && [ -z "$out" ] && ok "cron_quote rejects '%' and prints nothing" || bad "cron_quote % (rc=$rc out=$out)" +out="$(cron_quote $'/a\nb' L 2>/dev/null)"; rc=$? +[ $rc -ne 0 ] && [ -z "$out" ] && ok "cron_quote rejects LF and prints nothing" || bad "cron_quote LF" + +# --------------------------------------------------------------------------- +echo "== D. serialized job executes through sh with the right environment ==" + +# Build a job the same way start-monitor.sh does, but point the "monitor" at a +# probe script that records what it received, then run the saved line via sh -c. +for sh_bin in $SHELLS; do + BASE="$TMP/it's a dir/with space" + mkdir -p "$BASE/bin" + PROBE="$BASE/bin/monitor-hugegraph.sh" + cat > "$PROBE" <<'EOF' +#!/bin/sh +printf 'CONF=[%s]\nLOGS=[%s]\nPID=[%s]\nPLUG=[%s]\nJH=[%s]\n' \ + "$CONF_OVERRIDE" "$LOGS_OVERRIDE" "$PID_FILE_OVERRIDE" "$PLUGINS_OVERRIDE" "$JAVA_HOME" +EOF + chmod +x "$PROBE" + + c="/data/team's conf"; l='/var/log/a\b'; p='/run/$HOME/pid'; o='/opt/`id`/plugins'; jh="/usr/lib/jvm/jdk 17's" + job="*/1 * * * * export JAVA_HOME=$(cron_quote "$jh" JH) &&" + job="$job export CONF_OVERRIDE=$(cron_quote "$c" C) &&" + job="$job export LOGS_OVERRIDE=$(cron_quote "$l" L) &&" + job="$job export PID_FILE_OVERRIDE=$(cron_quote "$p" P) &&" + job="$job export PLUGINS_OVERRIDE=$(cron_quote "$o" O)" + job="$job && $(cron_quote "$PROBE" M)" + + cmd="${job#\*/1 \* \* \* \* }" # drop the 5 time fields: what cron hands to sh -c + res="$("$sh_bin" -c "$cmd" 2>&1)" + want="CONF=[$c] +LOGS=[$l] +PID=[$p] +PLUG=[$o] +JH=[$jh]" + if [ "$res" = "$want" ]; then ok "$(basename "$sh_bin"): all six values survive execution" + else bad "$(basename "$sh_bin"): got: +$res"; fi +done + +# --------------------------------------------------------------------------- +echo "== E. crontab_append / crontab_remove (stubbed crontab) ==" + +printf '%s\n' "# existing entry" "0 3 * * * /usr/bin/backup" > "$CRON_FILE" +BEFORE="$(cat "$CRON_FILE")" + +TOP="$TMP/team's top"; mkdir -p "$TOP/bin" +KEY="$(cron_quote "$TOP/bin/monitor-hugegraph.sh" M)" # what start-monitor.sh saves +JOB="*/1 * * * * export CONF_OVERRIDE=$(cron_quote "/data/team's conf" C) && $KEY" + +crontab_append "$JOB"; rc=$? +[ $rc -eq 0 ] && grep -qF -- "$JOB" "$CRON_FILE" && ok "append wrote the entry" || bad "append (rc=$rc)" + +crontab_append "$JOB"; rc=$? +[ $rc -ne 0 ] && ok "duplicate append is refused (rc=$rc)" || bad "duplicate append accepted" +[ "$(grep -cF -- "$JOB" "$CRON_FILE")" -eq 1 ] && ok "exactly one copy after duplicate add" || bad "duplicate entry written" + +# The unquoted path does not match the serialized line when it contains an apostrophe. +RAW_KEY="$TOP/bin/monitor-hugegraph.sh" +grep -qF -- "$RAW_KEY" "$CRON_FILE" \ + && bad "unquoted key unexpectedly matches the serialized line" \ + || ok "unquoted key does not match an apostrophe install path" + +# stop-monitor.sh derives the key with shell_quote; it must match what was saved. +STOP_KEY="$(shell_quote "$TOP/bin/monitor-hugegraph.sh")" +[ "$STOP_KEY" = "$KEY" ] && ok "stop key equals the saved key" || bad "stop key differs: $STOP_KEY vs $KEY" +crontab_remove "$STOP_KEY"; rc=$? +[ $rc -eq 0 ] && ok "remove succeeded for apostrophe install path" || bad "remove rc=$rc" +grep -qF -- "$JOB" "$CRON_FILE" && bad "job still present after remove" || ok "job removed" +[ "$(cat "$CRON_FILE")" = "$BEFORE" ] && ok "unrelated crontab lines untouched" || bad "other entries damaged" + +crontab_remove "$STOP_KEY"; rc=$? +[ $rc -eq 0 ] && ok "removing an absent job is a no-op success" || bad "absent remove rc=$rc" + +# printf, not echo: backslashes and a leading -n must be written verbatim. +: > "$CRON_FILE" +crontab_append '-n \t \\ literal'; rc=$? +[ "$(cat "$CRON_FILE")" = '-n \t \\ literal' ] && ok "append is literal (backslashes, leading -n)" \ + || bad "append mangled: $(cat "$CRON_FILE")" + +# grep -F -- : a job beginning with '-' is a pattern, not an option. +crontab_append '-n \t \\ literal'; rc=$? +[ $rc -ne 0 ] && ok "duplicate detection works for a job starting with '-'" || bad "leading '-' broke grep" + +# --------------------------------------------------------------------------- +echo "== F. rejected input never changes the existing crontab ==" + +printf '%s\n' "# keep me" "5 5 * * * /bin/true" > "$CRON_FILE" +SNAP="$(cat "$CRON_FILE")" + +# Reproduce start-monitor.sh's flow. +build_and_append() { + local q + q="$(cron_quote "$1" CONF_OVERRIDE)" || return 1 + crontab_append "*/1 * * * * export CONF_OVERRIDE=$q" +} +build_and_append "/opt/100%/conf"; rc=$? +[ $rc -ne 0 ] && [ "$(cat "$CRON_FILE")" = "$SNAP" ] && ok "'%' rejected, crontab unchanged" || bad "'%' flow (rc=$rc)" +build_and_append $'/opt/x\n* * * * * evil'; rc=$? +[ $rc -ne 0 ] && [ "$(cat "$CRON_FILE")" = "$SNAP" ] && ok "newline-injection rejected, crontab unchanged" || bad "LF flow (rc=$rc)" +grep -q evil "$CRON_FILE" && bad "injected job reached crontab" || ok "no injected job in crontab" + +# start-monitor.sh itself: wrong order of validation must exit non-zero w/o touching cron. +SM="$(cd "$(dirname "$UTIL")" && pwd)/start-monitor.sh" +if [ -f "$SM" ]; then + out="$(JAVA_HOME=/usr CONF_OVERRIDE='/opt/100%/conf' bash "$SM" 2>&1)"; rc=$? + [ $rc -ne 0 ] && [ "$(cat "$CRON_FILE")" = "$SNAP" ] \ + && ok "start-monitor.sh exits non-zero and leaves crontab unchanged on '%'" \ + || bad "start-monitor.sh with '%' (rc=$rc): $out" +fi + +# --------------------------------------------------------------------------- +echo +echo "Shells exercised: $SHELLS" +command -v dash >/dev/null 2>&1 || echo "NOTE: dash not installed here; run on Debian/Ubuntu/CI to cover it." +echo "Passed: $PASS Failed: $FAIL" +[ "$FAIL" -eq 0 ] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index e380ba266d..494db63307 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -312,27 +312,64 @@ function check_port() { return 0 } +# Emit $1 as ONE complete POSIX shell word, INCLUDING the surrounding quotes. +# Callers should NOT add another pair of quotes: +# /data/team's-logs -> '/data/team'\''s-logs' +# +function shell_quote() { + local sq="'" repl="'\\''" + printf "'%s'" "${1//$sq/$repl}" +} + +# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron +# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE +# canonicalize_dir/canonicalize_file. +# cron : crlf + reject '%'. +function reject_unsafe_path() { + case "$1" in + *[$'\n\r']*) + echo "ERROR: $2 must not contain CR/LF characters" >&2 + return 1 ;; + esac + + [ "$3" = "cron" ] || return 0 + + case "$1" in + *%*) + echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 + return 1 ;; + esac + return 0 +} + +# Validate (cron rules) then quote. $1=value $2=label. Prints the quoted word. +function cron_quote() { + reject_unsafe_path "$1" "$2" cron || return 1 + shell_quote "$1" +} + function crontab_append() { local job="$1" - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 fi - (crontab -l ; echo "$job") | crontab - + # printf, not echo: echo may interpret backslashes or swallow a leading -n/-e + { crontab -l 2>/dev/null; printf '%s\n' "$job"; } | crontab - } function crontab_remove() { local job="$1" # check exist before remove - crontab -l | grep -F "$job" >/dev/null 2>&1 - if [ $? -eq 1 ]; then + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 + if [ $? -ne 0 ]; then return 0 fi - crontab -l | grep -Fv "$job" | crontab - + crontab -l 2>/dev/null | grep -Fv -- "$job" | crontab - # Check exist after remove - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 else @@ -682,6 +719,6 @@ function kill_process_and_wait() { } function exit_with_usage_help(){ - echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-l logs_dir] [-m true|false] [-p true|false] [-s true|false] [-j java_options] [-t timeout] [-y true|false]" + echo "USAGE: $0 [-c conf_dir] [-d true|false] [-g g1] [-i pid_file] [-l logs_dir] [-m true|false] [-o plugins_dir] [-p true|false] [-s true|false] [-j java_options] [-t timeout] [-y true|false]" exit 1 } diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh index 8ccdd184b6..71a19712b0 100755 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/start-hugegraph-store.sh @@ -115,11 +115,11 @@ while getopts "d:c:g:i:j:l:o:y:" arg; do esac done -# Canonicalize relative path overrides to absolute paths -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" -LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" -PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" -PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" +# Canonicalize relative path overrides to absolute paths. +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" || exit 1 +PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" || exit 1 CONF="${CONF_OVERRIDE:-$TOP/conf}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" @@ -129,6 +129,7 @@ PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" ensure_path_writable "$LOGS" ensure_path_writable "$PLUGINS" +ensure_path_writable "$(dirname "$PID_FILE")" # The maximum and minimum heap memory that service can use (for production env set it 36GB) MAX_MEM=$((2 * 1024)) @@ -151,7 +152,7 @@ JAVA_VERSION=$($JAVA -version 2>&1 | sed 's/^1\.//' | cut -d'.' -f1) JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then - echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> ${OUTPUT} + echo "Please make sure that the JDK is installed and the version >= $EXPECT_JDK_VERSION" >> "${OUTPUT}" exit 1 fi @@ -160,14 +161,14 @@ if [ "$JAVA_OPTIONS" = "" ]; then XMX=$(calc_xmx $MIN_MEM $MAX_MEM) if [ $? -ne 0 ]; then echo "Failed to start HugeGraphStoreServer, requires at least ${MIN_MEM}m free memory" \ - >> ${OUTPUT} + >> "${OUTPUT}" exit 1 fi JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:MetaspaceSize=256M -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION} " # JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" # Rolling out detailed GC logs - JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=${LOGS}/logs/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" + JAVA_OPTIONS="${JAVA_OPTIONS} -Xlog:gc=info:file=${LOGS}/gc.log:time,uptime,level,tags:filecount=3,filesize=100m" fi # Using G1GC as the default garbage collector (Recommended for large memory machines) @@ -185,7 +186,7 @@ case "$GC_OPTION" in -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" ;; *) - echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> ${OUTPUT} + echo "Unrecognized gc option: '$GC_OPTION', default use g1, options only support 'ZGC' now" >> "${OUTPUT}" exit 1 esac @@ -201,7 +202,7 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then "${GITHUB}/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.1.0/${OT_JAR}" if [[ ! -e "${OT_JAR_PATH}" ]]; then - echo "## Error: Failed to download ${OT_JAR}." >>${OUTPUT} + echo "## Error: Failed to download ${OT_JAR}." >>"${OUTPUT}" exit 1 fi fi @@ -211,8 +212,8 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') if [[ "${expected_md5}" != "${actual_md5}" ]]; then - echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>${OUTPUT} - echo "## Tips: Remove the file and try again." >>${OUTPUT} + echo "## Error: MD5 checksum verification failed for ${OT_JAR_PATH}." >>"${OUTPUT}" + echo "## Tips: Remove the file and try again." >>"${OUTPUT}" exit 1 fi @@ -245,12 +246,12 @@ if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphStoreServer in daemon mode..." if [[ "${STDOUT_MODE:-false}" == "true" ]]; then exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml \ + -Dspring.config.location="${CONF}"/application.yml \ ${LIB}/hg-store-node-*.jar & else exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar >> ${OUTPUT} 2>&1 & + -Dspring.config.location="${CONF}"/application.yml \ + ${LIB}/hg-store-node-*.jar >> "${OUTPUT}" 2>&1 & fi PID="$!" # Write pid to file @@ -263,11 +264,11 @@ else echo "[+pid] $$" if [[ "${STDOUT_MODE:-false}" == "true" ]]; then exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml \ + -Dspring.config.location="${CONF}"/application.yml \ ${LIB}/hg-store-node-*.jar else exec ${JAVA} -Dname="HugeGraphStore" ${JVM_OPTIONS} ${JAVA_OPTIONS} -jar \ - -Dspring.config.location=${CONF}/application.yml \ - ${LIB}/hg-store-node-*.jar >> ${OUTPUT} 2>&1 + -Dspring.config.location="${CONF}"/application.yml \ + ${LIB}/hg-store-node-*.jar >> "${OUTPUT}" 2>&1 fi fi diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh index 0acf989f07..3e84999549 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/stop-hugegraph-store.sh @@ -42,12 +42,12 @@ TOP="$(cd $BIN/../ && pwd)" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=30 -if [ ! -f ${PID_FILE} ]; then +if [ ! -f "${PID_FILE}" ]; then echo "The pid file $PID_FILE doesn't exist" exit 0 fi -PID=`cat $PID_FILE` +PID=$(cat "$PID_FILE") kill_process_and_wait "HugeGraphStoreServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S" if [ $? -eq 0 ]; then diff --git a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh index 58f761c706..cb7e2d25fd 100644 --- a/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh @@ -81,27 +81,64 @@ function process_id() { return "$pid" } +# Emit $1 as ONE complete POSIX shell word, INCLUDING the surrounding quotes. +# Callers should NOT add another pair of quotes: +# /data/team's-logs -> '/data/team'\''s-logs' +# +function shell_quote() { + local sq="'" repl="'\\''" + printf "'%s'" "${1//$sq/$repl}" +} + +# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron +# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE +# canonicalize_dir/canonicalize_file. +# cron : crlf + reject '%'. +function reject_unsafe_path() { + case "$1" in + *[$'\n\r']*) + echo "ERROR: $2 must not contain CR/LF characters" >&2 + return 1 ;; + esac + + [ "$3" = "cron" ] || return 0 + + case "$1" in + *%*) + echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 + return 1 ;; + esac + return 0 +} + +# Validate (cron rules) then quote. $1=value $2=label. Prints the quoted word. +function cron_quote() { + reject_unsafe_path "$1" "$2" cron || return 1 + shell_quote "$1" +} + function crontab_append() { local job="$1" - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 fi - (crontab -l ; echo "$job") | crontab - + # printf, not echo: echo may interpret backslashes or swallow a leading -n/-e + { crontab -l 2>/dev/null; printf '%s\n' "$job"; } | crontab - } function crontab_remove() { local job="$1" # check exist before remove - crontab -l | grep -F "$job" >/dev/null 2>&1 - if [ $? -eq 1 ]; then + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 + if [ $? -ne 0 ]; then return 0 fi - crontab -l | grep -Fv "$job" | crontab - + crontab -l 2>/dev/null | grep -Fv -- "$job" | crontab - # Check exist after remove - crontab -l | grep -F "$job" >/dev/null 2>&1 + crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 if [ $? -eq 0 ]; then return 1 else From 860ec0faef003fa2737a27c5fbbc0d6666bc0145 Mon Sep 17 00:00:00 2001 From: Avanish Shukla Date: Wed, 21 Oct 2026 01:42:13 +0530 Subject: [PATCH 4/4] fix(scripts): quote-safe JVM launchers, relative-path guards, nocreate canonicalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Bash-array JVM launchers for quoting fix (PD/Store start+stop+restart) + test-launcher-argv.sh + test-override-guards.sh 2. reject_relative_conf_path util function (graphs/verify-path/license-path/data-path) — server, PD, store 3. ensure_file_readable util function (server, PD, store) 4. canonicalize_dir nocreate mode for -c and -i 5. classpath mode (reject ':') for -o validation 6. Moved test scripts out of assembly into src/test/shell and added them in server-tests.yml 7. stop-hugegraph-pd.sh timeout now exits 1 instead of falling through --- .github/workflows/server-tests.yml | 11 + .../assembly/static/bin/start-hugegraph-pd.sh | 116 +++-- .../assembly/static/bin/stop-hugegraph-pd.sh | 14 +- .../src/assembly/static/bin/util.sh | 110 ++++- .../assembly/static/bin/hugegraph-server.sh | 93 ++-- .../src/assembly/static/bin/init-store.sh | 19 +- .../assembly/static/bin/start-hugegraph.sh | 13 +- .../src/assembly/static/bin/stop-hugegraph.sh | 6 + .../src/assembly/static/bin/util.sh | 113 ++++- .../bin => test/shell}/test-canonicalize.sh | 97 +++- .../bin => test/shell}/test-cron-quote.sh | 56 ++- .../src/test/shell/test-launcher-argv.sh | 413 ++++++++++++++++++ .../src/test/shell/test-override-guards.sh | 254 +++++++++++ .../static/bin/restart-hugegraph-store.sh | 18 +- .../static/bin/start-hugegraph-store.sh | 126 ++++-- .../static/bin/stop-hugegraph-store.sh | 6 + .../src/assembly/static/bin/util.sh | 105 ++++- 17 files changed, 1414 insertions(+), 156 deletions(-) rename hugegraph-server/hugegraph-dist/src/{assembly/static/bin => test/shell}/test-canonicalize.sh (59%) rename hugegraph-server/hugegraph-dist/src/{assembly/static/bin => test/shell}/test-cron-quote.sh (86%) create mode 100644 hugegraph-server/hugegraph-dist/src/test/shell/test-launcher-argv.sh create mode 100644 hugegraph-server/hugegraph-dist/src/test/shell/test-override-guards.sh diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 1f707604ea..4d83d947e5 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -161,6 +161,17 @@ jobs: fi echo "can_run=true" >> "$GITHUB_OUTPUT" + - name: Run path-override guard and quoting unit tests + if: ${{ env.BACKEND == 'rocksdb' }} + run: | + # Pure bash harnesses: no JVM/cluster/real crontab needed (crontab is + # stubbed). + SHELL_TEST_DIR=hugegraph-server/hugegraph-dist/src/test/shell + bash "$SHELL_TEST_DIR/test-canonicalize.sh" + bash "$SHELL_TEST_DIR/test-cron-quote.sh" + bash "$SHELL_TEST_DIR/test-override-guards.sh" + bash "$SHELL_TEST_DIR/test-launcher-argv.sh" + - name: Run start-hugegraph.sh foreground mode tests if: ${{ env.BACKEND == 'rocksdb' && steps.server-preflight.outputs.can_run == 'true' }} run: | diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh index 76a98e24c7..00efe179c5 100755 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/start-hugegraph-pd.sh @@ -60,8 +60,16 @@ TOP="$(cd "$BIN"/../ && pwd)" . "$BIN"/util.sh +# Validate the RAW overrides before canonicalization: command substitution +# strips trailing newlines, which would hide an embedded CR/LF. +reject_unsafe_path "$CONF_OVERRIDE" "-c conf dir" crlf || exit 1 +reject_unsafe_path "$LOGS_OVERRIDE" "-l logs dir" crlf || exit 1 +reject_unsafe_path "$PID_FILE_OVERRIDE" "-i pid file" crlf || exit 1 +# The plugins dir is joined into ':'-separated classpath/agent settings +reject_unsafe_path "$PLUGINS_OVERRIDE" "-o plugins dir" classpath || exit 1 + # Canonicalize relative path overrides to absolute paths. -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE" nocreate)" || exit 1 LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" || exit 1 PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" || exit 1 @@ -70,7 +78,7 @@ CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" -OUTPUT=${LOGS}/hugegraph-pd-stdout.log +OUTPUT="${LOGS}/hugegraph-pd-stdout.log" GITHUB="https://github.com" PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" JVM_MODULE_OPTIONS="$BIN/jvm-module.options" @@ -80,12 +88,28 @@ if [[ ! -f "$JVM_MODULE_OPTIONS" || ! -r "$JVM_MODULE_OPTIONS" ]]; then exit 1 fi -. "$BIN"/util.sh ensure_path_writable "$LOGS" ensure_path_writable "$PLUGINS" ensure_path_writable "$(dirname "$PID_FILE")" +# These are resolved relative to the working directory (set to $TOP below), not +# relative to $CONF, so a relative value would silently keep reading the +# packaged conf dir even though -c points somewhere else. +if [ -n "$CONF_OVERRIDE" ]; then + # The file is also handed to the JVM as -Dspring.config.location below. + # Spring Boot would reject a missing location, but only after the JVM is + # up -- by which point this script has written the PID file and exited 0, + # so a supervisor sees a successful start and a process that dies moments + # later. Fail here instead, while the exit code still means something. + ensure_file_readable "${CONF}/application.yml" "PD config" || exit 1 + for PROPERTY in verify-path license-path; do + VALUE=$(sed -n "s/^[[:space:]]*${PROPERTY}:[[:space:]]*//p" \ + "${CONF}/application.yml" | head -1 | tr -d "'\"") + reject_relative_conf_path "$VALUE" "$PROPERTY" "${CONF}/application.yml" || exit 1 + done +fi + # The maximum and minimum heap memory that service can use MAX_MEM=$((32 * 1024)) MIN_MEM=$((1 * 512)) @@ -102,7 +126,7 @@ else fi # check jdk version -JAVA_VERSION=$($JAVA -version 2>&1 | +JAVA_VERSION=$("$JAVA" -version 2>&1 | awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' | sed 's/^1\.//' | cut -d'.' -f1) JAVA_VERSION="${JAVA_VERSION%%[!0-9]*}" @@ -111,19 +135,32 @@ if [[ -z $JAVA_VERSION || $JAVA_VERSION -lt $EXPECT_JDK_VERSION ]]; then exit 1 fi -# Set Java options -if [ "$JAVA_OPTIONS" = "" ]; then +# Set Java options. +# Script-generated options are kept in arrays so that overridden paths +# containing spaces (CONF/LOGS/PLUGINS) remain a single argument when passed +# to java. User-supplied strings ($JAVA_OPTIONS / $USER_OPTION) keep the +# historical whitespace-splitting contract. +JAVA_OPTIONS_ENV="${JAVA_OPTIONS}" +JAVA_OPTIONS=() +if [ "$JAVA_OPTIONS_ENV" = "" ]; then XMX=$(calc_xmx $MIN_MEM $MAX_MEM) if [ $? -ne 0 ]; then echo "Failed to start HugeGraphPDServer, requires at least ${MIN_MEM}m free memory" \ >> "${OUTPUT}" exit 1 fi - JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGS} ${USER_OPTION}" + JAVA_OPTIONS=("-Xms${MIN_MEM}m" "-Xmx${XMX}m" "-XX:+HeapDumpOnOutOfMemoryError" + "-XX:HeapDumpPath=${LOGS}") + + # Split user-supplied options on whitespace (legacy behaviour) + USER_OPTIONS=() + read -r -a USER_OPTIONS <<< "${USER_OPTION}" + JAVA_OPTIONS+=(${USER_OPTIONS[@]+"${USER_OPTIONS[@]}"}) # Rolling out detailed GC logs - #JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseGCLogFileRotation -XX:GCLogFileSize=10M -XX:NumberOfGCLogFiles=3 \ - # -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + #JAVA_OPTIONS+=("-Xlog:gc=info:file=${LOGS}/gc.log:time,uptime,level,tags:filecount=3,filesize=100m") +else + read -r -a JAVA_OPTIONS <<< "${JAVA_OPTIONS_ENV}" fi # Keep JVM/caller GC selection by default; explicitly select G1 when requested. @@ -131,19 +168,20 @@ case "$GC_OPTION" in ""|g1|G1) if [[ "$GC_OPTION" == g1 || "$GC_OPTION" == G1 ]]; then echo "Using G1GC" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseG1GC" + JAVA_OPTIONS+=("-XX:+UseG1GC") else echo "Using JVM garbage collector configuration" fi - JAVA_OPTIONS="-XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=50 \ - -XX:G1RSetUpdatingPauseTimePercent=5 ${JAVA_OPTIONS}" + JAVA_OPTIONS=("-XX:+ParallelRefProcEnabled" "-XX:InitiatingHeapOccupancyPercent=50" + "-XX:G1RSetUpdatingPauseTimePercent=5" + ${JAVA_OPTIONS[@]+"${JAVA_OPTIONS[@]}"}) ;; zgc|ZGC) echo "Using ZGC as the default garbage collector (requires Java 17 or later)" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \ - -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \ - -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \ - -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" + JAVA_OPTIONS+=("-XX:+UseZGC" "-XX:+UnlockExperimentalVMOptions" + "-XX:ConcGCThreads=2" "-XX:ParallelGCThreads=6" + "-XX:ZCollectionInterval=120" "-XX:ZAllocationSpikeTolerance=5" + "-XX:+UnlockDiagnosticVMOptions" "-XX:-ZProactive") ;; *) echo "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" >> "${OUTPUT}" @@ -176,7 +214,18 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then fi # Note: check carefully if multi "javeagent" params are set - export JAVA_TOOL_OPTIONS="-javaagent:${PLUGINS}/${OT_JAR}" + # The JVM splits JAVA_TOOL_OPTIONS on whitespace, so an overridden $PLUGINS + # containing a space would turn the agent option into two arguments. The JVM + # honours quoted options, so wrap it in a quote the path does not contain. + if [[ ${OT_JAR_PATH} != *"'"* ]]; then + AGENT_QUOTE="'" + elif [[ ${OT_JAR_PATH} != *\"* ]]; then + AGENT_QUOTE='"' + else + echo "## Error: ${OT_JAR_PATH} contains both quote characters, which JAVA_TOOL_OPTIONS cannot carry." >>"${OUTPUT}" + exit 1 + fi + export JAVA_TOOL_OPTIONS="${AGENT_QUOTE}-javaagent:${OT_JAR_PATH}${AGENT_QUOTE}" export OTEL_TRACES_EXPORTER=otlp export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=none @@ -191,12 +240,17 @@ fi #if [ "${JMX_EXPORT_PORT}" != "" ] && [ ${JMX_EXPORT_PORT} -ne 0 ] ; then # JAVA_OPTIONS="${JAVA_OPTIONS} -javaagent:${LIB}/jmx_prometheus_javaagent-0.16.1.jar=${JMX_EXPORT_PORT}:${CONF}/jmx_exporter.yml" #fi -if [ "$(ps -ef | grep -v grep | grep java | grep -cE "${CONF}")" -ne 0 ]; then +if [ "$(ps -ef | grep -v grep | grep java | grep -cF -- "${CONF}")" -ne 0 ]; then echo "HugeGraphPDServer is already running..." exit 0 fi -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS} -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager" +JVM_OPTIONS=("-Dlog4j.configurationFile=${CONF}/log4j2.xml" + "-Dlogging.config=${CONF}/log4j2.xml" + "-DLOG_PATH=${LOGS}" + "-Dbolt.log.path=${LOGS}" + "-Dlogging.path=${LOGS}" + "-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager") source "$BIN/preload-topling.sh" || exit 1 BOOT_JARS=("${LIB}"/hg-pd-service-*.jar) @@ -210,14 +264,20 @@ if [ -n "${TOPLING_RUNTIME_CLASSPATH:-}" ]; then fi # Turn on security check +# NOTE: "$JVM_MODULE_OPTIONS" and "$OUTPUT" are passed/redirected as single +# quoted arguments, and JVM_OPTIONS/JAVA_OPTIONS are expanded as arrays +# ("${JVM_OPTIONS[@]}"), so an overridden CONF/LOGS/PLUGINS path containing a +# space stays one argument instead of being split by word-splitting. if [[ $DAEMON == "true" ]]; then echo "Starting HugeGraphPDServer in daemon mode..." if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" & + exec "${JAVA}" "@${JVM_MODULE_OPTIONS}" -Dname="HugeGraphPD" \ + "${JVM_OPTIONS[@]}" "${JAVA_OPTIONS[@]}" \ + -Dspring.config.location="${CONF}/application.yml" "${JAVA_MAIN[@]}" "$@" & else - exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 & + exec "${JAVA}" "@${JVM_MODULE_OPTIONS}" -Dname="HugeGraphPD" \ + "${JVM_OPTIONS[@]}" "${JAVA_OPTIONS[@]}" \ + -Dspring.config.location="${CONF}/application.yml" "${JAVA_MAIN[@]}" "$@" >> "${OUTPUT}" 2>&1 & fi PID="$!" # Write pid to file @@ -229,10 +289,12 @@ else echo "$$" > "$PID_FILE" echo "[+pid] $$" if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" + exec "${JAVA}" "@${JVM_MODULE_OPTIONS}" -Dname="HugeGraphPD" \ + "${JVM_OPTIONS[@]}" "${JAVA_OPTIONS[@]}" \ + -Dspring.config.location="${CONF}/application.yml" "${JAVA_MAIN[@]}" "$@" else - exec ${JAVA} "@$JVM_MODULE_OPTIONS" -Dname="HugeGraphPD" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - -Dspring.config.location=${CONF}/application.yml "${JAVA_MAIN[@]}" "$@" >> ${OUTPUT} 2>&1 + exec "${JAVA}" "@${JVM_MODULE_OPTIONS}" -Dname="HugeGraphPD" \ + "${JVM_OPTIONS[@]}" "${JAVA_OPTIONS[@]}" \ + -Dspring.config.location="${CONF}/application.yml" "${JAVA_MAIN[@]}" "$@" >> "${OUTPUT}" 2>&1 fi fi diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh index c00d2f76e5..49f4ed090b 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/stop-hugegraph-pd.sh @@ -39,6 +39,12 @@ TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh +reject_unsafe_path "$PID_FILE_OVERRIDE" "-i pid file" crlf || exit 1 + +# Resolve to the same absolute path start-hugegraph-pd.sh wrote. "nocreate": +# stop only reads the pid file +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE" nocreate)" || exit 1 + PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=30 @@ -48,8 +54,10 @@ if [ ! -f "${PID_FILE}" ]; then fi PID=$(cat "$PID_FILE") -kill_process_and_wait "HugeGraphPDServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S" - -if [ $? -eq 0 ]; then +# An unchecked call would let the script fall off the end with status 0, so a +# shutdown that timed out with the JVM still alive would be reported as success. +if kill_process_and_wait "HugeGraphPDServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S"; then rm "$PID_FILE" +else + exit 1 fi diff --git a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh index 377b74675f..1408a68ffd 100644 --- a/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh @@ -91,10 +91,13 @@ function shell_quote() { printf "'%s'" "${1//$sq/$repl}" } -# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron -# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE -# canonicalize_dir/canonicalize_file. -# cron : crlf + reject '%'. +# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron|classpath +# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE +# canonicalize_dir/canonicalize_file. +# cron : crlf + reject '%'. +# classpath : crlf + reject ':'. For a directory whose contents are joined +# into a ':'-separated CLASSPATH (the plugins dir), where a ':' +# in the path itself would split one entry into two bogus ones. function reject_unsafe_path() { case "$1" in *[$'\n\r']*) @@ -102,12 +105,19 @@ function reject_unsafe_path() { return 1 ;; esac - [ "$3" = "cron" ] || return 0 - - case "$1" in - *%*) - echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 - return 1 ;; + case "$3" in + cron) + case "$1" in + *%*) + echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 + return 1 ;; + esac ;; + classpath) + case "$1" in + *:*) + echo "ERROR: $2 must not contain ':' (CLASSPATH separator)" >&2 + return 1 ;; + esac ;; esac return 0 } @@ -118,6 +128,55 @@ function cron_quote() { shell_quote "$1" } +# Fail fast when a required config file is missing or unreadable, instead of +# letting startup continue on silent defaults and fail later inside the JVM. +# $1 = file $2 = for error message +function ensure_file_readable() { + local file="$1" description="$2" + if [ ! -f "$file" ]; then + echo "ERROR: missing $description: '$file'" >&2 + if [ -n "${CONF_OVERRIDE:-}" ]; then + echo " The conf dir passed with -c must contain the same files" >&2 + echo " as the packaged conf directory." >&2 + fi + return 1 + fi + if [ ! -r "$file" ]; then + echo "ERROR: unreadable $description: '$file'" >&2 + echo " Check the file's ownership and permissions." >&2 + return 1 + fi + return 0 +} + +# A relative path INSIDE a config file is resolved by the JVM against the +# working directory, which the start script sets to $TOP -- not against the +# directory the config file itself lives in. So when -c points at an external +# conf dir, a stock value such as './conf/hugegraph.license' still reads +# $TOP/conf and the operator's copy is ignored with no message at all. +# Refuse to start instead, and say exactly what to change. +# $1 = value read from the config $2 = property name $3 = config file +function reject_relative_conf_path() { + local value="$1" property="$2" file="$3" + # Only an overridden conf dir can disagree with the working directory. + [ -z "${CONF_OVERRIDE:-}" ] && return 0 + [ -z "$value" ] && return 0 + case "$value" in + /*) return 0 ;; + esac + # Build the suggestion: the stock values are written relative to $TOP (e.g. + # './conf/hugegraph.license'), while $CONF_OVERRIDE already IS the conf dir, + # so drop a leading './' and 'conf/' to avoid suggesting '.../conf/conf/...'. + local suggestion="${value#./}" + suggestion="${suggestion#conf/}" + echo "ERROR: '$property' is the relative path '$value' in $file." >&2 + echo " Relative paths resolve against the working directory, so with" >&2 + echo " -c '$CONF_OVERRIDE' this would silently keep using the packaged" >&2 + echo " directory instead of your copy. Set '$property' to an absolute" >&2 + echo " path (for example '$CONF_OVERRIDE/$suggestion')." >&2 + return 1 +} + function crontab_append() { local job="$1" crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 @@ -267,24 +326,49 @@ function ensure_path_writable() { } # Canonicalize a (possibly relative) directory override into an absolute path. +# $1 = path, $2 = mode (optional) +# default : create the directory if missing. -l and -o name directories the +# scripts write into, so creating them is the expected setup step. +# "nocreate" : resolve only. The directory is never created, and a missing one +# is an error. Used for -c: a conf dir is only ever READ, so a +# typo such as '-c /dat/conf' must fail here naming the wrong +# directory, instead of silently creating an empty one and +# failing later on a file missing inside it. function canonicalize_dir() { local path="$1" + local mode="${2:-create}" [ -z "$path" ] && return 0 - mkdir -p "$path" 2>/dev/null + if [ "$mode" != "nocreate" ]; then + mkdir -p "$path" 2>/dev/null + elif [ ! -d "$path" ]; then + echo "Error: no such directory '$path'" >&2 + exit 1 + fi (cd "$path" 2>/dev/null && pwd) || { echo "Error: cannot resolve path '$path'" >&2 exit 1 } } -# Canonicalize a (possibly relative) file override (e.g. the pid file) into an absolute path +# Canonicalize a (possibly relative) file override (e.g. the pid file) into an +# absolute path. +# $1 = path, $2 = mode (optional) +# default : create the parent directory if missing (start/restart scripts, +# which have to be able to write the pid file afterwards). +# "nocreate" : resolve only. The parent is never created, and a missing one +# is an error. The stop scripts only ever READ the pid file, so +# they must not leave an empty directory tree behind for a typo +# such as '-i /var/run/hugegraf/pd.pid'. function canonicalize_file() { local path="$1" + local mode="${2:-create}" [ -z "$path" ] && return 0 local dir file abs_dir dir="$(dirname "$path")" file="$(basename "$path")" - mkdir -p "$dir" 2>/dev/null + if [ "$mode" != "nocreate" ]; then + mkdir -p "$dir" 2>/dev/null + fi abs_dir="$(cd "$dir" 2>/dev/null && pwd)" || { echo "Error: cannot resolve path '$path'" >&2 exit 1 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh index 49989a86c5..338a109112 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/hugegraph-server.sh @@ -39,7 +39,7 @@ LIB="$TOP/lib" EXT="$TOP/ext" PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" LOGS="${LOGS_OVERRIDE:-$TOP/logs}" -OUTPUT=${LOGS}/hugegraph-server.log +OUTPUT="${LOGS}/hugegraph-server.log" GITHUB="https://github.com" # Launcher errors and warnings go to ${OUTPUT} (hugegraph-server.log) when it can @@ -106,9 +106,9 @@ cd "${TOP}" || exit 1 # Find java & enable server option if [ "$JAVA_HOME" = "" ]; then - JAVA="java -server" + JAVA=("java" "-server") else - JAVA="$JAVA_HOME/bin/java -server" + JAVA=("$JAVA_HOME/bin/java" "-server") fi # Pick the JVM banner line explicitly, anchored to its "java version"/"openjdk @@ -117,7 +117,7 @@ fi # loaded that way may print its own banner containing 'version "..."' (an APM # agent, for example), so matching any line with 'version "' can read the # agent version instead of the runtime version. -JAVA_VERSION=$($JAVA -version 2>&1 | +JAVA_VERSION=$("${JAVA[@]}" -version 2>&1 | awk -F'"' '/^(java|openjdk) version "/ {print $2; exit}' | sed 's/^1\.//' | cut -d'.' -f1) # Drop any pre-release suffix, e.g. "24-ea" -> "24" @@ -132,18 +132,30 @@ if [[ ! -r ${JVM_MODULE_OPTIONS} ]]; then exit 1 fi -# Set Java options -if [ "$JAVA_OPTIONS" = "" ]; then +# Set Java options. +# Script-generated options are kept in arrays so that overridden paths +# containing spaces (CONF/LOGS/PLUGINS) remain a single argument when passed to +# java. User-supplied strings ($JAVA_OPTIONS / $USER_OPTION) keep the historical +# whitespace-splitting contract. +JAVA_OPTIONS_ENV="${JAVA_OPTIONS}" +JAVA_OPTIONS=() +if [ "$JAVA_OPTIONS_ENV" = "" ]; then XMX=$(calc_xmx $MIN_MEM $MAX_MEM) if [ $? -ne 0 ]; then report_error "Failed to start HugeGraphServer, requires at least ${MIN_MEM}MB free memory" exit 1 fi - JAVA_OPTIONS="-Xms${MIN_MEM}m -Xmx${XMX}m ${USER_OPTION}" + JAVA_OPTIONS=("-Xms${MIN_MEM}m" "-Xmx${XMX}m") + + # Split user-supplied options on whitespace (legacy behaviour) + USER_OPTIONS=() + read -r -a USER_OPTIONS <<< "${USER_OPTION}" + JAVA_OPTIONS+=(${USER_OPTIONS[@]+"${USER_OPTIONS[@]}"}) # Rolling out detailed GC logs - #JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseGCLogFileRotation -XX:GCLogFileSize=10M -XX:NumberOfGCLogFiles=3 \ - # -Xloggc:./logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps" + #JAVA_OPTIONS+=("-Xlog:gc=info:file=${LOGS}/gc.log:time,uptime,level,tags:filecount=3,filesize=100m") +else + read -r -a JAVA_OPTIONS <<< "${JAVA_OPTIONS_ENV}" fi # Keep JVM/caller GC selection by default; explicitly select G1 when requested. @@ -151,28 +163,33 @@ case "$GC_OPTION" in ""|g1|G1) if [[ "$GC_OPTION" == g1 || "$GC_OPTION" == G1 ]]; then echo "Using G1GC" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseG1GC" + JAVA_OPTIONS+=("-XX:+UseG1GC") else echo "Using JVM garbage collector configuration" fi - JAVA_OPTIONS="-XX:+ParallelRefProcEnabled -XX:InitiatingHeapOccupancyPercent=50 \ - -XX:G1RSetUpdatingPauseTimePercent=5 ${JAVA_OPTIONS}" + JAVA_OPTIONS=("-XX:+ParallelRefProcEnabled" "-XX:InitiatingHeapOccupancyPercent=50" + "-XX:G1RSetUpdatingPauseTimePercent=5" + ${JAVA_OPTIONS[@]+"${JAVA_OPTIONS[@]}"}) ;; zgc|ZGC) echo "Using ZGC as the default garbage collector (requires Java 17 or later)" - JAVA_OPTIONS="${JAVA_OPTIONS} -XX:+UseZGC -XX:+UnlockExperimentalVMOptions \ - -XX:ConcGCThreads=2 -XX:ParallelGCThreads=6 \ - -XX:ZCollectionInterval=120 -XX:ZAllocationSpikeTolerance=5 \ - -XX:+UnlockDiagnosticVMOptions -XX:-ZProactive" + JAVA_OPTIONS+=("-XX:+UseZGC" "-XX:+UnlockExperimentalVMOptions" + "-XX:ConcGCThreads=2" "-XX:ParallelGCThreads=6" + "-XX:ZCollectionInterval=120" "-XX:ZAllocationSpikeTolerance=5" + "-XX:+UnlockDiagnosticVMOptions" "-XX:-ZProactive") ;; *) report_error "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" exit 1 esac -JVM_OPTIONS="-Dlog4j.configurationFile=${CONF}/log4j2.xml -Dlogging.config=${CONF}/log4j2.xml -DLOG_PATH=${LOGS} -Dbolt.log.path=${LOGS} -Dlogging.path=${LOGS}" +JVM_OPTIONS=("-Dlog4j.configurationFile=${CONF}/log4j2.xml" + "-Dlogging.config=${CONF}/log4j2.xml" + "-DLOG_PATH=${LOGS}" + "-Dbolt.log.path=${LOGS}" + "-Dlogging.path=${LOGS}") -SECURITY_MANAGER_OPTION="" +SECURITY_MANAGER_OPTION=() if [[ ${OPEN_SECURITY_CHECK} == "true" ]]; then if [[ ${JAVA_VERSION} -gt ${MAX_SECURITY_JAVA_VERSION} ]]; then SECURITY_UNSUPPORTED_MSG=$(cat < setting a finite positive networkaddress.cache.ttl." fi - JVM_OPTIONS="${JVM_OPTIONS} \ - -Djava.security.properties=${SECURITY_PROPERTIES}" + JVM_OPTIONS+=("-Djava.security.properties=${SECURITY_PROPERTIES}") if [[ ${JAVA_VERSION} -ge 18 ]]; then # Required to install HugeSecurityManager programmatically on JDK 18+. - SECURITY_MANAGER_OPTION="-Djava.security.manager=allow" + SECURITY_MANAGER_OPTION=("-Djava.security.manager=allow") fi fi @@ -251,7 +270,19 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then # Note: check carefully if multi "javeagent" params are set # Append, so the operator's JAVA_TOOL_OPTIONS stays; the crash-file defaults # are put in front of all of it below. - export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:+${JAVA_TOOL_OPTIONS} }-javaagent:${PLUGINS}/${OT_JAR}" + # The JVM splits JAVA_TOOL_OPTIONS on whitespace, so an overridden $PLUGINS + # containing a space would turn the agent option into two arguments and fail + # the launch. The JVM does honour quoted options, so wrap the option in a + # quote character the path itself does not contain. + if [[ ${OT_JAR_PATH} != *"'"* ]]; then + AGENT_QUOTE="'" + elif [[ ${OT_JAR_PATH} != *\"* ]]; then + AGENT_QUOTE='"' + else + report_error "## Error: ${OT_JAR_PATH} contains both quote characters, which JAVA_TOOL_OPTIONS cannot carry." + exit 1 + fi + export JAVA_TOOL_OPTIONS="${JAVA_TOOL_OPTIONS:+${JAVA_TOOL_OPTIONS} }${AGENT_QUOTE}-javaagent:${OT_JAR_PATH}${AGENT_QUOTE}" export OTEL_TRACES_EXPORTER=otlp export OTEL_METRICS_EXPORTER=none export OTEL_LOGS_EXPORTER=none @@ -268,7 +299,7 @@ fi # stderr only reaches hugegraph-server-stdout.log; in STDOUT_MODE it reaches the # container log, which keeps a single earlier generation, so the file copy is # what survives a crash loop on a mounted logs volume. -JVM_OPTIONS="${JVM_OPTIONS} -Dhugegraph.bootstrap.error.log=${OUTPUT}" +JVM_OPTIONS+=("-Dhugegraph.bootstrap.error.log=${OUTPUT}") # Keep heap dumps and JVM crash logs in $LOGS whatever JAVA_OPTIONS holds. The # defaults come first in JAVA_TOOL_OPTIONS, ahead of the operator's own value. The @@ -364,14 +395,16 @@ esac # Turn on security check if [[ "${STDOUT_MODE:-false}" == "true" ]]; then - exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - ${SECURITY_MANAGER_OPTION} -cp "${CLASSPATH}:" \ + exec "${JAVA[@]}" @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" \ + "${JVM_OPTIONS[@]}" ${JAVA_OPTIONS[@]+"${JAVA_OPTIONS[@]}"} \ + ${SECURITY_MANAGER_OPTION[@]+"${SECURITY_MANAGER_OPTION[@]}"} -cp "${CLASSPATH}:" \ org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \ - ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} + "${OPEN_SECURITY_CHECK}" "${GREMLIN_SERVER_CONF}" "${REST_SERVER_CONF}" else - exec ${JAVA} @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" ${JVM_OPTIONS} ${JAVA_OPTIONS} \ - ${SECURITY_MANAGER_OPTION} -cp "${CLASSPATH}:" \ + exec "${JAVA[@]}" @"${JVM_MODULE_OPTIONS}" -Dname="HugeGraphServer" \ + "${JVM_OPTIONS[@]}" ${JAVA_OPTIONS[@]+"${JAVA_OPTIONS[@]}"} \ + ${SECURITY_MANAGER_OPTION[@]+"${SECURITY_MANAGER_OPTION[@]}"} -cp "${CLASSPATH}:" \ org.apache.hugegraph.bootstrap.HugeGraphServerBootstrap \ - ${OPEN_SECURITY_CHECK} ${GREMLIN_SERVER_CONF} ${REST_SERVER_CONF} \ + "${OPEN_SECURITY_CHECK}" "${GREMLIN_SERVER_CONF}" "${REST_SERVER_CONF}" \ >> "${LOGS}/hugegraph-server-stdout.log" 2>&1 fi diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh index b446231590..7246d659f3 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/init-store.sh @@ -40,8 +40,14 @@ done . "${BIN}"/util.sh +# Validate the RAW overrides before canonicalization: command substitution +# strips trailing newlines, which would hide an embedded CR/LF. +reject_unsafe_path "$CONF_OVERRIDE" "-c conf dir" crlf || exit 1 +# The plugins dir is joined into the ':'-separated CLASSPATH +reject_unsafe_path "$PLUGINS_OVERRIDE" "-o plugins dir" classpath || exit 1 + # Canonicalize relative path overrides to absolute paths -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE" nocreate)" || exit 1 PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 CONF="${CONF_OVERRIDE:-$TOP/conf}" @@ -52,6 +58,17 @@ JVM_MODULE_OPTIONS="${BIN}/jvm-module.options" configure_riscv64_libatomic || exit 1 ensure_path_writable "${PLUGINS}" +# Verify the config exists before reading it: read_property returns an empty +# string for a missing file, which is indistinguishable from an unset property. +# InitStore is handed this same file below, so fail here with a clear message. +ensure_file_readable "${CONF}/rest-server.properties" "REST server config" || exit 1 + +# InitStore reads 'graphs' relative to the working directory (set to $TOP +# below), not relative to $CONF, so a relative value would silently initialize +# the packaged graphs instead of the ones under an overridden -c directory. +GRAPHS_DIR=$(read_property "${CONF}/rest-server.properties" "graphs") +reject_relative_conf_path "$GRAPHS_DIR" "graphs" "${CONF}/rest-server.properties" || exit 1 + if [[ -n "$JAVA_HOME" ]]; then JAVA="$JAVA_HOME"/bin/java else diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh index 200c74de5b..5e61ca2ee4 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-hugegraph.sh @@ -56,7 +56,7 @@ while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do l) reject_unsafe_path "$OPTARG" "-l logs dir" crlf || exit 1 LOGS_OVERRIDE="$OPTARG" ;; m) OPEN_MONITOR="$OPTARG" ;; - o) reject_unsafe_path "$OPTARG" "-o plugins dir" crlf || exit 1 + o) reject_unsafe_path "$OPTARG" "-o plugins dir" classpath || exit 1 PLUGINS_OVERRIDE="$OPTARG" ;; p) PRELOAD="$OPTARG" ;; s) OPEN_SECURITY_CHECK="$OPTARG" ;; @@ -69,7 +69,7 @@ while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do done # Canonicalize relative path overrides to absolute paths. -CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE")" || exit 1 +CONF_OVERRIDE="$(canonicalize_dir "$CONF_OVERRIDE" nocreate)" || exit 1 LOGS_OVERRIDE="$(canonicalize_dir "$LOGS_OVERRIDE")" || exit 1 PLUGINS_OVERRIDE="$(canonicalize_dir "$PLUGINS_OVERRIDE")" || exit 1 PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE")" || exit 1 @@ -101,12 +101,21 @@ if [[ "$OPEN_SECURITY_CHECK" != "true" && "$OPEN_SECURITY_CHECK" != "false" ]]; exit_with_usage_help fi +# Verify the config files exist before reading them +ensure_file_readable "$CONF/rest-server.properties" "REST server config" || exit 1 +ensure_file_readable "$CONF/gremlin-server.yaml" "Gremlin server config" || exit 1 + GREMLIN_SERVER_URL=$(read_property "$CONF/rest-server.properties" "gremlinserver.url") if [ -z "$GREMLIN_SERVER_URL" ]; then GREMLIN_SERVER_URL="http://127.0.0.1:8182" fi REST_SERVER_URL=$(read_property "$CONF/rest-server.properties" "restserver.url") +# 'graphs' is read by HugeGraphServer/GraphManager relative to the working +# directory (set to $TOP below), not relative to $CONF. +GRAPHS_DIR=$(read_property "$CONF/rest-server.properties" "graphs") +reject_relative_conf_path "$GRAPHS_DIR" "graphs" "$CONF/rest-server.properties" || exit 1 + check_port "$GREMLIN_SERVER_URL" check_port "$REST_SERVER_URL" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh index 89eaf80da8..ac823e3468 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/stop-hugegraph.sh @@ -45,6 +45,12 @@ TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh +reject_unsafe_path "$PID_FILE_OVERRIDE" "-i pid file" crlf || exit 1 + +# Resolve to the same absolute path start-hugegraph.sh wrote. "nocreate": +# stop only reads the pid file +PID_FILE_OVERRIDE="$(canonicalize_file "$PID_FILE_OVERRIDE" nocreate)" || exit 1 + PID_FILE="${PID_FILE_OVERRIDE:-$BIN/pid}" SERVER_SHUTDOWN_TIMEOUT_S=10 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index 71faa5e670..db7c8c15d5 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -327,10 +327,13 @@ function shell_quote() { printf "'%s'" "${1//$sq/$repl}" } -# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron -# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE -# canonicalize_dir/canonicalize_file. -# cron : crlf + reject '%'. +# Validate a path-like value. $1=value $2=label for the error $3=crlf|cron|classpath +# crlf : reject CR/LF. Call on the RAW value (e.g. $OPTARG) BEFORE +# canonicalize_dir/canonicalize_file. +# cron : crlf + reject '%'. +# classpath : crlf + reject ':'. For a directory whose contents are joined +# into a ':'-separated CLASSPATH (the plugins dir), where a ':' +# in the path itself would split one entry into two bogus ones. function reject_unsafe_path() { case "$1" in *[$'\n\r']*) @@ -338,12 +341,19 @@ function reject_unsafe_path() { return 1 ;; esac - [ "$3" = "cron" ] || return 0 - - case "$1" in - *%*) - echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 - return 1 ;; + case "$3" in + cron) + case "$1" in + *%*) + echo "ERROR: $2 must not contain '%' (reserved by cron)" >&2 + return 1 ;; + esac ;; + classpath) + case "$1" in + *:*) + echo "ERROR: $2 must not contain ':' (CLASSPATH separator)" >&2 + return 1 ;; + esac ;; esac return 0 } @@ -354,6 +364,57 @@ function cron_quote() { shell_quote "$1" } +# Fail fast when a required config file is missing or unreadable. +# read_property() returns an empty string in that case, which otherwise looks +# exactly like "the property is not set" and lets startup continue with silent +# defaults. +# $1 = file $2 = for error message +function ensure_file_readable() { + local file="$1" description="$2" + if [ ! -f "$file" ]; then + echo "ERROR: missing $description: '$file'" >&2 + if [ -n "${CONF_OVERRIDE:-}" ]; then + echo " The conf dir passed with -c must contain the same files" >&2 + echo " as the packaged conf directory." >&2 + fi + return 1 + fi + if [ ! -r "$file" ]; then + echo "ERROR: unreadable $description: '$file'" >&2 + echo " Check the file's ownership and permissions." >&2 + return 1 + fi + return 0 +} + +# A relative path inside a config file is resolved by the JVM against the +# working directory, which the start scripts set to $TOP -- not against the +# directory the config file itself lives in. So when -c points at an external +# conf dir, a value such as 'graphs=./conf/graphs' still loads +# $TOP/conf/graphs and the operator's copy is ignored with no message at all. +# Refuse to start in such case when -c is set and say exactly what to change. +# $1 = value read from the config $2 = property name $3 = config file +function reject_relative_conf_path() { + local value="$1" property="$2" file="$3" + # Only an overridden conf dir can disagree with the working directory. + [ -z "${CONF_OVERRIDE:-}" ] && return 0 + [ -z "$value" ] && return 0 + case "$value" in + /*) return 0 ;; + esac + # Build the suggestion: the stock values are written relative to $TOP (e.g. + # './conf/graphs'), while $CONF_OVERRIDE already IS the conf dir, so drop a + # leading './' and a leading 'conf/' to avoid suggesting '.../conf/conf/...'. + local suggestion="${value#./}" + suggestion="${suggestion#conf/}" + echo "ERROR: '$property' is the relative path '$value' in $file." >&2 + echo " Relative paths resolve against the working directory, so with" >&2 + echo " -c '$CONF_OVERRIDE' this would silently keep using the packaged" >&2 + echo " directory instead of your copy. Set '$property' to an absolute" >&2 + echo " path (for example '$CONF_OVERRIDE/$suggestion')." >&2 + return 1 +} + function crontab_append() { local job="$1" crontab -l 2>/dev/null | grep -F -- "$job" >/dev/null 2>&1 @@ -705,10 +766,24 @@ function ensure_path_writable() { } # Canonicalize a (possibly relative) directory override into an absolute path. +# $1 = path, $2 = mode (optional) +# default : create the directory if missing. -l and -o name directories the +# scripts write into, so creating them is the expected setup step. +# "nocreate" : resolve only. The directory is never created, and a missing one +# is an error. Used for -c: a conf dir is only ever READ, so a +# typo such as '-c /dat/conf' must fail here naming the wrong +# directory, instead of silently creating an empty one and +# failing later on a file missing inside it. function canonicalize_dir() { local path="$1" + local mode="${2:-create}" [ -z "$path" ] && return 0 - mkdir -p "$path" 2>/dev/null + if [ "$mode" != "nocreate" ]; then + mkdir -p "$path" 2>/dev/null + elif [ ! -d "$path" ]; then + echo "Error: no such directory '$path'" >&2 + exit 1 + fi (cd "$path" 2>/dev/null && pwd) || { echo "Error: cannot resolve path '$path'" >&2 exit 1 @@ -716,14 +791,26 @@ function canonicalize_dir() { } # Canonicalize a (possibly relative) file override (e.g. the pid file) into an -# absolute path . +# absolute path. +# $1 = path, $2 = mode (optional) +# default : create the parent directory if missing (start/restart scripts, +# which have to be able to write the pid file afterwards). +# "nocreate" : resolve only. The parent is never created, and a missing one +# is an error. The stop scripts only ever READ the pid file, so +# they must not leave an empty directory tree behind for a typo +# such as '-i /var/run/hugegraf/server.pid'. function canonicalize_file() { local path="$1" + local mode="${2:-create}" [ -z "$path" ] && return 0 local dir file abs_dir dir="$(dirname "$path")" file="$(basename "$path")" - mkdir -p "$dir" 2>/dev/null + # In "nocreate" mode the parent is never created; a missing one then makes + # the cd below fail, which reports the error and exits. + if [ "$mode" != "nocreate" ]; then + mkdir -p "$dir" 2>/dev/null + fi abs_dir="$(cd "$dir" 2>/dev/null && pwd)" || { echo "Error: cannot resolve path '$path'" >&2 exit 1 diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh b/hugegraph-server/hugegraph-dist/src/test/shell/test-canonicalize.sh similarity index 59% rename from hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh rename to hugegraph-server/hugegraph-dist/src/test/shell/test-canonicalize.sh index 7677e453f3..2e9bfd13f0 100644 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-canonicalize.sh +++ b/hugegraph-server/hugegraph-dist/src/test/shell/test-canonicalize.sh @@ -16,31 +16,49 @@ # limitations under the License. # # Local unit tests for canonicalize_dir / canonicalize_file in util.sh. -# No cluster, Java, or CM needed. Run: bash test-canonicalize.sh +# Runs the full suite against the Server, PD and Store copies of util.sh in +# turn, since each module ships its own copy +# No cluster or Java needed. Run: bash test-canonicalize.sh set -u -UTIL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/util.sh" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Walk up to the repository root (the dir holding hugegraph-server/). +ROOT="$SCRIPT_DIR" +while [ "$ROOT" != "/" ] && [ ! -d "$ROOT/hugegraph-server" ]; do + ROOT="$(dirname "$ROOT")" +done -# We only want the helpers; source util.sh in a guarded way. util.sh has no -# top-level side effects for these functions, so a plain source is fine. -# shellcheck disable=SC1090 -. "$UTIL" +MODULE_NAMES=(server pd store) +MODULE_UTILS=( + "$ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh" + "$ROOT/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh" + "$ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh" +) PASS=0 FAIL=0 -TMP="$(mktemp -d)" -trap 'rm -rf "$TMP"' EXIT +SKIP=0 -ok() { echo "PASS: $1"; PASS=$((PASS+1)); } -bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } +ok() { echo "PASS: ${CURRENT_MODULE:+[$CURRENT_MODULE] }$1"; PASS=$((PASS+1)); } +bad() { echo "FAIL: ${CURRENT_MODULE:+[$CURRENT_MODULE] }$1"; FAIL=$((FAIL+1)); } +skip() { echo "SKIP: $1"; SKIP=$((SKIP+1)); } # Run a function in a subshell so its `exit 1` paths don't kill the harness. run_capture() { # usage: run_capture FUNC ARG ; sets OUT, RC - OUT="$("$@" 2>/tmp/_cz_err)"; RC=$? - ERR="$(cat /tmp/_cz_err)" + OUT="$("$@" 2>"$TMP/_cz_err")"; RC=$? + ERR="$(cat "$TMP/_cz_err")" } +run_suite() { # usage: run_suite MODULE_NAME UTIL_PATH + local CURRENT_MODULE="$1" + local UTIL="$2" + # shellcheck disable=SC1090 + . "$UTIL" + local TMP + TMP="$(mktemp -d)" + trap 'rm -rf "$TMP"' RETURN + echo "== canonicalize_dir ==" # 1. empty input -> empty output, rc 0 @@ -76,6 +94,47 @@ else fi chmod 700 "$RO" +echo "== canonicalize_dir nocreate (used for -c) ==" + +# 5a. nocreate: empty input is still a no-op +run_capture canonicalize_dir "" nocreate +[ $RC -eq 0 ] && [ -z "$OUT" ] && ok "dir/nocreate: empty input returns empty, rc=0" \ + || bad "dir/nocreate: empty input (rc=$RC out='$OUT')" + +# 5b. nocreate: an existing dir resolves exactly as in create mode +run_capture canonicalize_dir "$TMP" nocreate +[ "$OUT" = "$TMP" ] && ok "dir/nocreate: existing absolute path echoes itself" \ + || bad "dir/nocreate: existing path (out='$OUT' want='$TMP')" + +# 5c. nocreate: a relative existing dir is still canonicalized to absolute +mkdir -p "$TMP/exists" +( cd "$TMP" && run_capture canonicalize_dir "exists" nocreate \ + && [ "$OUT" = "$TMP/exists" ] ) \ + && ok "dir/nocreate: relative existing path canonicalized" \ + || bad "dir/nocreate: relative existing path (out='$OUT')" + +# 5d. THE POINT OF THE MODE: a missing dir must fail and must NOT be created. +# In create mode this same call would silently mkdir -p and succeed, so a typo +# like '-c /data/conf' would appear later as a missing file inside it. +MISSING="$TMP/missing_parent/missing_child" +run_capture canonicalize_dir "$MISSING" nocreate +{ [ $RC -eq 1 ] && [ ! -d "$MISSING" ] && echo "$ERR" | grep -q "no such directory"; } \ + && ok "dir/nocreate: missing dir -> exit 1, not created, clear message" \ + || bad "dir/nocreate: missing dir (rc=$RC created=$([ -d "$MISSING" ] && echo yes || echo no) err='$ERR')" + +# 5e. the default mode must be unchanged (create), so -l/-o keep working +run_capture canonicalize_dir "$TMP/default_mode_dir/subdir" +[ "$OUT" = "$TMP/default_mode_dir/subdir" ] && [ -d "$TMP/default_mode_dir/subdir" ] \ + && ok "dir: default mode still creates (regression guard for -l/-o)" \ + || bad "dir: default mode (out='$OUT')" + +# 5f. an unrecognized mode string must behave as 'create', not silently as +# nocreate -- a typo in a caller must not disable directory creation. +run_capture canonicalize_dir "$TMP/unknown_mode_dir/subdir" nocrate +[ "$OUT" = "$TMP/unknown_mode_dir/subdir" ] && [ -d "$TMP/unknown_mode_dir/subdir" ] \ + && ok "dir: unknown mode falls back to create" \ + || bad "dir: unknown mode (out='$OUT')" + echo "== canonicalize_file ==" # 6. empty input -> empty, rc 0 @@ -137,7 +196,19 @@ run_capture canonicalize_file "$TMP/startcreates/s.pid" run_capture canonicalize_file "" nocreate [ $RC -eq 0 ] && [ -z "$OUT" ] && ok "nocreate: empty input returns empty, rc=0" \ || bad "nocreate: empty input (rc=$RC out='$OUT')" +} + +for i in "${!MODULE_NAMES[@]}"; do + name="${MODULE_NAMES[$i]}" + util="${MODULE_UTILS[$i]}" + if [ ! -f "$util" ]; then + skip "$name: util.sh not found at $util" + continue + fi + echo "---- module: $name ($util) ----" + run_suite "$name" "$util" +done echo "-----------------------------" -echo "PASS=$PASS FAIL=$FAIL" +echo "PASS=$PASS FAIL=$FAIL SKIP=$SKIP" [ $FAIL -eq 0 ] diff --git a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh b/hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh similarity index 86% rename from hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh rename to hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh index 2689f0cf12..aa9bbe829a 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/test-cron-quote.sh +++ b/hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh @@ -18,23 +18,39 @@ # Local regression tests for the cron serialization helpers in util.sh: # shell_quote, reject_unsafe_path, cron_quote, # crontab_append, crontab_remove. +# Runs the full suite against the Server, PD and Store copies of util.sh in +# turn, since each module ships its own copy # No cluster, Java, or real crontab needed: `crontab` is replaced by a stub that # reads/writes a temp file, so the invoking user's crontab is not touched. # Run: bash test-cron-quote.sh set -u -UTIL="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/util.sh" -# shellcheck disable=SC1090 -. "$UTIL" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Walk up to the repository root (the dir holding hugegraph-server/). +ROOT="$SCRIPT_DIR" +while [ "$ROOT" != "/" ] && [ ! -d "$ROOT/hugegraph-server" ]; do + ROOT="$(dirname "$ROOT")" +done + +MODULE_NAMES=(server pd store) +MODULE_UTILS=( + "$ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh" + "$ROOT/hugegraph-pd/hg-pd-dist/src/assembly/static/bin/util.sh" + "$ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin/util.sh" +) PASS=0 FAIL=0 +SKIP=0 + +ok() { echo "PASS: ${CURRENT_MODULE:+[$CURRENT_MODULE] }$1"; PASS=$((PASS+1)); } +bad() { echo "FAIL: ${CURRENT_MODULE:+[$CURRENT_MODULE] }$1"; FAIL=$((FAIL+1)); } +skip() { echo "SKIP: $1"; SKIP=$((SKIP+1)); } + TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT -ok() { echo "PASS: $1"; PASS=$((PASS+1)); } -bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } # ---- stub crontab --------------------------------------------------------- STUB_DIR="$TMP/stubbin" @@ -59,6 +75,13 @@ SHELLS="/bin/sh" command -v dash >/dev/null 2>&1 && SHELLS="$SHELLS $(command -v dash)" [ -x /bin/bash ] && SHELLS="$SHELLS /bin/bash" +run_suite() { # usage: run_suite MODULE_NAME UTIL_PATH + local CURRENT_MODULE="$1" + local UTIL="$2" + # shellcheck disable=SC1090 + . "$UTIL" + : > "$CRON_FILE" + # --------------------------------------------------------------------------- echo "== A. shell_quote round-trips through each shell ==" @@ -147,6 +170,13 @@ expect_rc "cron: '%' rejected" 1 "/opt/100%/x" L cron expect_rc "cron: LF rejected" 1 $'/opt/x\ny' L cron expect_rc "cron: apostrophe/space accepted" 0 "/data/team's logs" L cron expect_rc "cron: empty accepted" 0 "" L cron +expect_rc "classpath: plain path accepted" 0 "/opt/x" L classpath +expect_rc "classpath: ':' rejected" 1 "/data/a:b/plugins" L classpath +expect_rc "classpath: LF rejected" 1 $'/opt/x\ny' L classpath +expect_rc "classpath: '%' accepted (not cron)" 0 "/opt/100%/x" L classpath +expect_rc "classpath: space/apostrophe accepted" 0 "/data/team's plugins" L classpath +expect_rc "crlf: ':' accepted " 0 "/data/a:b" L crlf +expect_rc "cron: ':' accepted " 0 "/data/a:b" L cron expect_rc "message names the label" 1 "a%b" "CONF_OVERRIDE" cron case "$LAST_ERR" in *CONF_OVERRIDE*"%"*) ok "error mentions label and %" ;; *) bad "error text: $LAST_ERR" ;; esac @@ -276,6 +306,8 @@ build_and_append $'/opt/x\n* * * * * evil'; rc=$? grep -q evil "$CRON_FILE" && bad "injected job reached crontab" || ok "no injected job in crontab" # start-monitor.sh itself: wrong order of validation must exit non-zero w/o touching cron. +# Only Server ships start-monitor.sh; PD/Store have no monitor script, so this +# check is naturally skipped for those modules via the -f guard below. SM="$(cd "$(dirname "$UTIL")" && pwd)/start-monitor.sh" if [ -f "$SM" ]; then out="$(JAVA_HOME=/usr CONF_OVERRIDE='/opt/100%/conf' bash "$SM" 2>&1)"; rc=$? @@ -283,10 +315,22 @@ if [ -f "$SM" ]; then && ok "start-monitor.sh exits non-zero and leaves crontab unchanged on '%'" \ || bad "start-monitor.sh with '%' (rc=$rc): $out" fi +} + +for i in "${!MODULE_NAMES[@]}"; do + name="${MODULE_NAMES[$i]}" + util="${MODULE_UTILS[$i]}" + if [ ! -f "$util" ]; then + skip "$name: util.sh not found at $util" + continue + fi + echo "---- module: $name ($util) ----" + run_suite "$name" "$util" +done # --------------------------------------------------------------------------- echo echo "Shells exercised: $SHELLS" command -v dash >/dev/null 2>&1 || echo "NOTE: dash not installed here; run on Debian/Ubuntu/CI to cover it." -echo "Passed: $PASS Failed: $FAIL" +echo "Passed: $PASS Failed: $FAIL Skipped: $SKIP" [ "$FAIL" -eq 0 ] diff --git a/hugegraph-server/hugegraph-dist/src/test/shell/test-launcher-argv.sh b/hugegraph-server/hugegraph-dist/src/test/shell/test-launcher-argv.sh new file mode 100644 index 0000000000..cef1454adf --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/test/shell/test-launcher-argv.sh @@ -0,0 +1,413 @@ +#!/bin/bash +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ARGV REGRESSION TEST for the real Server / PD / Store JVM launchers. +# +# The helpers (canonicalize_*, cron_quote) can be correct while the +# launcher can still break because the bug lives in the final `exec`: script-built +# options were interpolated into ONE string and then expanded unquoted. For eg: +# +# -c '/data/external conf' +# +# reached Java as TWO arguments: +# +# -Dlog4j.configurationFile=/data/external <- truncated +# conf/log4j2.xml <- read as the MAIN CLASS +# +# which can cause the reported failure like +# "Could not find or load main class conf.log4j2.xml". +# +# This does not inspect the scripts: it RUNS them against a stub `java` +# that dumps its argv one element per line, and asserts on that argv. A bad +# split shows up as an extra standalone element ("conf/log4j2.xml", "logs", +# "plugins") . +# +# bash test-launcher-argv.sh +# +# No JVM or cluster explictly is needed for this test. + +PASS=0; FAIL=0; SKIP=0 +ok() { echo "PASS: $1"; PASS=$((PASS+1)); } +bad() { echo "FAIL: $1"; FAIL=$((FAIL+1)); } +skip() { echo "SKIP: $1"; SKIP=$((SKIP+1)); } + +BIN="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$BIN" +while [ "$ROOT" != "/" ] && [ ! -d "$ROOT/hugegraph-server" ]; do + ROOT="$(dirname "$ROOT")" +done +SERVER_SRC="$ROOT/hugegraph-server/hugegraph-dist/src/assembly/static/bin" +PD_SRC="$ROOT/hugegraph-pd/hg-pd-dist/src/assembly/static/bin" +STORE_SRC="$ROOT/hugegraph-store/hg-store-dist/src/assembly/static/bin" + +# mktemp gives a space-free path on purpose: the point of the review is that the +# split happens even with a NORMAL install path, so only the overrides below +# carry spaces. +TMP="$(mktemp -d)" || { echo "cannot create temp dir"; exit 1; } +trap 'rm -rf "$TMP"' EXIT + +DUMP="$TMP/argv.txt" + +# --------------------------------------------------------------------------- +# Overrides whose LAST path segment is a distinctive word. If the launcher +# splits on the space, that word shows up as its own argv element, which is +# what the assertions look for. +# '<...>/external conf' split -> "conf/log4j2.xml" +# '<...>/external logs' split -> "logs" +# '<...>/external plugins' split -> "plugins" +# --------------------------------------------------------------------------- +CONF_X="$TMP/external conf" +LOGS_X="$TMP/external logs" +PLUGINS_X="$TMP/external plugins" +mkdir -p "$CONF_X" "$LOGS_X" "$PLUGINS_X" + +# --------------------------------------------------------------------------- +# Stub binaries +# --------------------------------------------------------------------------- +STUB="$TMP/stub" +mkdir -p "$STUB/bin" + +# The launchers probe the version first ("$JAVA" -version / "${JAVA[@]}" -version) +# and parse a line anchored to '^(java|openjdk) version "'. Answer that probe, +# and on the REAL launch dump argv one element per line and exit without +# starting anything. +cat > "$STUB/bin/java" <<'EOF' +#!/bin/bash +for a in "$@"; do + if [ "$a" = "-version" ]; then + echo 'openjdk version "17.0.9" 2023-10-17' >&2 + echo 'OpenJDK Runtime Environment (build 17.0.9+9)' >&2 + echo 'OpenJDK 64-Bit Server VM (build 17.0.9+9, mixed mode)' >&2 + exit 0 + fi +done +: > "${ARGV_DUMP:?ARGV_DUMP must be set}" +for a in "$@"; do printf '%s\n' "$a" >> "$ARGV_DUMP"; done +exit 0 +EOF + +# Store unconditionally tries to fetch libjemalloc before parsing any flag. +# Failing the download fast keeps the test offline; the launcher treats it as +# optional ("Failed to verify or download ... skip it") and continues. +cat > "$STUB/bin/curl" <<'EOF' +#!/bin/bash +exit 1 +EOF +cp "$STUB/bin/curl" "$STUB/bin/wget" +chmod +x "$STUB/bin/java" "$STUB/bin/curl" "$STUB/bin/wget" + +# --------------------------------------------------------------------------- +# build_install +# A minimal but structurally real install tree: the launcher scripts are copied +# VERBATIM from source (that is what is under test); only the surrounding jars +# and config files are synthetic. +# --------------------------------------------------------------------------- +build_install() { + local dest="$1" src="$2" bootjar="$3" + mkdir -p "$dest/bin" "$dest/lib" "$dest/ext" "$dest/conf" "$dest/plugins" + + cp "$src"/*.sh "$dest/bin/" 2>/dev/null + # preload-topling.sh and jvm-module.options ship in every component's bin in + # a real dist, but live only in the Server source tree here. + cp "$SERVER_SRC/preload-topling.sh" "$dest/bin/" 2>/dev/null + cp "$SERVER_SRC/jvm-module.options" "$dest/bin/" 2>/dev/null + chmod +x "$dest/bin"/*.sh 2>/dev/null + + # find -L needs real files; content is irrelevant to argv assembly. + : > "$dest/lib/placeholder.jar" + [ -n "$bootjar" ] && : > "$dest/lib/$bootjar" + return 0 +} + +# Spring Boot config for PD/Store. 'verify-path'/'license-path' must be +# ABSOLUTE or reject_relative_conf_path refuses to start when -c is set. +write_app_yml() { + cat > "$1/application.yml" </dev/null)" + echo "${n:-0}" +} +argv_has() { [ "$(argv_count "$1")" -ge 1 ]; } +argv_after() { # echo the element following the first exact match of $1 + [ -f "$DUMP" ] || return 1 + awk -v flag="$1" 'found {print; exit} $0 == flag {found=1}' "$DUMP" +} + +# One option must be ONE argv element, with the space intact. +assert_single() { #