From cd9f0a4ff7a3f2823c24b5b20b5fcec72dfea3b0 Mon Sep 17 00:00:00 2001 From: richerfu Date: Sun, 24 May 2026 11:07:12 +0800 Subject: [PATCH 1/4] Add arkvm test --- .github/workflows/ci.yml | 41 ++++++++++++ .gitignore | 1 + scripts/run_arkvm_test.sh | 83 +++++++++++++++++++++++ src/lib.zig | 40 +++++++----- src/pack.zig | 2 + test/arkvm_ping.ts | 134 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 285 insertions(+), 16 deletions(-) create mode 100755 scripts/run_arkvm_test.sh create mode 100644 test/arkvm_ping.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae58a2f..67355b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,3 +30,44 @@ jobs: - name: Build run: | zig build + + arkvm-test: + runs-on: ubuntu-latest + name: ArkVM test + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Zig for OpenHarmony + uses: openharmony-zig/setup-zig-ohos@v0.1.0 + with: + tag: "0.16.0" + + - name: Setup ArkVM + id: setup-arkvm + uses: harmony-contrib/arkts-vm@v2.0.0 + with: + cache: true + + - name: Install runtime dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libatomic1 + + - name: Validate Ark host bundle + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: | + set -euo pipefail + test -x "${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" + test -x "${ARK_HOST_TOOLS_DIR}/es2abc" + test -f "${ARK_HOST_TOOLS_DIR}/libace_napi.so" + test -f "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" + test -f "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" + test -f "${ARK_HOST_TOOLS_DIR}/hello.abc" + + - name: Run ArkVM test + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: bash scripts/run_arkvm_test.sh diff --git a/.gitignore b/.gitignore index 87d3fdb..cb9010b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ zig-pkg/ zig-out dist/ +.tmp_arkvm_runner/ package.har package/libs/ diff --git a/scripts/run_arkvm_test.sh b/scripts/run_arkvm_test.sh new file mode 100755 index 0000000..6062cd6 --- /dev/null +++ b/scripts/run_arkvm_test.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." &>/dev/null && pwd)" + +: "${ARK_HOST_TOOLS_DIR:?ARK_HOST_TOOLS_DIR is required}" + +ARK_ES2ABC="${ARK_HOST_TOOLS_DIR}/es2abc" +ARK_JS_NAPI_CLI="${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" +TEST_TIMEOUT_SEC="${TEST_TIMEOUT_SEC:-90}" +RESULT_GRACE_SEC="${RESULT_GRACE_SEC:-2}" +KEEP_WORKDIR="${KEEP_WORKDIR:-0}" +WORK_ROOT="${ARKVM_WORK_ROOT:-${ROOT_DIR}/.tmp_arkvm_runner}" +WORKSPACE="${WORK_ROOT}/ping" +ABC="${WORKSPACE}/suite.abc" +FILES_INFO="${WORKSPACE}/filesInfo.txt" +LOG_FILE="${WORKSPACE}/arkvm.log" +RESULT_PREFIX="__ZIG_PING_ARKVM_RESULT__" + +[[ -x "${ARK_ES2ABC}" ]] || { echo "Missing binary: ${ARK_ES2ABC}" >&2; exit 1; } +[[ -x "${ARK_JS_NAPI_CLI}" ]] || { echo "Missing binary: ${ARK_JS_NAPI_CLI}" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/libace_napi.so" ]] || { echo "Missing shared lib: ${ARK_HOST_TOOLS_DIR}/libace_napi.so" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" ]] || { echo "Missing shared lib: ${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" ]] || { echo "Missing ArkTS stdlib: ${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" >&2; exit 1; } +[[ -f "${ARK_HOST_TOOLS_DIR}/hello.abc" ]] || { echo "Missing ArkVM fixture abc: ${ARK_HOST_TOOLS_DIR}/hello.abc" >&2; exit 1; } + +rm -rf "${WORKSPACE}" +mkdir -p "${WORKSPACE}/module" + +if [[ "${ARKVM_SKIP_BUILD:-0}" != "1" ]]; then + (cd "${ROOT_DIR}" && zig build -Darkvm-test=true -Doptimize=ReleaseSafe) +fi + +cp "${ROOT_DIR}/zig-out/arkvm-host/libzig_ping.so" "${WORKSPACE}/module/" +ln -sf "${ARK_HOST_TOOLS_DIR}/libets_interop_js_napi.so" "${WORKSPACE}/module/libets_interop_js_napi.so" +cp "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" "${WORKSPACE}/" +cp "${ARK_HOST_TOOLS_DIR}/hello.abc" "${WORKSPACE}/" + +TEST_SOURCE="${ROOT_DIR}/test/arkvm_ping.ts" +TEST_REL="${TEST_SOURCE#${ROOT_DIR}/}" +TEST_RECORD="${TEST_REL%.*}" +printf '%s;%s;esm;%s;%s;false\n' "${TEST_SOURCE}" "${TEST_RECORD}" "${TEST_REL}" "${TEST_RECORD}" > "${FILES_INFO}" +"${ARK_ES2ABC}" --merge-abc --extension=ts --module --output "${ABC}" "@${FILES_INFO}" + +: > "${LOG_FILE}" +( + cd "${WORKSPACE}" + export LD_LIBRARY_PATH="${WORKSPACE}:${WORKSPACE}/module:${ARK_HOST_TOOLS_DIR}:${LD_LIBRARY_PATH:-}" + "${ARK_JS_NAPI_CLI}" --entry-point "${TEST_RECORD}" "${ABC}" +) >"${LOG_FILE}" 2>&1 & + +pid=$! +deadline=$((SECONDS + TEST_TIMEOUT_SEC)) +result_deadline=0 +while kill -0 "${pid}" 2>/dev/null; do + if (( result_deadline == 0 )) && grep -q "^${RESULT_PREFIX}" "${LOG_FILE}" 2>/dev/null; then + result_deadline=$((SECONDS + RESULT_GRACE_SEC)) + fi + if (( result_deadline != 0 && SECONDS >= result_deadline )); then + kill -TERM "${pid}" 2>/dev/null || true + wait "${pid}" >/dev/null 2>&1 || true + break + fi + if (( SECONDS >= deadline )); then + kill -TERM "${pid}" 2>/dev/null || true + sleep 1 + kill -KILL "${pid}" 2>/dev/null || true + wait "${pid}" >/dev/null 2>&1 || true + echo "ArkVM test timed out after ${TEST_TIMEOUT_SEC}s" >&2 + cat "${LOG_FILE}" >&2 + exit 124 + fi + sleep 0.2 +done + +cat "${LOG_FILE}" +if grep -Eq 'error\(DebugAllocator\)|Segmentation fault|SIGSEGV|panic:|Cannot execute panda file|load native module failed' "${LOG_FILE}"; then + echo "ArkVM test emitted a fatal runtime diagnostic" >&2 + exit 1 +fi +grep -q "^${RESULT_PREFIX} status=ok" "${LOG_FILE}" + +[[ "${KEEP_WORKDIR}" == "1" ]] || rm -rf "${WORKSPACE}" diff --git a/src/lib.zig b/src/lib.zig index 36c0028..0356f67 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -139,10 +139,11 @@ fn ping_execute(config: PingConfig) !ArrayList(PingResult) { errdefer deinitPartialPingResults(allocator, &results); if (target_addr.family != posix.AF.INET and target_addr.family != posix.AF.INET6) { - return napi.Error.fromReason("IPv4 is not supported"); + return napi.Error.fromReason("IP address family is not supported"); } - const socket_rc = std.c.socket(@intCast(target_addr.family), @intCast(posix.SOCK.DGRAM), @intCast(posix.IPPROTO.ICMP)); + const protocol: u32 = if (target_addr.family == posix.AF.INET6) @intCast(posix.IPPROTO.ICMPV6) else @intCast(posix.IPPROTO.ICMP); + const socket_rc = std.c.socket(@intCast(target_addr.family), @intCast(posix.SOCK.DGRAM), protocol); if (socket_rc < 0) { return napi.Error.fromReason("Failed to create socket"); } @@ -166,6 +167,10 @@ fn ping_execute(config: PingConfig) !ArrayList(PingResult) { for (0..config.config.count) |index| { // Create ICMP packet with auto-generated payload var packet = pack.ICMPPacket.init(allocator, 1, @intCast(index)) catch @panic("Failed to initialize ICMP packet"); + const echo_reply_type = if (target_addr.family == posix.AF.INET6) pack.ICMPV6_ECHO_REPLY else pack.ICMP_ECHO_REPLY; + if (target_addr.family == posix.AF.INET6) { + packet.header.type = pack.ICMPV6_ECHO_REQUEST; + } defer allocator.free(packet.data); const packet_data = packet.serialize(allocator) catch { @@ -188,18 +193,21 @@ fn ping_execute(config: PingConfig) !ArrayList(PingResult) { const rtt_ms = @as(f64, @floatFromInt(rtt_ns)) / 1_000_000.0; // Parse the received packet - const icmp_data = pack.extractICMPFromIP(buffer[0..bytes_received]) catch { - appendPingResult( - allocator, - &results, - 1, - rtt_ms, - false, - "Failed to extract ICMP data from IP packet", - target_ip, - ) catch @panic("Failed to append PingResult"); - continue; - }; + const icmp_data = if (target_addr.family == posix.AF.INET6) + buffer[0..bytes_received] + else + pack.extractICMPFromIP(buffer[0..bytes_received]) catch { + appendPingResult( + allocator, + &results, + 1, + rtt_ms, + false, + "Failed to extract ICMP data from IP packet", + target_ip, + ) catch @panic("Failed to append PingResult"); + continue; + }; const received_packet = pack.ICMPPacket.parse(allocator, icmp_data) catch { appendPingResult( @@ -215,7 +223,7 @@ fn ping_execute(config: PingConfig) !ArrayList(PingResult) { }; // Verify checksum - if (!received_packet.verifyChecksum()) { + if (target_addr.family == posix.AF.INET and !received_packet.verifyChecksum()) { appendPingResult( allocator, &results, @@ -229,7 +237,7 @@ fn ping_execute(config: PingConfig) !ArrayList(PingResult) { } // Check if it's an echo reply - const is_echo_reply = received_packet.header.type == pack.ICMP_ECHO_REPLY; + const is_echo_reply = received_packet.header.type == echo_reply_type; const sequence_match = std.mem.nativeToBig(u16, received_packet.header.sequence) == index; appendPingResult( diff --git a/src/pack.zig b/src/pack.zig index 4f639bb..3f5f59f 100644 --- a/src/pack.zig +++ b/src/pack.zig @@ -22,6 +22,8 @@ pub const ICMPHeader = packed struct { pub const ICMP_ECHO_REQUEST: u8 = 8; pub const ICMP_ECHO_REPLY: u8 = 0; +pub const ICMPV6_ECHO_REQUEST: u8 = 128; +pub const ICMPV6_ECHO_REPLY: u8 = 129; pub const IPHeader = packed struct { version_ihl: u8, diff --git a/test/arkvm_ping.ts b/test/arkvm_ping.ts new file mode 100644 index 0000000..1e1c70e --- /dev/null +++ b/test/arkvm_ping.ts @@ -0,0 +1,134 @@ +declare function requireNapiPreview(name: string, isApp: boolean): ESObject; +declare function print(message: string): void; +declare function setInterval(callback: () => void, delay: number): number; +declare function clearInterval(id: number): void; + +type NativeAddon = ESObject; + +const RESULT_PREFIX = "__ZIG_PING_ARKVM_RESULT__"; +const KEEP_ALIVE_INTERVAL_MS = 10; +const SUITE_TIMEOUT_MS = 30000; + +function fail(message: string): never { + print(`${RESULT_PREFIX} status=fail message=${message}`); + throw new Error(message); +} + +function assert(condition: boolean, message: string) { + if (!condition) { + fail(message); + } +} + +async function assertRejects(promise: Promise, expectedMessage: string, message: string) { + let rejected = false; + try { + await promise; + } catch (err) { + rejected = true; + const actual = String(err && (err.message || err)); + assert(actual.indexOf(expectedMessage) >= 0, `${message}: ${actual}`); + } + assert(rejected, `${message}: expected rejection`); +} + +function assertIPv4(value: string, message: string) { + assert(/^\d{1,3}(\.\d{1,3}){3}$/.test(value), `${message}: ${value}`); +} + +function assertIPv6(value: string, message: string) { + assert(value.indexOf(":") >= 0, `${message}: ${value}`); +} + +async function assertPingResult( + native: NativeAddon, + host: string, + ipVersion: string, + checkIP: (value: string, message: string) => void, +) { + const results = await native.ping(host, { + count: 1, + interval_ms: 1, + timeout_ms: 1000, + ip_version: ipVersion, + }); + + assert(Array.isArray(results), `${host} ${ipVersion} result should be an array`); + assert(results.length === 1, `${host} ${ipVersion} result should contain one item`); + + const first = results[0]; + assert(typeof first.sequence === "number", `${host} ${ipVersion} sequence should be a number`); + assert(typeof first.rtt_ms === "number", `${host} ${ipVersion} rtt_ms should be a number`); + assert(typeof first.success === "boolean", `${host} ${ipVersion} success should be a boolean`); + assert(first.success === true, `${host} ${ipVersion} ping should succeed`); + assert(typeof first.ip_addr === "string", `${host} ${ipVersion} ip_addr should be a string`); + checkIP(first.ip_addr, `${host} ${ipVersion} ip_addr`); +} + +function installTimerRuntime() { + const etsInterop = requireNapiPreview("ets_interop_js_napi", true) as ESObject; + const created = etsInterop.createRuntime({ + "panda-files": "./hello.abc", + "boot-panda-files": "./etsstdlib.abc:./hello.abc", + "xgc-trigger-type": "never", + }); + assert(!!created, "failed to initialize ArkVM timer runtime"); +} + +async function run(native: NativeAddon) { + assert(typeof native.ping === "function", "ping export should be a function"); + + await assertRejects( + native.ping("zig-ping.invalid", { count: 1, timeout_ms: 10, ip_version: "ipv4" }), + "Failed to get IP address", + "invalid host should reject", + ); + + await assertPingResult(native, "127.0.0.1", "ipv4", assertIPv4); + await assertPingResult(native, "127.0.0.1.sslip.io", "ipv4", assertIPv4); + await assertPingResult( + native, + "0000-0000-0000-0000-0000-0000-0000-0001.sslip.io", + "ipv6", + assertIPv6, + ); +} + +installTimerRuntime(); + +let finished = false; +let elapsed = 0; +const keepAlive = setInterval(() => { + if (finished) { + clearInterval(keepAlive); + return; + } + elapsed += KEEP_ALIVE_INTERVAL_MS; + if (elapsed >= SUITE_TIMEOUT_MS) { + finished = true; + clearInterval(keepAlive); + fail(`suite timed out after ${SUITE_TIMEOUT_MS}ms`); + } +}, KEEP_ALIVE_INTERVAL_MS); + +Promise.resolve() + .then(() => run(requireNapiPreview("zig_ping", true) as NativeAddon)) + .then( + () => { + if (finished) { + return; + } + finished = true; + clearInterval(keepAlive); + print(`${RESULT_PREFIX} status=ok`); + }, + (err) => { + if (finished) { + return; + } + finished = true; + clearInterval(keepAlive); + const message = String(err && (err.message || err)); + fail(message); + }, + ); From 0f536599cd89c3638e67d190ba6e1753bb0dcb17 Mon Sep 17 00:00:00 2001 From: richerfu Date: Sun, 24 May 2026 11:14:22 +0800 Subject: [PATCH 2/4] Fix create socket in CI --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 67355b8..abc43b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - name: Install runtime dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends libatomic1 + sudo apt-get install -y --no-install-recommends libatomic1 libcap2-bin - name: Validate Ark host bundle env: @@ -67,6 +67,11 @@ jobs: test -f "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" test -f "${ARK_HOST_TOOLS_DIR}/hello.abc" + - name: Allow ICMP sockets + env: + ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} + run: sudo setcap cap_net_raw+ep "${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" + - name: Run ArkVM test env: ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} From 6f28cc1bff80e35d28aa5d046b8b9ae5e2cca480 Mon Sep 17 00:00:00 2001 From: richerfu Date: Sun, 24 May 2026 11:21:05 +0800 Subject: [PATCH 3/4] Fix ci role --- .github/workflows/ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc43b9..f1d5aad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - name: Install runtime dependencies run: | sudo apt-get update - sudo apt-get install -y --no-install-recommends libatomic1 libcap2-bin + sudo apt-get install -y --no-install-recommends libatomic1 - name: Validate Ark host bundle env: @@ -67,12 +67,12 @@ jobs: test -f "${ARK_HOST_TOOLS_DIR}/etsstdlib.abc" test -f "${ARK_HOST_TOOLS_DIR}/hello.abc" - - name: Allow ICMP sockets - env: - ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} - run: sudo setcap cap_net_raw+ep "${ARK_HOST_TOOLS_DIR}/ark_js_napi_cli" - - name: Run ArkVM test env: ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} - run: bash scripts/run_arkvm_test.sh + run: | + zig build -Darkvm-test=true -Doptimize=ReleaseSafe + sudo env \ + ARK_HOST_TOOLS_DIR="${ARK_HOST_TOOLS_DIR}" \ + ARKVM_SKIP_BUILD=1 \ + bash scripts/run_arkvm_test.sh From 0e8a2341cbbc7b3f6cf6382977c6e7289e569409 Mon Sep 17 00:00:00 2001 From: richerfu Date: Sun, 24 May 2026 11:28:39 +0800 Subject: [PATCH 4/4] Fix socket role --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f1d5aad..91ff8df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y --no-install-recommends libatomic1 + sudo sysctl -w net.ipv4.ping_group_range="0 2147483647" - name: Validate Ark host bundle env: @@ -70,9 +71,4 @@ jobs: - name: Run ArkVM test env: ARK_HOST_TOOLS_DIR: ${{ steps.setup-arkvm.outputs.arkvm-path }} - run: | - zig build -Darkvm-test=true -Doptimize=ReleaseSafe - sudo env \ - ARK_HOST_TOOLS_DIR="${ARK_HOST_TOOLS_DIR}" \ - ARKVM_SKIP_BUILD=1 \ - bash scripts/run_arkvm_test.sh + run: bash scripts/run_arkvm_test.sh