从"装不上 JVM"到"零权限诊断":Arthas Diag开源软件适配鸿蒙 PC 适配实战全记录

欢迎加入开源鸿蒙 PC 社区:https://harmonypc.csdn.net/

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_arthas

本文记录一个 JVM 诊断工具在 HarmonyOS PC 上的完整适配历程:从第一版方案的彻底溃败,到架构级重构,再到真机联调中一个个错误码的排查,最后在鸿蒙 PC 的 openEuler 融合开发环境里跑通"本机 JVM + 原生 GUI"的闭环。所有命令、错误码、截图均来自真实操作记录,希望能给鸿蒙 PC 软件适配的同学一份可复用的参考。
在这里插入图片描述

一、缘起:为什么要做这个工具

Arthas 是阿里巴巴开源的 Java 诊断利器,日常排查线上 JVM 问题几乎离不开它。但它的官方交互形态是命令行(telnet/终端),在 PC 上做诊断时,工程师往往还想要一个图形化面板:线程、内存、JVM 参数一屏尽览,点开即用。
在这里插入图片描述

我们的目标是:在 HarmonyOS PC 上做一个原生的 Arthas GUI 诊断工具。名字很朴素,叫 Arthas Diag,功能定为五大只读面板——Dashboard、Threads、JVM、SysProp、SysEnv。

应用主界面

HarmonyOS PC 应用市场 AppGallery

看起来是个"客户端壳子"的活儿,实际上这条适配之路走了三个阶段、推翻了一次架构、踩了至少六个大坑。先从最惨的部分讲起。

二、第一版方案的溃败:把 JVM 搬进鸿蒙的三条死路

适配组最初的思路非常直觉:Arthas 是 Java 写的,那就把 Java 运行时一起搬进鸿蒙 PC。具体做法是用 HNP(HarmonyOS Native Package)把完整 JDK + Arthas 打包,通过 HiShell 启动。

这个方案在纸面上成立,在真机上撞了三条硬约束,每一条都是系统级的、绕不过去的:

约束具体表现
JIT 需要 execmem 权限normal 签名的应用拿不到,HotSpot JIT 一开就崩
attach 其他 JVM 进程鸿蒙沙箱隔离,跨进程 JVMTI attach 直接被拒
HNP 二进制 noexec能装进 hdc 域,但文件系统挂了 noexec,装了也跑不起来

三条约束分别封死了运行时(JVM 起不来)、诊断机制(attach 不了目标进程)和分发方式(二进制无法执行)。也就是说,在鸿蒙 PC 的应用沙箱里,"自带一个完整 JVM 去诊断别的 JVM"这条路线在物理上就不成立。

这里有个值得所有适配同学记住的教训:

在受限平台上,"把整个运行时打包进去"往往是下策。先问一句:目标平台原生提供了什么能力?我真正需要的是运行时本身,还是运行时产出的数据?

Arthas 的本质是"采集 JVM 内部数据 + 渲染"。我们需要的其实只是数据,而不是在鸿蒙沙箱里复刻一个 JVM。

三、架构重构:HAP 只做 HTTP 客户端

想通上面这一点,新架构就自然浮出来了:让目标 JVM 自己跑 Arthas agent(它本来就在任何标准 JVM 上能跑),鸿蒙端的 HAP 退化成一个纯粹的 HTTP 客户端

在这里插入图片描述

Arthas 从早期版本就内置了 Http API:向 http://<host>:<port>/api 发一个 {"action":"exec","command":"dashboard -n 1"},返回标准 JSON。这个接口天然适合 GUI 化——命令行的一切能力(dashboardthreadjvmsyspropsysenv)都能映射成一个面板。

新方案的收益是全方位的:

  • 零特权:所有通信走 @kit.NetworkKithttp,声明 INTERNET + GET_NETWORK_INFO 两个 normal 权限即可,normal APL 签名直接过;
  • 零注入:不碰 JVMTI、不碰 execmem、不碰字节码增强,只读诊断;
  • 跨平台:目标 JVM 可以是本机的,也可以是局域网里任何一台机器——鸿蒙 PC 由此变成了整个研发网的"移动诊断终端"。

