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 d687db42cb..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 @@ -30,14 +30,18 @@ if [ -z "$DAEMON" ]; then DAEMON="true" fi -while getopts "d:g:j: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 [-d true|false] [-g g1|ZGC] [-j xxx] [-y true|false]" && exit 1 ;; + ?) echo "USAGE: $0 [-d true|false] [-c conf_dir] [-g g1|ZGC] [-i pid_file] [-j xxx] [-l logs_dir] [-o plugins_dir] [-y true|false]" && exit 1 ;; esac done @@ -53,13 +57,30 @@ function abs_path() { BIN=$(abs_path) TOP="$(cd "$BIN"/../ && pwd)" -CONF="$TOP/conf" + +. "$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" 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 + +CONF="${CONF_OVERRIDE:-$TOP/conf}" LIB="$TOP/lib" -PLUGINS="$TOP/plugins" -LOGS="$TOP/logs" -OUTPUT=${LOGS}/hugegraph-pd-stdout.log +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" +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}" JVM_MODULE_OPTIONS="$BIN/jvm-module.options" if [[ ! -f "$JVM_MODULE_OPTIONS" || ! -r "$JVM_MODULE_OPTIONS" ]]; then @@ -67,10 +88,27 @@ 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)) @@ -88,28 +126,41 @@ 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]*}" 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 -# 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} + >> "${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. @@ -117,22 +168,23 @@ 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} + echo "Unrecognized gc option: '$GC_OPTION', supported options: g1, ZGC" >> "${OUTPUT}" exit 1 esac @@ -146,7 +198,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,13 +208,24 @@ 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 # 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 @@ -177,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 -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) @@ -196,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 @@ -215,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 2220dd678f..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 @@ -27,22 +27,37 @@ 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 +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 -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) -kill_process_and_wait "HugeGraphPDServer" "$PID" "$SERVER_SHUTDOWN_TIMEOUT_S" - -if [ $? -eq 0 ]; then +PID=$(cat "$PID_FILE") +# 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 b476d7935b..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 @@ -82,27 +82,123 @@ 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|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']*) + echo "ERROR: $2 must not contain CR/LF characters" >&2 + return 1 ;; + esac + + 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 +} + +# 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" +} + +# 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 | 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 @@ -229,6 +325,57 @@ function ensure_path_writable() { fi } +# 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 + 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. +# $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")" + 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 + } + echo "$abs_dir/$file" +} + function get_ip() { local os=$(uname) local loopback="127.0.0.1" 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 3e0276ec0b..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 @@ -34,12 +34,12 @@ 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" -OUTPUT=${LOGS}/hugegraph-server.log +PLUGINS="${PLUGINS_OVERRIDE:-$TOP/plugins}" +LOGS="${LOGS_OVERRIDE:-$TOP/logs}" +OUTPUT="${LOGS}/hugegraph-server.log" GITHUB="https://github.com" # Launcher errors and warnings go to ${OUTPUT} (hugegraph-server.log) when it can @@ -93,7 +93,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")" @@ -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,27 +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" -SECURITY_MANAGER_OPTION="" +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 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 @@ -237,7 +257,7 @@ if [ "${OPEN_TELEMETRY}" == "true" ]; then fi fi - # Note: remember update it if we change the jar + # Note: remember update it if we change the jar expected_md5="e3bcbbe8ed9b6d840fa4c333b36f369f" actual_md5=$(md5sum "${OT_JAR_PATH}" | awk '{print $1}') @@ -250,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 @@ -267,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 @@ -363,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} \ - >> ${LOGS}/hugegraph-server-stdout.log 2>&1 + "${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 754a74cd67..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 @@ -27,16 +27,48 @@ 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 +# 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" nocreate)" || 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}" +# 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/monitor-hugegraph.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/monitor-hugegraph.sh index 11be239fe1..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,35 +25,42 @@ 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 -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 +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 - $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 28f36eb427..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 @@ -39,30 +39,60 @@ 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 +while getopts "c:d:g:i:j:l:m:o:p:s:t:y:" arg; do + case ${arg} in + c) reject_unsafe_path "$OPTARG" "-c conf dir" crlf || exit 1 + CONF_OVERRIDE="$OPTARG" ;; + d) DAEMON="$OPTARG" ;; + g) GC_OPTION="$OPTARG" ;; + i) reject_unsafe_path "$OPTARG" "-i pid file" crlf || exit 1 + PID_FILE_OVERRIDE="$OPTARG" ;; + j) USER_OPTION="$OPTARG" ;; + 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" classpath || exit 1 + PLUGINS_OVERRIDE="$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 +# Canonicalize relative path overrides to absolute paths. +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 + +# 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 exit_with_usage_help fi @@ -71,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/start-monitor.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/start-monitor.sh index ffe1a2243d..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" @@ -37,6 +37,30 @@ 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. +# 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 + 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 + 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 + 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 + PLUGINS_Q="$(cron_quote "$PLUGINS_OVERRIDE" "PLUGINS_OVERRIDE")" || exit 1 + CRONTAB_JOB="$CRONTAB_JOB export PLUGINS_OVERRIDE=$PLUGINS_Q &&" +fi +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 c63035e072..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 @@ -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,13 @@ TOP="$(cd $BIN/../ && pwd)" . "$BIN"/util.sh -PID_FILE=$BIN/pid +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 if [ "$CLOSE_MONITOR" == "true" ]; then @@ -61,7 +68,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/util.sh b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh index 65720021eb..db7c8c15d5 100755 --- a/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh +++ b/hugegraph-server/hugegraph-dist/src/assembly/static/bin/util.sh @@ -318,27 +318,125 @@ 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|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']*) + echo "ERROR: $2 must not contain CR/LF characters" >&2 + return 1 ;; + esac + + 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 +} + +# 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" +} + +# 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 | 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 @@ -667,6 +765,59 @@ function ensure_path_writable() { fi } +# 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 + 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. +# $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")" + # 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 + } + echo "$abs_dir/$file" +} + function get_ip() { local os=$(uname) local loopback="127.0.0.1" @@ -839,7 +990,8 @@ function kill_process_and_wait() { function exit_with_usage_help(){ echo "USAGE: $0 [-d true|false] [-g g1|G1|zgc|ZGC] [-m true|false] [-p true|false]" \ - "[-s true|false] [-j java_options] [-t timeout] [-y true|false]" + "[-s true|false] [-j java_options] [-t timeout] [-y true|false]" \ + "[-c conf_dir] [-l logs_dir] [-o plugins_dir] [-i pid_file]" echo "Without -g, JVM/caller collector selection applies. -j applies when JAVA_OPTIONS is empty." echo "Default/G1 tuning precedes caller options, so caller tuning values win." echo "Do not combine -g with a different collector in JAVA_OPTIONS or -j." 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 bc40d2955e..a301f1fe22 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-server/hugegraph-dist/src/test/shell/test-canonicalize.sh b/hugegraph-server/hugegraph-dist/src/test/shell/test-canonicalize.sh new file mode 100644 index 0000000000..2e9bfd13f0 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/test/shell/test-canonicalize.sh @@ -0,0 +1,214 @@ +#!/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. +# 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 + +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)); } + +# 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")" +} + +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 +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_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 +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')" +} + +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 SKIP=$SKIP" +[ $FAIL -eq 0 ] diff --git a/hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh b/hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh new file mode 100755 index 0000000000..aa9bbe829a --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/test/shell/test-cron-quote.sh @@ -0,0 +1,336 @@ +#!/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. +# 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 + +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 + + +# ---- 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" + +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 ==" + +# 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 "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 +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. +# 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=$? + [ $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 +} + +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 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() { #