#!/usr/bin/env bash # WIKI: https://arthas.aliyun.com/doc # This script only supports bash, do not support posix sh. # If you have the problem like Syntax error: "(" unexpected (expecting "fi"), # Try to run "bash -version" to check the version. # Try to visit WIKI to find a solution. # program : Arthas # author : Core Engine @ Taobao.com # date : 2024-08-12 # current arthas script version ARTHAS_SCRIPT_VERSION=4.0.0 # SYNOPSIS # rreadlink # DESCRIPTION # Resolves to its ultimate target, if it is a symlink, and # prints its canonical path. If it is not a symlink, its own canonical path # is printed. # A broken symlink causes an error that reports the non-existent target. # LIMITATIONS # - Won't work with filenames with embedded newlines or filenames containing # the string ' -> '. # COMPATIBILITY # This is a fully POSIX-compliant implementation of what GNU readlink's # -e option does. # EXAMPLE # In a shell script, use the following to get that script's true directory of origin: # trueScriptDir=$(dirname -- "$(rreadlink "$0")") rreadlink() ( # Execute the function in a *subshell* to localize variables and the effect of `cd`. target=$1 fname= targetDir= CDPATH= # Try to make the execution environment as predictable as possible: # All commands below are invoked via `command`, so we must make sure that # `command` itself is not redefined as an alias or shell function. # (Note that command is too inconsistent across shells, so we don't use it.) # `command` is a *builtin* in bash, dash, ksh, zsh, and some platforms do not # even have an external utility version of it (e.g, Ubuntu). # `command` bypasses aliases and shell functions and also finds builtins # in bash, dash, and ksh. In zsh, option POSIX_BUILTINS must be turned on for # that to happen. { \unalias command; \unset -f command; } >/dev/null 2>&1 [ -n "$ZSH_VERSION" ] && options[POSIX_BUILTINS]=on # make zsh find *builtins* with `command` too. while :; do # Resolve potential symlinks until the ultimate target is found. [ -L "$target" ] || [ -e "$target" ] || { command printf '%s\n' "ERROR: '$target' does not exist." >&2; return 1; } command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path. fname=$(command basename -- "$target") # Extract filename. [ "$fname" = '/' ] && fname='' # !! curiously, `basename /` returns '/' if [ -L "$fname" ]; then # Extract [next] target path, which may be defined # *relative* to the symlink's own directory. # Note: We parse `ls -l` output to find the symlink target # which is the only POSIX-compliant, albeit somewhat fragile, way. target=$(command ls -l "$fname") target=${target#* -> } continue # Resolve [next] symlink target. fi break # Ultimate target reached. done targetDir=$(command pwd -P) # Get canonical dir. path # Output the ultimate target's canonical path. # Note that we manually resolve paths ending in /. and /.. to make sure we have a normalized path. if [ "$fname" = '.' ]; then command printf '%s\n' "${targetDir%/}" elif [ "$fname" = '..' ]; then # Caveat: something like /var/.. will resolve to /private (assuming /var@ -> /private/var), i.e. the '..' is applied # AFTER canonicalization. command printf '%s\n' "$(command dirname -- "${targetDir}")" else command printf '%s\n' "${targetDir%/}/$fname" fi ) DIR=$(dirname -- "$(rreadlink "${BASH_SOURCE[0]}")") ############ Command Arguments ############ # define arthas's home ARTHAS_HOME= # define arthas's lib if [ -z "${ARTHAS_LIB_DIR}" ]; then ARTHAS_LIB_DIR=${HOME}/.arthas/lib else echo "[INFO] ARTHAS_LIB_DIR: ${ARTHAS_LIB_DIR}" fi # target process id to attach TARGET_PID= # target process id to attach, default 127.0.0.1 TARGET_IP= DEFAULT_TARGET_IP="127.0.0.1" # telnet port, default 3658 TELNET_PORT= DEFAULT_TELNET_PORT="3658" # http port, default 8563 HTTP_PORT= DEFAULT_HTTP_PORT="8563" # telnet session timeout seconds, default 1800 SESSION_TIMEOUT= # use specify version USE_VERSION= # remote repo to download arthas REPO_MIRROR= # use http to download arthas USE_HTTP=false # attach only, do not telnet connect ATTACH_ONLY=false # pass debug arguments to the attach java process DEBUG_ATTACH=false # arthas-client terminal height HEIGHT= # arthas-client terminal width WIDTH= # select target process by classname or JARfilename SELECT= # Verbose, print debug info. VERBOSE=false # command to execute COMMAND= # batch file to execute BATCH_FILE= # tunnel server url TUNNEL_SERVER= # agent id AGENT_ID= # stat report url STAT_URL= # app name APP_NAME= # username USERNAME= # password PASSWORD= # disabledCommands DISABLED_COMMANDS= ############ Command Arguments ############ # if arguments contains -c/--command or -f/--batch-file, BATCH_MODE will be true BATCH_MODE=false # define arthas's temp dir TMP_DIR=/tmp # arthas remote url # https://arthas.aliyun.com/download/3.1.7?mirror=aliyun REMOTE_DOWNLOAD_URL="https://arthas.aliyun.com/download/PLACEHOLDER_VERSION?mirror=PLACEHOLDER_REPO" # update timeout(sec) SO_TIMEOUT=5 # define JVM's OPS JVM_OPTS="" ARTHAS_OPTS="-Djava.awt.headless=true" OS_TYPE= case "$(uname -s)" in Linux*) OS_TYPE=Linux;; Darwin*) OS_TYPE=Mac;; CYGWIN*) OS_TYPE=Cygwin;; MINGW*) OS_TYPE=MinGw;; *) OS_TYPE="UNKNOWN" esac # check curl/grep/awk/telnet/unzip command if ! [ -x "$(command -v curl)" ]; then echo 'Error: curl is not installed. Try to use java -jar arthas-boot.jar' >&2 exit 1 fi if ! [ -x "$(command -v grep)" ]; then echo 'Error: grep is not installed. Try to use java -jar arthas-boot.jar' >&2 exit 1 fi if ! [ -x "$(command -v awk)" ]; then echo 'Error: awk is not installed. Try to use java -jar arthas-boot.jar' >&2 exit 1 fi if ! [ -x "$(command -v telnet)" ]; then echo 'Error: telnet is not installed. Try to use java -jar arthas-boot.jar' >&2 exit 1 fi if ! [ -x "$(command -v unzip)" ]; then echo 'Error: unzip is not installed. Try to use java -jar arthas-boot.jar' >&2 exit 1 fi # exit shell with err_code # $1 : err_code # $2 : err_msg exit_on_err() { [[ ! -z "${2}" ]] && echo "${2}" 1>&2 exit ${1} } # get with default value # $1 : target value # $2 : default value default() { [[ ! -z "${1}" ]] && echo "${1}" || echo "${2}" } # check arthas permission check_permission() { [ ! -w "${HOME}" ] \ && exit_on_err 1 "permission denied, ${HOME} is not writable." } # reset arthas work environment # reset some options for env reset_for_env() { unset JAVA_TOOL_OPTIONS # init ARTHAS' lib mkdir -p "${ARTHAS_LIB_DIR}" \ || exit_on_err 1 "create ${ARTHAS_LIB_DIR} fail." # if env define the JAVA_HOME, use it first # if is alibaba opts, use alibaba ops's default JAVA_HOME [ -z "${JAVA_HOME}" ] && [ -d /opt/taobao/java ] && JAVA_HOME=/opt/taobao/java if [[ (-z "${JAVA_HOME}") && ( -e "/usr/libexec/java_home") ]]; then # for mac JAVA_HOME=`/usr/libexec/java_home` fi if [ -z "${JAVA_HOME}" ]; then # try to find JAVA_HOME from java command local JAVA_COMMAND_PATH=$( rreadlink $(type -p java) ) JAVA_HOME=$(echo "$JAVA_COMMAND_PATH" | sed -n 's/\/bin\/java$//p') fi # iterater through candidates to find a proper JAVA_HOME at least contains tools.jar which is required by arthas. if [ ! -d "${JAVA_HOME}" ]; then JAVA_HOME_CANDIDATES=($(ps aux | grep java | grep -v 'grep java' | awk '{print $11}' | sed -n 's/\/bin\/java$//p')) for JAVA_HOME_TEMP in ${JAVA_HOME_CANDIDATES[@]}; do if [ -f "${JAVA_HOME_TEMP}/lib/tools.jar" ]; then JAVA_HOME=`rreadlink "${JAVA_HOME_TEMP}"` break fi done fi if [ -z "${JAVA_HOME}" ]; then exit_on_err 1 "Can not find JAVA_HOME, please set \$JAVA_HOME bash env first." fi # maybe 1.8.0_162 , 11-ea local JAVA_VERSION local IFS=$'\n' # remove \r for Cygwin local lines=$("${JAVA_HOME}"/bin/java -version 2>&1 | tr '\r' '\n') for line in $lines; do if [[ (-z $JAVA_VERSION) && ($line = *"version \""*) ]] then local ver=$(echo $line | sed -e 's/.*version "\(.*\)"\(.*\)/\1/; 1q') # on macOS, sed doesn't support '?' if [[ $ver = "1."* ]] then JAVA_VERSION=$(echo $ver | sed -e 's/1\.\([0-9]*\)\(.*\)/\1/; 1q') else JAVA_VERSION=$(echo $ver | sed -e 's/\([0-9]*\)\(.*\)/\1/; 1q') fi fi done # when java version less than 9, we can use tools.jar to confirm java home. # when java version greater than 9, there is no tools.jar. if [[ "$JAVA_VERSION" -lt 9 ]];then # possible java homes javaHomes=("${JAVA_HOME%%/}" "${JAVA_HOME%%/}/.." "${JAVA_HOME%%/}/../..") for javaHome in ${javaHomes[@]} do toolsJar="$javaHome/lib/tools.jar" if [ -f $toolsJar ]; then JAVA_HOME=$( rreadlink $javaHome ) BOOT_CLASSPATH=-Xbootclasspath/a:$( rreadlink $toolsJar ) break fi done [ -z "${BOOT_CLASSPATH}" ] && exit_on_err 1 "tools.jar was not found, so arthas could not be launched!" fi echo "[INFO] JAVA_HOME: ${JAVA_HOME}" # reset CHARSET for alibaba opts, we use GBK [[ -x /opt/taobao/java ]] && JVM_OPTS="-Dinput.encoding=GBK ${JVM_OPTS} " } # get latest version from local get_local_version() { ls "${ARTHAS_LIB_DIR}" | sort | tail -1 } get_repo_url() { local repoUrl="${REPO_MIRROR}" if [ "$USE_HTTP" = true ] ; then repoUrl=${repoUrl/https/http} fi echo "${repoUrl}" } # get latest version from remote get_remote_version() { curl -sLk "https://arthas.aliyun.com/api/latest_version" } # check version greater version_gt() { local remote_version=$1 local arthas_local_version=$2 [[ "$remote_version" > "$arthas_local_version" ]] && return 0 || return 1 } # update arthas if necessary update_if_necessary() { local update_version=$1 if [ ! -d "${ARTHAS_LIB_DIR}/${update_version}" ]; then echo "updating version ${update_version} ..." local temp_target_lib_dir="$TMP_DIR/temp_${update_version}_$$" local temp_target_lib_zip="${temp_target_lib_dir}/arthas-${update_version}-bin.zip" local target_lib_dir="${ARTHAS_LIB_DIR}/${update_version}/arthas" # clean rm -rf "${temp_target_lib_dir}" rm -rf "${target_lib_dir}" mkdir -p "${temp_target_lib_dir}" \ || exit_on_err 1 "create ${temp_target_lib_dir} fail." # download current arthas version local downloadUrl="${REMOTE_DOWNLOAD_URL//PLACEHOLDER_REPO/$(get_repo_url)}" downloadUrl="${downloadUrl//PLACEHOLDER_VERSION/${update_version}}" echo "Download arthas from: ${downloadUrl}" curl \ -#Lk \ --connect-timeout ${SO_TIMEOUT} \ -o "${temp_target_lib_zip}" \ "${downloadUrl}" \ || return 1 # unzip arthas lib if ! (unzip "${temp_target_lib_zip}" -d "${temp_target_lib_dir}") ; then rm -rf "${temp_target_lib_dir}" "${ARTHAS_LIB_DIR}/${update_version}" return 1 fi mkdir -p "${ARTHAS_LIB_DIR}/${update_version}" # rename mv "${temp_target_lib_dir}" "${target_lib_dir}" || return 1 # print success echo "update completed." fi } # jps command may crash, so need to check it check_jps() { "${JAVA_HOME}/bin/jps" > /dev/null 2>&1 local exit_code=$? if [ $exit_code -ne 0 ]; then echo "jps command failed with exit code ${exit_code}" >&2 fi return $exit_code } call_jps() { check_jps local exit_code=$? if [[ "$exit_code" -eq 0 ]]; then # jps command is ok local jps_command=("${JAVA_HOME}/bin/jps" "-l") if [ "${VERBOSE}" = true ] ; then jps_command=("${JAVA_HOME}/bin/jps" "-l" "-v") fi local jps_output=$("${jps_command[@]}") echo "$jps_output" else # jps command failed, use ps and grep ps_output=$(ps aux | grep java | grep -v grep | awk '{print $2" "$11}') echo "$ps_output" fi } # the usage usage() { echo " Usage: $0 [-h] [--target-ip ] [--telnet-port ] [--http-port ] [--session-timeout ] [--arthas-home ] [--tunnel-server ] [--agent-id ] [--stat-url ] [--app-name ] [--username ] [--password ] [--disabled-commands ] [--use-version ] [--repo-mirror ] [--versions] [--use-http] [--attach-only] [-c ] [-f ] [-v] [pid] Options and Arguments: -h,--help Print usage --target-ip The target jvm listen ip, default 127.0.0.1 --telnet-port The target jvm listen telnet port, default 3658 --http-port The target jvm listen http port, default 8563 --session-timeout The session timeout seconds, default 1800 (30min) --arthas-home The arthas home --use-version Use special version arthas --repo-mirror Use special remote repository mirror, value is center/aliyun or http repo url. --versions List local and remote arthas versions --use-http Enforce use http to download, default use https --attach-only Attach target process only, do not connect --debug-attach Debug attach agent --tunnel-server Remote tunnel server url --agent-id Special agent id --app-name Special app name --username Special username --password Special password --disabled-commands Disable special commands --select select target process by classname or JARfilename -c,--command Command to execute, multiple commands separated by ; -f,--batch-file The batch file to execute --height arthas-client terminal height --width arthas-client terminal width -v,--verbose Verbose, print debug info. Target pid EXAMPLES: ./as.sh ./as.sh --telnet-port 9999 --http-port -1 ./as.sh --username admin --password ./as.sh --tunnel-server 'ws://192.168.10.11:7777/ws' --app-name demoapp ./as.sh --tunnel-server 'ws://192.168.10.11:7777/ws' --agent-id bvDOe8XbTM2pQWjF4cfw ./as.sh --stat-url 'http://192.168.10.11:8080/api/stat' ./as.sh -c 'sysprop; thread' ./as.sh -f batch.as ./as.sh --use-version 4.0.0 ./as.sh --session-timeout 3600 ./as.sh --attach-only ./as.sh --disabled-commands stop,dump ./as.sh --select math-game ./as.sh --repo-mirror aliyun --use-http WIKI: https://arthas.aliyun.com/doc Here is the list of possible java process(es) to attatch: " call_jps | grep -v sun.tools.jps.Jps } # list arthas versions list_versions() { echo "Arthas versions under ${ARTHAS_LIB_DIR}:" ls -1 "${ARTHAS_LIB_DIR}" } # find the process tcp listen at the port # $1 : port number find_listen_port_process() { if [ -x "$(command -v lsof)" ]; then echo $(lsof -t -s TCP:LISTEN -i TCP:$1) fi } getTargetIPOrDefault() { local targetIP=${DEFAULT_TARGET_IP} if [ "${TARGET_IP}" ]; then targetIP=${TARGET_IP} fi echo $targetIP } getTelnetPortOrDefault() { local telnetPort=${DEFAULT_TELNET_PORT} if [ "${TELNET_PORT}" ]; then telnetPort=${TELNET_PORT} fi echo $telnetPort } getHttpPortOrDefault() { local httpPort=${DEFAULT_HTTP_PORT} if [ "${HTTP_PORT}" ]; then httpPort=${HTTP_PORT} fi echo $httpPort } # Status from com.taobao.arthas.client.TelnetConsole # Execute commands timeout STATUS_EXEC_TIMEOUT=100 # Execute commands error STATUS_EXEC_ERROR=101 # find the process pid of target telnet port # maybe another user start an arthas server at the same port, but invisible for current user find_listen_port_process_by_client() { local arthas_lib_dir="${ARTHAS_HOME}" # http://www.inonit.com/cygwin/faq/ if [ "${OS_TYPE}" = "Cygwin" ]; then arthas_lib_dir=`cygpath -wp "$arthas_lib_dir"` fi "${JAVA_HOME}/bin/java" ${ARTHAS_OPTS} ${JVM_OPTS} \ -jar "${arthas_lib_dir}/arthas-client.jar" \ $(getTargetIPOrDefault) \ $(getTelnetPortOrDefault) \ -c "session" \ --execution-timeout 2000 \ 2>&1 # return java process exit status code ! return $? } parse_arguments() { POSITIONAL=() while [[ $# -gt 0 ]] do key="$1" case $key in -h|--help) usage exit 0 ;; --versions) list_versions exit 0 ;; --target-ip) TARGET_IP="$2" shift # past argument shift # past value ;; --telnet-port) TELNET_PORT="$2" shift # past argument shift # past value ;; --http-port) HTTP_PORT="$2" shift # past argument shift # past value ;; --session-timeout) SESSION_TIMEOUT="$2" shift # past argument shift # past value ;; --arthas-home) ARTHAS_HOME="$2" shift # past argument shift # past value ;; --use-version) USE_VERSION="$2" shift # past argument shift # past value ;; --repo-mirror) REPO_MIRROR="$2" shift # past argument shift # past value ;; -c|--command) COMMAND="$2" BATCH_MODE=true shift # past argument shift # past value ;; -f|--batch-file) BATCH_FILE="$2" BATCH_MODE=true shift # past argument shift # past value ;; --tunnel-server) TUNNEL_SERVER="$2" shift # past argument shift # past value ;; --agent-id) AGENT_ID="$2" shift # past argument shift # past value ;; --stat-url) STAT_URL="$2" shift # past argument shift # past value ;; --app-name) APP_NAME="$2" shift # past argument shift # past value ;; --username) USERNAME="$2" shift # past argument shift # past value ;; --password) PASSWORD="$2" shift # past argument shift # past value ;; --disabled-commands) DISABLED_COMMANDS="$2" shift # past argument shift # past value ;; --use-http) USE_HTTP=true shift # past argument ;; --attach-only) ATTACH_ONLY=true shift # past argument ;; --debug-attach) DEBUG_ATTACH=true if [ -z "$JPDA_TRANSPORT" ]; then JPDA_TRANSPORT="dt_socket" fi if [ -z "$JPDA_ADDRESS" ]; then JPDA_ADDRESS="8888" fi if [ -z "$JPDA_SUSPEND" ]; then JPDA_SUSPEND="y" fi if [ -z "$JPDA_OPTS" ]; then JPDA_OPTS="-agentlib:jdwp=transport=$JPDA_TRANSPORT,address=$JPDA_ADDRESS,server=y,suspend=$JPDA_SUSPEND" fi ARTHAS_OPTS="$JPDA_OPTS $ARTHAS_OPTS" shift # past argument ;; --height) HEIGHT="$2" shift # past argument shift # past value ;; --width) WIDTH="$2" shift # past argument shift # past value ;; --select) SELECT="$2" shift # past argument shift # past value ;; -v|--verbose) VERBOSE=true shift # past argument ;; --default) DEFAULT=YES shift # past argument ;; *) # unknown option POSITIONAL+=("$1") # save it in an array for later shift # past argument ;; esac done set -- "${POSITIONAL[@]}" # restore positional parameters if [[ -n $1 ]]; then # parse pid TARGET_PID=$(echo ${1}|awk -F "@" '{print $1}'); local targetIp=$(echo ${1}|awk -F "@|:" '{print $2}'); [[ "$targetIp" ]] && TARGET_IP=$targetIp local telnetPort=$(echo ${1}|awk -F ":" '{print $2}'); [[ "$telnetPort" ]] && TELNET_PORT=$telnetPort local httpPort=$(echo ${1}|awk -F ":" '{print $3}'); [[ "$httpPort" ]] && HTTP_PORT=$httpPort fi # check telnet port/http port local telnetPortPid local httpPortPid local telnetPortOrDefault=$(getTelnetPortOrDefault) local httpPortOrDefault=$(getHttpPortOrDefault) if [[ $telnetPortOrDefault > 0 ]]; then telnetPortPid=$(find_listen_port_process $telnetPortOrDefault) if [ $telnetPortPid ]; then echo "[INFO] Process $telnetPortPid already using port $telnetPortOrDefault" fi fi if [[ $httpPortOrDefault > 0 ]]; then httpPortPid=$(find_listen_port_process $httpPortOrDefault) if [ $telnetPortPid ]; then echo "[INFO] Process $httpPortPid already using port $httpPortOrDefault" fi fi if [ -z ${REPO_MIRROR} ]; then REPO_MIRROR="center" # if timezone is +0800, set REPO_MIRROR to aliyun if [[ -x "$(command -v date)" ]] && [[ $(date +%z) == "+0800" ]]; then REPO_MIRROR="aliyun" fi fi # try to find target pid by --select option if [ -z ${TARGET_PID} ] && [ ${SELECT} ]; then local IFS=$'\n' CANDIDATES=($(call_jps | grep -v sun.tools.jps.Jps | grep "${SELECT}" | awk '{print $0}')) if [ ${#CANDIDATES[@]} -eq 1 ]; then TARGET_PID=`echo ${CANDIDATES[0]} | cut -d ' ' -f 1` fi fi # check pid if [ -z ${TARGET_PID} ]; then # interactive mode local IFS=$'\n' CANDIDATES=($(call_jps | grep -v sun.tools.jps.Jps | awk '{print $0}')) if [ ${#CANDIDATES[@]} -eq 0 ]; then echo "Error: no available java process to attach." return 1 fi echo "Found existing java process, please choose one and input the serial number of the process, eg : 1. Then hit ENTER." index=0 suggest=1 # auto select tomcat/pandora-boot process for process in "${CANDIDATES[@]}"; do index=$(($index+1)) if [ $(echo ${process} | grep -c org.apache.catalina.startup.Bootstrap) -eq 1 ] \ || [ $(echo ${process} | grep -c com.taobao.pandora.boot.loader.SarLauncher) -eq 1 ] then suggest=${index} break fi done index=0 for process in "${CANDIDATES[@]}"; do index=$(($index+1)) if [ ${index} -eq ${suggest} ]; then echo "* [$index]: ${process}" else echo " [$index]: ${process}" fi done read choice if [ -z ${choice} ]; then choice=${suggest} fi TARGET_PID=`echo ${CANDIDATES[$(($choice-1))]} | cut -d ' ' -f 1` # check the process already using telnet port if equals to target pid if [[ ($telnetPortPid) && ($TARGET_PID != $telnetPortPid) ]]; then print_telnet_port_pid_error exit 1 fi if [[ ($httpPortPid) && ($TARGET_PID != $httpPortPid) ]]; then echo "Target process $TARGET_PID is not the process using port $(getHttpPortOrDefault), you will connect to an unexpected process." echo "1. Try to restart as.sh, select process $httpPortPid, shutdown it first with running the 'stop' command." echo "2. Try to use different http port, for example: as.sh --telnet-port 9998 --http-port 9999" exit 1 fi elif [ -z ${TARGET_PID} ]; then # batch mode is enabled, no interactive process selection. echo "Illegal arguments, the is required." 1>&2 return 1 fi } # attach arthas to target jvm attach_jvm() { local arthas_lib_dir=$1 # http://www.inonit.com/cygwin/faq/ if [ "${OS_TYPE}" = "Cygwin" ]; then arthas_lib_dir=`cygpath -wp "$arthas_lib_dir"` fi echo "Attaching to ${TARGET_PID} using version ${1}..." local java_command=("${JAVA_HOME}"/bin/java) if [ "${BOOT_CLASSPATH}" ]; then java_command+=("${BOOT_CLASSPATH}") fi local tempArgs=() if [ "${TUNNEL_SERVER}" ]; then tempArgs+=("-tunnel-server") tempArgs+=("${TUNNEL_SERVER}") fi if [ "${AGENT_ID}" ]; then tempArgs+=("-agent-id") tempArgs+=("${AGENT_ID}") fi if [ "${STAT_URL}" ]; then tempArgs+=("-stat-url") tempArgs+=("${STAT_URL}") fi if [ "${APP_NAME}" ]; then tempArgs+=("-app-name") tempArgs+=("${APP_NAME}") fi if [ "${USERNAME}" ]; then tempArgs+=("-username") tempArgs+=("${USERNAME}") fi if [ "${PASSWORD}" ]; then tempArgs+=("-password") tempArgs+=("${PASSWORD}") fi if [ "${DISABLED_COMMANDS}" ]; then tempArgs+=("-disabled-commands") tempArgs+=("${DISABLED_COMMANDS}") fi if [ "${TARGET_IP}" ]; then tempArgs+=("-target-ip") tempArgs+=("${TARGET_IP}") fi if [ "${TELNET_PORT}" ]; then tempArgs+=("-telnet-port") tempArgs+=("${TELNET_PORT}") fi if [ "${HTTP_PORT}" ]; then tempArgs+=("-http-port") tempArgs+=("${HTTP_PORT}") fi if [ "${SESSION_TIMEOUT}" ]; then tempArgs+=("-session-timeout") tempArgs+=("${SESSION_TIMEOUT}") fi "${java_command[@]}" \ ${ARTHAS_OPTS} ${JVM_OPTS} \ -jar "${arthas_lib_dir}/arthas-core.jar" \ -pid ${TARGET_PID} \ "${tempArgs[@]}" \ -core "${arthas_lib_dir}/arthas-core.jar" \ -agent "${arthas_lib_dir}/arthas-agent.jar" } sanity_check() { # only Linux/Mac support ps to find process, Cygwin/MinGw may fail. if ([ "${OS_TYPE}" != "Linux" ] && [ "${OS_TYPE}" != "Mac" ]); then return fi # 0 check whether the pid exist local pid=$(ps -p ${TARGET_PID} -o pid= 2>&1 ) # get ps command exit code local exitCode="$(ps -p ${TARGET_PID} -o pid= > /dev/null 2>&1; echo $?)" # If ps exist code not 0, the TARGET_PID process maybe not exist or ps do not support -p options. if [ "${exitCode}" != "0" ]; then # if ps do not support -p or -o , ${pid} will be error message, just return if [ -n "${pid}" ]; then return fi fi if [ -z ${pid} ]; then exit_on_err 1 "The target pid (${TARGET_PID}) does not exist!" fi # 1 check the current user matches the process owner local current_user=$(id -u -n) # the last '=' after 'user' eliminates the column header local target_user=$(ps -p "${TARGET_PID}" -o user=) if [ "$current_user" != "$target_user" ]; then echo "The current user ($current_user) does not match with the owner of process ${TARGET_PID} ($target_user)." echo "To solve this, choose one of the following command:" echo " 1) sudo su $target_user && ./as.sh" echo " 2) sudo -u $target_user -EH ./as.sh" exit_on_err 1 fi } port_pid_check() { if [[ $(getTelnetPortOrDefault) > 0 ]]; then local telnet_output local find_process_status # declare local var before var=$() telnet_output=$(find_listen_port_process_by_client) find_process_status=$? #echo "find_process_status: $find_process_status" #echo "telnet_output: $telnet_output" #check return code if [[ $find_process_status -eq $STATUS_EXEC_TIMEOUT ]]; then print_telnet_port_used_error "detection timeout" exit 1 elif [[ $find_process_status -eq $STATUS_EXEC_ERROR ]]; then print_telnet_port_used_error "detection error" exit 1 fi if [[ -n $telnet_output ]]; then # check JAVA_PID telnetPortPid=$(echo "$telnet_output" | grep JAVA_PID | awk '{ print $2 }') #echo "telnetPortPid: $telnetPortPid" # check the process already using telnet port if equals to target pid if [[ -n $telnetPortPid && ($TARGET_PID != $telnetPortPid) ]]; then print_telnet_port_pid_error exit 1 fi fi fi } print_telnet_port_pid_error() { echo "[ERROR] The telnet port $(getTelnetPortOrDefault) is used by process $telnetPortPid instead of target process $TARGET_PID, you will connect to an unexpected process." echo "[ERROR] 1. Try to restart as.sh, select process $telnetPortPid, shutdown it first with running the 'stop' command." echo "[ERROR] 2. Try to stop the existing arthas instance: java -jar arthas-client.jar 127.0.0.1 $(getTelnetPortOrDefault) -c \"stop\"" echo "[ERROR] 3. Try to use different telnet port, for example: as.sh --telnet-port 9998 --http-port -1" } print_telnet_port_used_error() { local error_msg=$1 echo "[ERROR] The telnet port $(getTelnetPortOrDefault) is used, but process $error_msg, you will connect to an unexpected process." echo "[ERROR] Try to use different telnet port, for example: as.sh --telnet-port 9998 --http-port -1" } # active console # $1 : arthas_lib_dir active_console() { local arthas_lib_dir=$1 # http://www.inonit.com/cygwin/faq/ if [ "${OS_TYPE}" = "Cygwin" ]; then arthas_lib_dir=`cygpath -wp $arthas_lib_dir` fi if [ "${BATCH_MODE}" = "true" ]; then local tempArgs=() if [ "${HEIGHT}" ]; then tempArgs+=("--height") tempArgs+=("${HEIGHT}") fi if [ "${WIDTH}" ]; then tempArgs+=("--width") tempArgs+=("${WIDTH}") fi if [ "${COMMAND}" ] ; then "${JAVA_HOME}/bin/java" ${ARTHAS_OPTS} ${JVM_OPTS} \ -jar "${arthas_lib_dir}/arthas-client.jar" \ $(getTargetIPOrDefault) \ $(getTelnetPortOrDefault) \ "${tempArgs[@]}" \ -c "${COMMAND}" fi if [ "${BATCH_FILE}" ] ; then "${JAVA_HOME}/bin/java" ${ARTHAS_OPTS} ${JVM_OPTS} \ -jar "${arthas_lib_dir}/arthas-client.jar" \ $(getTargetIPOrDefault) \ $(getTelnetPortOrDefault) \ "${tempArgs[@]}" \ -f ${BATCH_FILE} fi elif type telnet 2>&1 >> /dev/null; then # use telnet if [[ $(command -v telnet) == *"system32"* ]] ; then # Windows/system32/telnet.exe can not run in Cygwin/MinGw echo "It seems that current bash is under Windows. $(command -v telnet) can not run under bash." echo "Please start cmd.exe from Windows start menu, and then run telnet $(getTargetIPOrDefault) $(getTelnetPortOrDefault) to connect to target process." echo "Or visit http://127.0.0.1:$(getHttpPortOrDefault) to connect to target process." return 1 fi echo "telnet connecting to arthas server... current timestamp is `date +%s`" telnet $(getTargetIPOrDefault) $(getTelnetPortOrDefault) else echo "'telnet' is required." 1>&2 return 1 fi } # the main main() { echo "Arthas script version: $ARTHAS_SCRIPT_VERSION" check_permission reset_for_env parse_arguments "${@}" \ || exit_on_err 1 "$(usage)" # try to find arthas home from --use-version if [[ (-z "${ARTHAS_HOME}") && (! -z "${USE_VERSION}") ]]; then if [[ ! -d "${ARTHAS_LIB_DIR}/${USE_VERSION}/arthas" ]] ; then update_if_necessary "${USE_VERSION}" || echo "update fail, ignore this update." 1>&2 fi ARTHAS_HOME="${ARTHAS_LIB_DIR}/${USE_VERSION}/arthas" fi # try to set arthas home from as.sh directory if [ -z "${ARTHAS_HOME}" ] ; then [[ -a "${DIR}/arthas-core.jar" ]] \ && [[ -a "${DIR}/arthas-agent.jar" ]] \ && [[ -a "${DIR}/arthas-spy.jar" ]] \ && ARTHAS_HOME="${DIR}" fi # try to find arthas under ~/.arthas/lib if [ -z "${ARTHAS_HOME}" ] ; then local remote_version=$(get_remote_version) local arthas_local_version=$(get_local_version) if $(version_gt $remote_version $arthas_local_version) ; then update_if_necessary "${remote_version}" || echo "update fail, ignore this update." 1>&2 fi local arthas_local_version=$(get_local_version) ARTHAS_HOME="${ARTHAS_LIB_DIR}/${arthas_local_version}/arthas" fi echo "Arthas home: ${ARTHAS_HOME}" if [ ! -d "${ARTHAS_HOME}" ] ; then exit_on_err 1 "Arthas home is not a directory, please delete it and retry." fi sanity_check port_pid_check echo "Calculating attach execution time..." time (attach_jvm "${ARTHAS_HOME}" || exit 1) if [ $? -ne 0 ]; then exit_on_err 1 "attach to target jvm (${TARGET_PID}) failed, check ${HOME}/logs/arthas/arthas.log or stderr of target jvm for any exceptions." fi echo "Attach success." if [ ${ATTACH_ONLY} = false ]; then active_console "${ARTHAS_HOME}" fi } main "${@}"