工程结构也随之极简,核心就三个文件:

entry/src/main/ets/
├── common/
│   ├── ArthasClient.ets   # HTTP 客户端(核心,约 200 行)
│   ├── DiagFormat.ets     # 数据格式化层
│   └── Theme.ets          # 暗色主题 Token
├── entryability/EntryAbility.ets
└── pages/Index.ets        # 主页面(连接栏 + 5 面板)

四、ArkTS 实现的四个关键细节

架构简单,不等于实现没有坑。这四个细节是实战中反复打磨出来的。

4.1 诊断流量必须直连:usingProxy: false

这是最隐蔽的一个坑。鸿蒙 PC 上如果系统配置了代理(公司内网环境太常见了),http.request 默认可能走代理,而诊断目标往往是内网 IP(192.168.x.x),代理根本转发不进去,表现为超时或者莫名的连接失败。所以必须显式声明直连:

const resp = await request.request(url, {
  method: http.RequestMethod.POST,
  header: headers,
  // 诊断目标是指定 JVM,必须直连,不能走系统代理
  usingProxy: false,
  extraData: JSON.stringify(payload),
  connectTimeout: 8000,
  readTimeout: timeoutMs + 5000,
  expectDataType: http.HttpDataType.STRING
});

4.2 ArkTS 严格模式:禁止索引签名

Arthas 返回的 results 数组里,每条结果的字段随命令变化(dashboardmemory/threads,threadthreadInfo…)。习惯了 TS 的同学会顺手写 obj['type'] 这种索引访问——ArkTS 严格模式直接报错。解法是用 Record<string, Object> 显式承接动态字段,再封装一个安全取值函数:

export interface ArthasResult {
  type: string;
  statusCode: number;
  /** 该条结果的完整原始对象(含所有动态字段) */
  data: Record<string, Object>;
}

export function getField(obj: Record<string, Object> | undefined,
                         key: string): Object | undefined {
  if (!obj) {
    return undefined;
  }
  const v: Object | undefined = obj[key];
  return v;
}

这套"显式接口 + Record + getField"的模式,后来成了项目里处理一切动态 JSON 的标准姿势。

4.3 Arthas 的鉴权语义:非 localhost 强制 Basic Auth

Arthas 有个容易被忽略的安全策略:localConnectionNonAuth 只对来自 127.0.0.1 的连接免鉴权;只要来源不是本机回环,就强制要求 Basic Auth。而当你用 --target-ip 0.0.0.0 把 Arthas 暴露到网卡上时,它会自动生成一个随机密码,写在自己的启动日志里(~/logs/arthas/arthas.log)。

所以客户端必须做两件事:支持 Basic Auth 头,以及对 401 给出人话提示:

if (resp.responseCode === 401) {
  return this.fail('认证失败(401):该 Arthas 要求用户名密码,请在密码框填入其启动日志中的密码');
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.username.length > 0) {
  headers['Authorization'] = `Basic ${this.basicAuth()}`;
}

4.4 把网络自检做进 UI

联调时最浪费时间的莫过于"连不上,但不知道是哪一层的锅"。我们把网络自检直接做成了连接栏的一个按钮,一键打印本机 IP、网关、DNS、目标地址:

async runDiag(): Promise<void> {
  const lines: string[] = [];
  const has = await connection.hasDefaultNet();
  lines.push(`默认网络:${has ? '有' : '无'}`);
  if (has) {
    const net = await connection.getDefaultNet();
    const props = await connection.getConnectionProperties(net);
    // ... 输出本机IP / DNS
  }
  lines.push(`目标:${this.host}:${this.portText}`);
  this.diagText = lines.join('  ');
}

连接栏与网络自检

