Fix as.sh Java home selection and validation (#3273)

This commit is contained in:
hengyunabc
2026-09-29 15:46:50 +08:00
committed by GitHub
parent d6e847e8b0
commit dbafd23644
4 changed files with 344 additions and 67 deletions
+83 -60
View File
@@ -229,6 +229,64 @@ check_permission()
}
# 验证候选 Java,成功后才更新 JAVA_HOME 和对应的启动参数。
try_java_home()
{
local java_home="$1"
[ -n "$java_home" ] || return 1
if [ ! -x "$java_home/bin/java" ]; then
echo "[WARN] Skip JAVA_HOME '$java_home': bin/java is missing or not executable." >&2
return 1
fi
local version_output
if ! version_output=$("$java_home/bin/java" -version 2>&1); then
echo "[WARN] Skip JAVA_HOME '$java_home': java -version failed." >&2
return 1
fi
local java_version= line version
local version_pattern='version[[:space:]]+"([^"]+)"'
while IFS= read -r line; do
if [[ "$line" =~ $version_pattern ]]; then
version=${BASH_REMATCH[1]}
[[ "$version" == 1.* ]] && version=${version#1.}
if [[ "$version" =~ ^([0-9]+)($|[-._+]) ]]; then
java_version=$((10#${BASH_REMATCH[1]}))
break
fi
fi
done <<< "$version_output"
if [ -z "$java_version" ] || [ "$java_version" -lt 1 ]; then
echo "[WARN] Skip JAVA_HOME '$java_home': unrecognized Java version." >&2
return 1
fi
java_home=$(rreadlink "$java_home") || return 1
local boot_classpath= candidate
# 仅 Java 8 及以下需要 tools.jar;保留从 JRE 子目录向父 JDK 查找的行为。
if [ "$java_version" -lt 9 ]; then
local java_homes=("$java_home" "$java_home/.." "$java_home/../..")
for candidate in "${java_homes[@]}"; do
if [ -x "$candidate/bin/java" ] && [ -f "$candidate/lib/tools.jar" ]; then
java_home=$(rreadlink "$candidate") || return 1
boot_classpath="-Xbootclasspath/a:$java_home/lib/tools.jar"
break
fi
done
if [ -z "$boot_classpath" ]; then
echo "[WARN] Skip JAVA_HOME '$java_home': Java $java_version requires a JDK with lib/tools.jar." >&2
return 1
fi
fi
JAVA_HOME=$java_home
BOOT_CLASSPATH=$boot_classpath
return 0
}
# reset arthas work environment
# reset some options for env
reset_for_env()
@@ -239,73 +297,38 @@ reset_for_env()
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
local configured_java_home="${JAVA_HOME}"
JAVA_HOME=
BOOT_CLASSPATH=
if [[ (-z "${JAVA_HOME}") && ( -e "/usr/libexec/java_home") ]]; then
# for mac
JAVA_HOME=`/usr/libexec/java_home`
# 有效的显式配置优先;只有当前来源不可用时才查找下一个来源。
if ! try_java_home "$configured_java_home" && [ -d /opt/taobao/java ]; then
try_java_home /opt/taobao/java
fi
if [ -z "${JAVA_HOME}" ] && [ -x /usr/libexec/java_home ]; then
local mac_java_home
mac_java_home=$(/usr/libexec/java_home 2>/dev/null) && try_java_home "$mac_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')
local java_command_path
java_command_path=$(type -p java)
if [ -n "$java_command_path" ] && java_command_path=$(rreadlink "$java_command_path"); then
case "$java_command_path" in
*/bin/java) try_java_home "${java_command_path%/bin/java}" ;;
esac
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
if [ -z "${JAVA_HOME}" ]; then
local candidate
while IFS= read -r candidate; do
try_java_home "$candidate" && break
done < <(ps aux | awk '$11 ~ /\/bin\/java$/ { sub(/\/bin\/java$/, "", $11); print $11 }')
fi
[ -n "${JAVA_HOME}" ] || exit_on_err 1 "Can not find a usable JAVA_HOME; see the candidate errors above."
echo "[INFO] JAVA_HOME: ${JAVA_HOME}"
# reset CHARSET for alibaba opts, we use GBK
@@ -40,6 +40,10 @@ public class AsScriptTest {
private Path javaCalls;
private Path curlCalls;
private Path download;
private Path javaHome;
private Path platformJavaHome;
private Path macJavaHome;
private Path discoveryCalls;
private Map<String, String> environment;
@Before
@@ -50,6 +54,9 @@ public class AsScriptTest {
javaCalls = temporaryFolder.newFile("java-calls").toPath();
curlCalls = temporaryFolder.newFile("curl-calls").toPath();
download = temporaryFolder.newFile("download.zip").toPath();
discoveryCalls = temporaryFolder.newFile("discovery-calls").toPath();
platformJavaHome = temporaryFolder.getRoot().toPath().resolve("platform-jdk");
macJavaHome = temporaryFolder.getRoot().toPath().resolve("mac-java-home");
createArchive(true);
// 只暴露启动器所需命令,避免宿主机的 telnet、lsof 和 Java 进程影响结果。
@@ -61,7 +68,16 @@ public class AsScriptTest {
Files.createSymbolicLink(commands.resolve(command), Paths.get(new String(output, StandardCharsets.UTF_8).trim()));
}
writeExecutable(commands.resolve("telnet"), "exit 0\n");
writeExecutable(commands.resolve("ps"), "case \"$*\" in *user=*) id -u -n;; *) echo 1234;; esac\n");
writeExecutable(commands.resolve("ps"),
"case \"$*\" in\n"
+ " *user=*) id -u -n;;\n"
+ " aux)\n"
+ " printf 'ps\\n' >> \"$AS_TEST_DISCOVERY_CALLS\"\n"
+ " if [[ -n $AS_TEST_PROCESS_JAVA_HOME ]]; then\n"
+ " printf 'user 1234 0 0 0 0 ? Sl 00:00 0:00 %s/bin/java example.Main\\n' \"$AS_TEST_PROCESS_JAVA_HOME\"\n"
+ " fi;;\n"
+ " *) echo 1234;;\n"
+ "esac\n");
writeExecutable(commands.resolve("curl"),
"printf '%s\\n' \"$*\" >> \"$AS_TEST_CURL_CALLS\"\n"
+ "output=\n"
@@ -77,12 +93,7 @@ public class AsScriptTest {
+ " cp \"$AS_TEST_DOWNLOAD\" \"$output\"\n"
+ "fi\n");
Path javaHome = temporaryFolder.newFolder("jdk").toPath();
Files.createDirectory(javaHome.resolve("bin"));
writeExecutable(javaHome.resolve("bin/java"),
"if [[ $1 == -version ]]; then echo 'openjdk version \"17.0.2\"' >&2; exit 0; fi\n"
+ "printf '%s\\n' \"$*\" >> \"$AS_TEST_JAVA_CALLS\"\n");
writeExecutable(javaHome.resolve("bin/jps"), "echo '1234 example.Main'\n");
javaHome = createJavaHome(temporaryFolder.getRoot().toPath().resolve("jdk"), "17.0.2", false);
Path project = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
while (project != null && !Files.isRegularFile(project.resolve("bin/as.sh"))) {
@@ -90,6 +101,9 @@ public class AsScriptTest {
}
assertTrue("Cannot locate bin/as.sh", project != null);
String script = new String(Files.readAllBytes(project.resolve("bin/as.sh")), StandardCharsets.UTF_8);
// 隔离固定平台路径,保留完整 Java 初始化和启动流程。
script = script.replace("/opt/taobao/java", platformJavaHome.toString())
.replace("/usr/libexec/java_home", macJavaHome.toString());
int mainCall = script.lastIndexOf("\nmain \"${@}\"");
assertTrue("Cannot locate launcher entry point", mainCall >= 0);
// 显式设置 umask,避免宿主设置影响权限断言;旧版本的 /tmp 仍隔离到测试目录。
@@ -108,9 +122,172 @@ public class AsScriptTest {
environment.put("AS_TEST_DOWNLOAD", download.toString());
environment.put("AS_TEST_JAVA_CALLS", javaCalls.toString());
environment.put("AS_TEST_CURL_CALLS", curlCalls.toString());
environment.put("AS_TEST_DISCOVERY_CALLS", discoveryCalls.toString());
environment.put("AS_TEST_TMP_DIR", temporaryFolder.newFolder("downloads").getAbsolutePath());
}
@Test
public void invalidExplicitJavaHomeFallsBackToPlatform() throws Exception {
Path fallback = createJavaHome(platformJavaHome, "11.0.27.26-AJDK", false);
Path missing = temporaryFolder.getRoot().toPath().resolve("missing-jdk");
Path empty = temporaryFolder.newFolder("empty-jdk").toPath();
Path nonExecutable = createJavaHome(temporaryFolder.getRoot().toPath().resolve("non-executable"), "17", false);
Files.setPosixFilePermissions(nonExecutable.resolve("bin/java"), PosixFilePermissions.fromString("rw-r--r--"));
for (Path invalid : Arrays.asList(missing, empty, nonExecutable)) {
environment.put("JAVA_HOME", invalid.toString());
String output = runHelp(0);
assertJavaHome(output, fallback);
assertTrue(output, output.contains("bin/java is missing or not executable"));
}
assertEquals("", read(discoveryCalls));
}
@Test
public void validExplicitJavaHomeAvoidsFallbackDiscovery() throws Exception {
Path fallback = createJavaHome(platformJavaHome, "11", false);
useMacJava(fallback);
Files.createSymbolicLink(commands.resolve("java"), fallback.resolve("bin/java"));
environment.put("AS_TEST_PROCESS_JAVA_HOME", fallback.toString());
String output = runHelp(0);
assertJavaHome(output, javaHome);
assertFalse(output, output.contains("[WARN]"));
assertEquals("", read(discoveryCalls));
}
@Test
public void platformJavaTakesPrecedenceOverMacAndPath() throws Exception {
environment.put("JAVA_HOME", "");
Path fallback = createJavaHome(platformJavaHome, "11", false);
useMacJava(javaHome);
Files.createSymbolicLink(commands.resolve("java"), javaHome.resolve("bin/java"));
assertJavaHome(runHelp(0), fallback);
assertEquals("", read(discoveryCalls));
}
@Test
public void macJavaTakesPrecedenceOverPath() throws Exception {
environment.put("JAVA_HOME", "");
useMacJava(javaHome);
Path pathJava = createJavaHome(temporaryFolder.getRoot().toPath().resolve("path-jdk"), "21", false);
Files.createSymbolicLink(commands.resolve("java"), pathJava.resolve("bin/java"));
assertJavaHome(runHelp(0), javaHome);
assertEquals("mac\n", read(discoveryCalls));
}
@Test
public void failedMacLookupFallsBackToPath() throws Exception {
environment.put("JAVA_HOME", "");
writeExecutable(macJavaHome, "exit 1\n");
Files.createSymbolicLink(commands.resolve("java"), javaHome.resolve("bin/java"));
assertJavaHome(runHelp(0), javaHome);
}
@Test
public void pathWithSpacesAndSymlinkWorksWithoutOptionalPlatformPaths() throws Exception {
environment.put("JAVA_HOME", "");
Path path = temporaryFolder.newFolder("path with spaces").toPath();
Files.createSymbolicLink(path.resolve("java"), javaHome.resolve("bin/java"));
environment.put("PATH", path + File.pathSeparator + commands);
String output = runHelp(0);
assertJavaHome(output, javaHome);
assertFalse(output, output.contains("[WARN]"));
assertEquals("", read(discoveryCalls));
}
@Test
public void processFallbackAcceptsModernJavaWithoutToolsJar() throws Exception {
environment.put("JAVA_HOME", temporaryFolder.getRoot().toPath().resolve("missing-jdk").toString());
Path processJava = createJavaHome(temporaryFolder.getRoot().toPath().resolve("process-jdk"), "11", false);
environment.put("AS_TEST_PROCESS_JAVA_HOME", processJava.toString());
assertJavaHome(runHelp(0), processJava);
assertEquals("ps\n", read(discoveryCalls));
}
@Test
public void failedVersionCommandIsRejectedEvenWithRecognizableOutput() throws Exception {
Path fallback = createJavaHome(platformJavaHome, "11", false);
writeJava(javaHome, "openjdk version \"17.0.2\"", 42);
String output = runHelp(0);
assertJavaHome(output, fallback);
assertTrue(output, output.contains("java -version failed"));
}
@Test
public void invalidVersionsFallBackInsteadOfAssumingJava8() throws Exception {
Path fallback = createJavaHome(platformJavaHome, "11", false);
for (String version : Arrays.asList("", "unrecognized vendor output", "openjdk version \"0\"",
"openjdk version \"invalid\"")) {
writeJava(javaHome, version, 0);
String output = runHelp(0);
assertJavaHome(output, fallback);
assertTrue(output, output.contains("unrecognized Java version"));
}
}
@Test
public void modernJavaVersionsDoNotNeedToolsJar() throws Exception {
for (String version : Arrays.asList("9", "11.0.27.26-AJDK", "11-ea", "17.0.15", "21.0.7", "25")) {
writeJava(javaHome, "openjdk version \"" + version + "\"\r\nOpenJDK Runtime Environment\r", 0);
assertJavaHome(runHelp(0), javaHome);
}
}
@Test
public void java8AndNestedJreUseJdkToolsJarWithSpaces() throws Exception {
Path jdk = createJavaHome(temporaryFolder.getRoot().toPath().resolve("jdk8 with spaces"), "1.8.0_402", true);
Path jre = createJavaHome(jdk.resolve("jre"), "1.8.0_402", false);
Path nestedJre = createJavaHome(jre.resolve("nested"), "1.8.0_402", false);
Path arthas = cache("4.3.5");
for (Path candidate : Arrays.asList(jdk, jre, nestedJre)) {
Files.write(javaCalls, new byte[0]);
environment.put("JAVA_HOME", candidate.toString());
environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar");
assertJavaHome(run(0, "--arthas-home", arthas.toString(), "--attach-only", "1234"), jdk);
String calls = read(javaCalls);
assertTrue(calls, calls.contains("-Xbootclasspath/a:" + jdk.resolve("lib/tools.jar")));
assertFalse(calls, calls.contains("/stale/tools.jar"));
}
}
@Test
public void java8WithoutUsableParentJdkFallsBack() throws Exception {
Path fallback = createJavaHome(platformJavaHome, "11", false);
Path parent = temporaryFolder.newFolder("jdk8-without-java").toPath();
Files.createDirectories(parent.resolve("lib"));
Files.createFile(parent.resolve("lib/tools.jar"));
Path jre = createJavaHome(parent.resolve("jre"), "1.8.0_402", false);
environment.put("JAVA_HOME", jre.toString());
String output = runHelp(0);
assertJavaHome(output, fallback);
assertTrue(output, output.contains("requires a JDK with lib/tools.jar"));
}
@Test
public void modernJavaClearsInheritedBootClasspathBeforeAttach() throws Exception {
environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar");
Path arthas = cache("4.3.5");
assertJavaHome(run(0, "--arthas-home", arthas.toString(), "--attach-only", "1234"), javaHome);
assertFalse(read(javaCalls), read(javaCalls).contains("-Xbootclasspath"));
}
@Test
public void inheritedBootClasspathCannotMakeInvalidJavaUsable() throws Exception {
environment.put("JAVA_HOME", temporaryFolder.getRoot().toPath().resolve("missing-jdk").toString());
environment.put("BOOT_CLASSPATH", "-Xbootclasspath/a:/stale/tools.jar");
String output = runHelp(1);
assertTrue(output, output.contains("Can not find a usable JAVA_HOME"));
assertFalse(output, output.contains("tools.jar was not found, so arthas could not be launched!"));
}
@Test
public void unknownVersionReportsSelectionFailureBeforeDownloadOrAttach() throws Exception {
writeJava(javaHome, "unrecognized vendor output", 0);
String output = runHelp(1);
assertTrue(output, output.contains("unrecognized Java version"));
assertTrue(output, output.contains("Can not find a usable JAVA_HOME"));
assertFalse(output, output.contains("tools.jar was not found, so arthas could not be launched!"));
}
@Test
public void choosesNewestNumericCachedVersion() throws Exception {
cache("4.3.9");
@@ -325,6 +502,10 @@ public class AsScriptTest {
command.addAll(Arrays.asList(arguments));
Path output = temporaryFolder.newFile().toPath();
ProcessBuilder builder = new ProcessBuilder(command).directory(temporaryFolder.getRoot());
for (String variable : Arrays.asList("BOOT_CLASSPATH", "BASH_ENV", "ENV", "JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS",
"JDK_JAVA_OPTIONS")) {
builder.environment().remove(variable);
}
builder.environment().putAll(environment);
builder.redirectErrorStream(true).redirectOutput(output.toFile());
Process process = builder.start();
@@ -337,6 +518,47 @@ public class AsScriptTest {
return text;
}
private String runHelp(int expectedExit) throws Exception {
String output = run(expectedExit, "--help");
assertEquals("Java selection must not invoke download commands", "", read(curlCalls));
assertEquals("Java selection must not invoke attach or client commands", "", read(javaCalls));
if (expectedExit == 0) {
assertTrue(output, output.contains("Usage:"));
}
return output;
}
private void assertJavaHome(String output, Path expected) throws IOException {
assertTrue(output, output.contains("[INFO] JAVA_HOME: " + expected.toRealPath() + "\n"));
}
private Path createJavaHome(Path home, String version, boolean tools) throws IOException {
Files.createDirectories(home.resolve("bin"));
writeJava(home, "openjdk version \"" + version + "\"", 0);
writeExecutable(home.resolve("bin/jps"), "echo '1234 example.Main'\n");
if (tools) {
Files.createDirectories(home.resolve("lib"));
Files.createFile(home.resolve("lib/tools.jar"));
}
return home.toRealPath();
}
private void writeJava(Path home, String versionOutput, int exitCode) throws IOException {
writeExecutable(home.resolve("bin/java"),
"if [[ $1 == -version ]]; then\n"
+ " printf '%s\\n' '" + versionOutput.replace("'", "'\\''") + "' >&2\n"
+ " exit " + exitCode + "\n"
+ "fi\n"
+ "printf '%s\\n' \"$*\" >> \"$AS_TEST_JAVA_CALLS\"\n");
}
private void useMacJava(Path home) throws IOException {
environment.put("AS_TEST_MAC_JAVA_HOME", home.toString());
writeExecutable(macJavaHome,
"printf 'mac\\n' >> \"$AS_TEST_DISCOVERY_CALLS\"\n"
+ "printf '%s\\n' \"$AS_TEST_MAC_JAVA_HOME\"\n");
}
private void assertHome(String output, Path home) {
assertTrue(output, output.contains("Arthas home: " + home + "\n"));
}
+16
View File
@@ -37,6 +37,22 @@ curl -L https://arthas.aliyun.com/install.sh | sh
也可以执行`./as.sh -h`来获取更多参数信息。
`as.sh` 按以下顺序查找可用的 Java,选中后停止查找:
1. 环境变量 `JAVA_HOME`。
2. `/opt/taobao/java`(目录存在时)。
3. macOS 的 `/usr/libexec/java_home` 返回的目录。
4. `PATH` 中的 `java`,解析软链接后确定安装目录。
5. 运行中 Java 进程的安装目录(需要进程列表中包含完整的 `/bin/java` 路径)。
每个候选目录都需要包含可执行的 `bin/java`,并且 `java -version` 执行成功、版本可以识别。Java 8 及以下还需要 `lib/tools.jar`;如果 `JAVA_HOME` 指向 JRE 子目录,脚本会向上查找最多两级,选择包含 `tools.jar` 和可执行 `bin/java` 的 JDK。Java 9 及以上不需要 `tools.jar`。
有效的 `JAVA_HOME` 始终优先。已配置的目录失效时,脚本会输出原因并继续查找;全部候选不可用时,会在下载或 attach 之前退出。可以为单次启动指定 Java:
```bash
JAVA_HOME="/path/to/jdk" ./as.sh
```
## 全量安装
最新版本,点击下载:[![](https://img.shields.io/maven-central/v/com.taobao.arthas/arthas-packaging.svg?style=flat-square "Arthas")](https://arthas.aliyun.com/download/latest_version?mirror=aliyun)
+16
View File
@@ -29,6 +29,22 @@ The command above will download the bootstrap script `as.sh` to the current dire
You can enter its interactive interface by executing `as.sh`, or execute `as.sh -h` for more help information.
`as.sh` searches for a usable Java installation in the following order and stops at the first valid candidate:
1. The `JAVA_HOME` environment variable.
2. `/opt/taobao/java`, if the directory exists.
3. The directory returned by `/usr/libexec/java_home` on macOS.
4. The `java` command on `PATH`, resolving symbolic links to locate its installation directory.
5. The installation directories of running Java processes, when the process list contains their full `/bin/java` paths.
Each candidate must contain an executable `bin/java`, and `java -version` must succeed and report a recognizable version. Java 8 and earlier also require `lib/tools.jar`. If `JAVA_HOME` points to a JRE subdirectory, the script searches up to two parent directories for a JDK containing both `tools.jar` and an executable `bin/java`. Java 9 and later do not require `tools.jar`.
A valid `JAVA_HOME` always takes precedence. If a configured installation is invalid, the script reports the reason and continues searching. If no candidate is usable, it exits before downloading or attaching. To select Java for a single invocation:
```bash
JAVA_HOME="/path/to/jdk" ./as.sh
```
## Full installation
Latest Version, Click To Download: [![](https://img.shields.io/maven-central/v/com.taobao.arthas/arthas-packaging.svg?style=flat-square "Arthas")](https://arthas.aliyun.com/download/latest_version)