Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,45 @@ 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
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"

- 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
zig-pkg/
zig-out
dist/
.tmp_arkvm_runner/

package.har
package/libs/
83 changes: 83 additions & 0 deletions scripts/run_arkvm_test.sh
Original file line number Diff line number Diff line change
@@ -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}"
40 changes: 24 additions & 16 deletions src/lib.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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 {
Expand All @@ -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(
Expand All @@ -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,
Expand All @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/pack.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
134 changes: 134 additions & 0 deletions test/arkvm_ping.ts
Original file line number Diff line number Diff line change
@@ -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<ESObject>, 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);
},
);
Loading