这个 200 行不到的小功能,在后面的联调里立了大功——看到"本机IP:192.168.1.7 目标:192.168.1.9"却依然连不上,我们才能确定问题不在地址,而在网段或代理。

五、真机联调实录:每一个错误码背后都是一堂课

架构正确了,联调依然是一场硬仗。以下按时间顺序记录几个典型回合,错误码均为真机实录。

5.1 第一回合:局域网直连不通

最初的目标 JVM 跑在 Mac 上(192.168.1.9:8563),鸿蒙 PC 填这个地址,连接失败。网络自检显示本机是 192.168.1.7,同网段,却依然不通——最终定位是中间网络设备/代理策略的问题,而非应用配置。

解法:hdc 反向转发,绕开整个局域网。 用 USB 调试通道把 Mac 的 8563 映射到鸿蒙 PC 的回环口:

# Mac 侧执行
hdc rport tcp:8563 tcp:8563

之后鸿蒙 PC 上访问 127.0.0.1:8563,流量走 USB 线直达 Mac 的 Arthas,不经过任何局域网设备。这一招让"App 连不上开发机"的问题被结构性消灭,也让第一版 GUI 顺利截图验证。

复盘:跨设备联调时,USB 通道(hdc rport/fport)是比排查局域网策略可靠得多的兜底路径。rport 反向(设备端口→主机服务)、fport 正向(主机端口→设备服务),配合使用几乎可以无视网络环境。

5.2 第二回合:错误码 2300007 与一个字符的战争

连接失败 2300007

服务端 netstat 明明显示 127.0.0.1:8563 LISTEN,应用却报 2300007(Failed to connect),反复重试无果。排查了近十个回合,最后发现地址栏里填的是 127.0.1——不是 127.0.0.1,少了一个 0。五段变四段,输入框里肉眼几乎不可辨,服务端一切正常,唯独客户端目标地址是错的。

这个回合的教训促成了两个产品决策:

  1. 对 IPv4 地址做格式校验:四段、每段 0-255,不合格直接拦下并提示,而不是放行后吐一个裸错误码;
  2. 错误提示要带上下文:把"目标地址:端口"回显在错误信息里,让"填错地址"这类问题一眼可见。

人眼校验是最低效的调试手段。能放进代码里的校验,就不要留给肉眼。

5.3 第三回合:用户名 arthas、密码留空,还是 401

连上本机回环后,又卡在鉴权上。这里恰好是 Arthas 鉴权语义(见 4.3)的实践现场:

  • 127.0.0.1 时,来源是本机回环,免鉴权,用户名密码留空即通;
  • 一旦换成 --target-ip 0.0.0.0 暴露到网卡、从别的地址连,必须带 Basic Auth,且密码是 Arthas 自动生成、写在目标机 ~/logs/arthas/arthas.log 里的随机串。

于是客户端的连接表单最终定型为:地址、端口、用户名(默认 arthas)、密码(可空)四项,并对 401 单独给出"去启动日志里找密码"的提示。

5.4 第四回合:在 openEuler 容器里点亮本机 JVM

最初的联调目标是 Mac 上的 JVM,但产品愿景毕竟是"鸿蒙 PC 自成一体"。于是有了最后一章探索:鸿蒙 PC 自己能不能跑 JVM?

鸿蒙 PC 的原生系统里确实没有 java。但 HarmonyOS PC 提供了融合开发引擎——一个 openEuler 容器环境。在里面实测:

openEuler 环境

$ java -version
openjdk version "17.0.15" 2025-04-15
OpenJDK Runtime Environment BiSheng (build 17.0.15+6)
OpenJDK 64-Bit Server VM BiSheng (build 17.0.15+6, mixed mode, sharing)

终端实录:容器内确认 Java 运行时

毕昇 JDK 17,完整可用,javac 也在。于是三步走:

工具链确认:编译器 javac 也就位

第一步,写一个有活动负载的 Demo 进程(后台线程做计算、主线程周期性分配内存,这样 Dashboard 上才有东西可看):

cd ~/arthas-demo && cat > Demo.java <<'EOF'
// business-worker 线程每秒 busyWork;
// 主线程每 2s 分配 512KB,到 50 块清空,制造 GC 波形
EOF
javac Demo.java && nohup java Demo > demo.log 2>&1 &

编写并编译 Demo.java

jps 确认 Demo 进程运行,PID 397

第二步,挂 Arthas(容器内在线下载一路畅通):

curl -sL https://arthas.aliyun.com/arthas-boot.jar -o arthas-boot.jar
java -jar arthas-boot.jar 397 \
  --telnet-port 8563 --http-port 3658 \
  --target-ip 0.0.0.0 \
  --username arthas --password arthas

Arthas 4.3.5 自动下载、attach 成功,熟悉的 ASCII 大字 logo 跳出来:

[INFO] Attach process 397 success.
[INFO] arthas-client connect 127.0.0.1 8563
wiki         https://arthas.aliyun.com/doc
version      4.3.5
main_class   Demo
pid          397
[arthas@397]$

Arthas 4.3.5 下载并 attach 成功

顺带一个反向提醒:[arthas@397]$ 这个提示符不是 shell。把 netstatcd 甚至误触的 v 敲进去,只会得到 command not found——它只认 dashboardthreadwatch 这些 Arthas 自家命令。实战中在这里空转过几个回合,值得记一笔。

Arthas 交互控制台只认自家命令

第三步,验证端口与连通:

$ netstat -an | grep -E '8563|3658'
tcp6  0  0 127.0.0.1:8563   :::*  LISTEN
tcp6  0  0 127.0.0.1:3658   :::*  LISTEN

端口验证实录:netstat 确认 8563/3658 双端口监听

到这里出现了一个非常有价值的认知修正:容器内 netstat 显示监听 127.0.0.1,但鸿蒙原生应用连这个 127.0.0.1 是连不到的——openEuler 容器与鸿蒙原生系统是隔离的两套网络栈,容器里的回环地址只在容器内可见。这和第二部分"沙箱里装不下 JVM"是同一类结构问题:边界之内的一切,对边界之外都是透明的反面——不可见

正确姿势是让容器内服务监听 0.0.0.0(如上命令),再从容器分到的网卡 IP 去连;跨环境的 127.0.0.1 永远只指"自己所在的这一层"。

在这里插入图片描述

5.5 联调方法论沉淀

四个回合下来,沉淀出一套鸿蒙 PC 网络问题的分层排查顺序,后来凡遇连接问题按此走,基本不再空转:

  1. 客户端校验层:地址格式、端口范围——把 127.0.1 这类问题拦在发起请求之前;
  2. 系统网络层:网络自检(本机 IP/DNS/默认网络),确认不是无网、不是网段错;
  3. 代理层:usingProxy: false 了吗?环境代理是否吞掉了内网请求;
  4. 服务端层:目标机上 netstat/ss 确认监听地址与端口——注意监听在 127.0.0.1 还是 0.0.0.0,跨环境时这是生死线;
  5. 鉴权层:401 → 查目标机 ~/logs/arthas/arthas.log 里的自动生成密码;
  6. 兜底:USB 通道 hdc rport/fport,绕开一切网络策略。

六、五大面板的实现映射

连通之后,面板就是纯粹的"命令 → JSON → UI"映射,这也是 HTTP 客户端方案的优雅之处——Arthas 命令行的能力边界就是 GUI 的能力边界:

面板Arthas 命令说明
Dashboarddashboard -n 1线程统计、内存、运行时、CPU Top 线程
Threadsthread全部线程列表,含状态/优先级/CPU 占比
JVMjvmJVM 参数分组 KV 展示
SysPropsysprop系统属性,支持实时过滤
SysEnvsysenv环境变量,支持实时过滤

Dashboard 面板

值得强调的是只读纪律:客户端只下发查询类命令,不封装 watch/trace/retransform 这类有侵入性的命令,保证工具本身对目标 JVM 零风险——这也是它能通过 normal APL 审核、被放心使用的底气。

七、总结:鸿蒙 PC 适配的六条军规

整条适配之路,可以浓缩成六条经验:

  1. 先问"该不该跑",再问"能不能跑"。三条系统级硬约束(execmem/attach/noexec)证明:在受限平台上,搬运完整运行时常常是死路;重新切分职责(数据采集留在目标 JVM,渲染留在 HAP)才是活路。
  2. 善用平台已有的容器能力。鸿蒙 PC 的 openEuler 融合开发引擎自带毕昇 JDK 17,“鸿蒙 PC 上没有 Java"是个伪命题——准确说是"原生侧没有,容器里有”。
  3. 容器边界意识。容器内外的 127.0.0.1 不是同一个地址;跨环境连接,要么监听 0.0.0.0 走真实 IP,要么用 hdc 端口转发打洞。
  4. 诊断流量直连usingProxy: false 应该是所有运维/诊断类工具的默认选择。
  5. 把校验和自检做进产品。地址格式校验、一键网络自检、带上下文的错误提示(包括把错误码翻译成人话,比如 2300007 → 连接失败),这些"小事"在联调时省下的时间是指数级的。
  6. 错误码是最好的文档。每一个 2300007、每一次 401,背后都对应一层明确的原因;把排查顺序固化成分层清单,问题就变成了查表。

从"多少个回合都连不上"的深夜,到 openEuler 容器里 [arthas@397]$ 的提示符亮起,再到鸿蒙 PC 原生界面上 Dashboard 数据开始滚动——这条路上真正的产出,不只是一个 200 行核心客户端的 HAP,更是一套"在受限平台上重新思考软件形态"的方法论。

鸿蒙 PC 的生态适配,拼的从来不是蛮力移植,而是对平台边界的精确理解,以及在边界之内做对的架构选择。

八、常见问题 FAQ

Q1:鸿蒙 PC 上能跑 Java 吗?

原生侧不能,但融合开发引擎的 openEuler 容器里自带毕昇 JDK 17(含 javac),java -version 直接可用。“鸿蒙 PC 没有 Java"是个伪命题——准确说是"原生侧没有,容器里有”。

Q2:连接报 2300007(Failed to connect)怎么排查?

按分层清单走:先核对地址格式(实战中曾把 127.0.0.1 误填成 127.0.1,肉眼几乎不可辨)→ 应用内网络自检确认本机 IP/网段 → 确认目标机 netstat 端口在监听 → 检查是否被系统代理拦截(客户端需 usingProxy: false)。

Q3:Arthas 的密码是什么?401 怎么破?

Arthas 只对来自 127.0.0.1 的连接免鉴权。一旦用 --target-ip 0.0.0.0 监听到网卡,它会自动生成随机密码,写在目标机 ~/logs/arthas/arthas.log 里,用户名默认 arthas,填进连接表单即可。

Q4:容器里明明监听了 127.0.0.1:8563,原生 App 为什么连不上?

openEuler 容器与鸿蒙原生系统是隔离的两套网络栈,容器内的回环地址只在容器内可见。解法:服务监听 0.0.0.0,App 改连容器分到的网卡 IP;或者用 hdc rport/fport 做 USB 通道端口转发打洞。

Q5:normal APL 签名能通过审核吗?

能。本方案只声明 INTERNET + GET_NETWORK_INFO 两个标准权限,全程标准 HTTP + JSON,不碰 JVMTI/execmem/字节码注入,普通 normal 签名即可运行。

Q6:在 [arthas@397]$ 提示符里敲 netstatcd 报 command not found?

这不是 shell,是 Arthas 的交互控制台,只认 dashboardthreadwatch 等自家命令。要跑系统命令,请另开一个终端。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