多模输入真机测试脚手架

SkillCloud & infra

Use when building or running multimodal input system tests on a real DAYU200 device. Covers NativeToken permission setup, server-side bypass, test binary compile/deploy/run pipeline, phase-based hidumper capture, device discovery polling, and dump field reference.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the 多模输入真机测试脚手架 skill

What this skill tells your AI

The instructions your AI receives, as published by openharmonyinsight/openharmony-skills in skills/ohos-dev-distributed-mmi-device-test-harness/SKILL.md and read by ahel’s review.

在 DAYU200 (RK3568) 真机上运行多模输入子系统测试二进制的完整基础设施。

When to Use

  • 新建多模输入系统测试二进制时,需要权限获取、编译部署、dump 捕获等基础能力
  • 已有测试需要在真机上运行和验证
  • 需要理解 hidumper dump 输出的含义

设备连接拓扑

优先按实际连接方式选择命令形态,不要默认所有环境都有 Windows 跳板机。

直连设备

本机能直接访问 USB 设备时,只需要参数化设备序列号:

export DEVICE="<device_serial>"
export HDC="hdc -t $DEVICE"

Windows 跳板机

设备接在 Windows 测试机、当前环境只能通过 SSH 访问时,再使用跳板机变量。所有命令必须参数化,避免硬编码凭据:

# 设置一次,全 session 复用(根据实际环境替换占位符;不要提交真实值)
export DEVICE="<device_serial>"                    # hdc 设备序列号
export WIN_SSH="ssh <ssh_options> -p <port> <user>@<host>"
export WIN_SCP="scp <scp_options> -P <port>"
export WIN_SCP_DST="<user>@<host>:Desktop"         # SCP 目标(跳板机桌面)
export WIN_HOME='C:\Users\<user>'                  # Windows 用户主目录
export HDC="hdc -t $DEVICE"

后续文档中的 $WIN_SSH$WIN_SCP$WIN_SCP_DST$WIN_HOME$HDC 均引用这些变量。直连场景下去掉 $WIN_SSH 包装,直接运行 $HDC ...。 SSH 认证方式由环境决定,可使用 SSH key、CI secret 或其他跳板机机制。Skill 输出和 eval 结果中不得包含真实密码、主机名、用户名或设备 serial。

设备状态修改安全门槛

以下操作会改变真机系统状态,执行前必须先取得用户明确授权:

  • server bypass 代码改动对应的 .so 部署
  • mount -o rw,remount /
  • 替换 /system/lib/*.so
  • kill $(pidof multimodalinput) 或其他服务重启命令
  • setenforce 0 / setenforce 1
  • 设备重启、系统分区写入或清理备份文件

授权前先向用户列出:

  • 要修改的设备和目标进程
  • 要备份、替换、还原的文件路径
  • 备份保存位置和校验方式
  • 预期还原状态
  • 最终验证命令

报告中必须记录备份结果、替换结果、还原结果和最终验证结果。没有授权时,只能生成命令计划和检查清单,不得直接执行会改设备状态的命令。

权限获取

测试进程调用 InputManager IPC 需要系统权限:

#include "accesstoken_kit.h"
#include "nativetoken_kit.h"
#include "token_setproc.h"

static void InitNativeToken() {
    const char *perms[] = {
        "ohos.permission.INPUT_DEVICE_CONFIGURATOR",
        "ohos.permission.INPUT_MONITORING",
    };
    NativeTokenInfoParams info = {
        .dcapsNum = 0,
        .permsNum = sizeof(perms) / sizeof(perms[0]),
        .aclsNum = 0,
        .dcaps = nullptr,
        .perms = perms,
        .acls = nullptr,
        .processName = "YourTestName",
        .aplStr = "system_basic",
    };
    uint64_t tokenId = GetAccessTokenId(&info);
    SetSelfTokenID(tokenId);
    OHOS::Security::AccessToken::AccessTokenKit::ReloadNativeTokenInfo();
}

必须在 main() 最开始、其他 InputManager 调用之前执行。

权限列表按测试需要调整:

  • INPUT_DEVICE_CONFIGURATOR — 设备绑定/解绑
  • INPUT_MONITORING — 事件监听

Server 端绕过

测试进程不是 WMS/DMS 进程,server 端会校验调用方 token 类型。只有本地验证确实无法通过 NativeToken/权限配置完成时,才允许临时 server-side bypass。

这些绕过只允许用于本地开发验证。提交产品代码、PR 或测试报告前必须还原,且不得把绕过作为正式测试能力描述。

收窄原则

  • 首选 test-only 编译宏、测试进程名白名单或本地 patch 名称限制,不要提交“允许所有 token 类型”的通用绕过。
  • patch 中必须标记 [TEST BYPASS]、目标测试名和还原方式。
  • 测试报告只能说明“本地验证使用临时 bypass”,不能把绕过当作产品行为或长期测试能力。
  • 如果必须短暂放开 token 检查,报告中要明确这是最宽松 fallback,并记录还原验证。

1. OnDisplayInfo — 允许测试进程调 UpdateDisplayInfo

文件: service/message_handle/src/server_msg_handler.cpp

int32_t ServerMsgHandler::OnDisplayInfo(SessionPtr sess, NetPacket &pkt) {
    // [TEST BYPASS] prefer a test-only macro or process whitelist.
    // Avoid broad "allow all token types" changes except as a last-resort local fallback.
}

2. OnWindowGroupInfo — 允许测试进程调 UpdateWindowInfo

文件: service/message_handle/src/server_msg_handler.cpp

int32_t ServerMsgHandler::OnWindowGroupInfo(SessionPtr sess, NetPacket &pkt) {
    // [TEST BYPASS] same narrow local-only rule as OnDisplayInfo.
}

3. CheckBindDevicePermission — 允许测试进程绑定设备

文件: service/module_loader/src/mmi_service.cpp

// [TEST BYPASS] prefer allowing only the named local test process.
// Avoid unconditional return RET_OK except as a last-resort local fallback.

部署绕过后的 .so

# 备份原始 .so
$WIN_SSH "$HDC shell mount -o rw,remount /"
$WIN_SSH "$HDC shell cp /system/lib/libmmi-server.z.so /system/lib/libmmi-server.z.so.bak"
$WIN_SSH "$HDC shell cp /system/lib/libmmi-service.z.so /system/lib/libmmi-service.z.so.bak"

# 推送修改后的 .so(从编译产物)
$WIN_SCP libmmi-server.z.so "$WIN_SCP_DST/libmmi-server.z.so"
$WIN_SSH "$HDC file send $WIN_HOME\\Desktop\\libmmi-server.z.so /system/lib/libmmi-server.z.so"
$WIN_SCP libmmi-service.z.so "$WIN_SCP_DST/libmmi-service.z.so"
$WIN_SSH "$HDC file send $WIN_HOME\\Desktop\\libmmi-service.z.so /system/lib/libmmi-service.z.so"

# 重启 multimodalinput 服务
$WIN_SSH "$HDC shell kill \$(pidof multimodalinput)"

注意:.so 在 /system/lib/,不是 /system/lib/platformsdk//system/lib/chipset-pub-sdk/

还原

$WIN_SSH "$HDC shell mount -o rw,remount /"
$WIN_SSH "$HDC shell cp /system/lib/libmmi-server.z.so.bak /system/lib/libmmi-server.z.so"
$WIN_SSH "$HDC shell cp /system/lib/libmmi-service.z.so.bak /system/lib/libmmi-service.z.so"
$WIN_SSH "$HDC shell kill \$(pidof multimodalinput)"

测试完成后必须还原。 绕过会降低系统安全性。

编译部署流水线

编译

测试二进制通过 compile_test.py 编译(使用 OpenHarmony 工具链交叉编译):

cd <openharmony-source-root>
python3 <compile_test_script>

compile_test.py 的关键配置:

  • SOURCE — 测试 .cpp 源文件路径
  • BUILD_DIR<oh_root>/code/out/rk3568
  • OUTPUT_DIR — 编译产物子目录(如 multimodalinput/input
  • 产物位于 $BUILD_DIR/$OUTPUT_DIR/<target_name>

部署到设备

BINARY="<oh_root>/code/out/rk3568/<output_dir>/<target>"

直连设备:

$HDC file send "$BINARY" /data/local/tmp/<target>

Windows 跳板机:

$WIN_SCP "$BINARY" "$WIN_SCP_DST/<target>"
$WIN_SSH "$HDC file send $WIN_HOME\\Desktop\\<target> /data/local/tmp/<target>"

路径注意:只有 hdc 在 Windows 跳板机上运行时,file send 的源路径才是 Windows 路径(C:\\...)。

运行

$HDC shell chmod 755 /data/local/tmp/<target>
$HDC shell /data/local/tmp/<target>

# Windows 跳板机时:
# $WIN_SSH "$HDC shell chmod 755 /data/local/tmp/<target>"
# $WIN_SSH "$HDC shell /data/local/tmp/<target>"

hdc shell 引号陷阱:不要在 hdc shell 后用单引号包裹命令(hdc shell 'cmd1 && cmd2'),会报 no closing quote。拆成多条 hdc 命令。

Phase-Based Dump 捕获

原理

测试二进制通过写 phase 文件通知外部脚本当前阶段,外部脚本检测到新 phase 后执行 hidumper 捕获。

C++ 端 — DumpPhase()

static void DumpG(const char *label)
{
    std::cout << "\n===== " << label << " =====" << std::endl;
    std::cout.flush();
    FILE *f = fopen("/data/local/tmp/dual_group_phase.txt", "w");
    if (f) { fprintf(f, "%s\n", label); fclose(f); }
    std::this_thread::sleep_for(std::chrono::seconds(3));  // 等外部脚本捕获
}

3 秒 sleep 是关键 — 外部脚本 1 秒轮询一次,需要留出足够时间。

Shell 端 — 后台捕获脚本

#!/system/bin/sh
DUMP=/data/local/tmp/dump.txt
PHASE=/data/local/tmp/dual_group_phase.txt
TEST=/data/local/tmp/<test_binary>
rm -f "$DUMP" "$PHASE"
chmod 755 "$TEST"
$TEST &
PID=$!
LAST=""
while kill -0 $PID 2>/dev/null; do
    if [ -f "$PHASE" ]; then
        CUR=$(cat "$PHASE" 2>/dev/null)
        if [ "$CUR" != "$LAST" ] && [ -n "$CUR" ]; then
            echo "===== DUMP: $CUR =====" >> "$DUMP"
            hidumper -s MultimodalInput -a '-G' >> "$DUMP" 2>&1
            LAST="$CUR"
        fi
    fi
    sleep 1
done
wait $PID 2>/dev/null
echo "TEST_DONE" >> "$DUMP"

关键:PHASE 文件路径必须与 C++ 端一致。之前出过 phase.txt vs dual_group_phase.txt 不匹配导致整个 dump 为空的 bug。

拉取 dump 到本地

# 直连设备
$HDC file recv /data/local/tmp/dump.txt ./dump.txt

# Windows 跳板机
$WIN_SSH "$HDC file recv /data/local/tmp/dump.txt $WIN_HOME\\Desktop\\dump.txt"
$WIN_SCP "$WIN_SCP_DST/../Desktop/dump.txt" /tmp/dump.txt

设备发现轮询

uinput UI_DEV_CREATE 后设备需要被 libinput 扫描到才能通过 InputManager 发现。轮询模式:

static int FindDevice(const std::string &targetName) {
    struct FindState {
        std::mutex mtx;
        std::condition_variable cv;
        int foundId = -1;
        int pending = 0;
        bool done = false;
    };
    for (int attempt = 0; attempt < 30; ++attempt) {
        auto state = std::make_shared<FindState>();
        InputManager::GetInstance()->GetDeviceIds([state, targetName](std::vector<int32_t> &ids) {
            {
                std::lock_guard<std::mutex> lock(state->mtx);
                state->pending = static_cast<int>(ids.size());
                if (state->pending == 0) {
                    state->done = true;
                    state->cv.notify_one();
                    return;
                }
            }
            for (auto id : ids) {
                InputManager::GetInstance()->GetDevice(id,
                    [state, id, targetName](std::shared_ptr<InputDevice> dev) {
                        bool notify = false;
                        {
                            std::lock_guard<std::mutex> lock(state->mtx);
                            if (dev && dev->GetName() == targetName && state->foundId < 0) {
                                state->foundId = id;
                            }
                            if (--state->pending == 0) {
                                state->done = true;
                                notify = true;
                            }
                        }
                        if (notify) {
                            state->cv.notify_one();
                        }
                    });
            }
        });
        std::unique_lock<std::mutex> lock(state->mtx);
        state->cv.wait_for(lock, std::chrono::milliseconds(500), [&] { return state->done; });
        if (state->foundId >= 0) return state->foundId;
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
    }
    return -1;
}

最多等 15 秒(30 次 x 500ms)。实测通常 1-3 秒内发现。

hidumper Dump 字段参考

常用字段和解读细节见 references/hidumper-dump-fields.md。主流程只要求在报告中引用实际 dump 阶段、关键字段和结论,不要复制完整 dump 表。

SELinux 注意

真机默认 SELinux Enforcing,测试前需要临时关闭:

$WIN_SSH "$HDC shell setenforce 0"   # Permissive
# 测试完成后还原:
$WIN_SSH "$HDC shell setenforce 1"   # Enforcing

不关闭 SELinux 时,/dev/uinput 访问和部分 IPC 会被拒绝。

GN 构建目标配置

测试二进制的 BUILD.gn 配置要点:

ohos_executable("dual_group_interleave_test") {
  sources = [ "dual_group_interleave_test.cpp" ]
  deps = [
    "//foundation/multimodalinput/input/frameworks/proxy:libmmi-client",
    "//base/security/access_token/interfaces/innerkits/nativetoken:libnativetoken",
    "//base/security/access_token/interfaces/innerkits/accesstoken:libaccesstoken_sdk",
    "//base/security/access_token/interfaces/innerkits/token_setproc:libtoken_setproc",
  ]
  external_deps = [ "input:libmmi-client" ]
  testonly = true      # 测试专用,不参与正式构建
  part_name = "input"
  subsystem_name = "multimodalinput"
}

testonly = true 确保不会被正式产品构建拉入。

完整执行顺序

准备阶段:
  1. 获取用户明确授权,并记录备份/还原/验证计划
  2. 修改 server 端绕过 → 编译 .so
  3. 备份原 .so → 部署新 .so → 重启服务 → 记录替换验证
  4. SELinux permissive(如测试确需)

测试阶段:
  5. 编译测试二进制 (compile_test.py)
  6. 部署后台 dump 脚本 + 测试二进制到设备
  7. 运行后台 dump 脚本(启动测试 + hidumper 捕获)
  8. 拉取 dump 文件到本地分析

还原阶段:
  9. 还原 server 端 .so(从 .bak)
  10. SELinux enforcing(如果前面改成 permissive)
  11. 验证还原:重启服务 + 确认正常功能,并把结果写入报告

Common Mistakes

错误后果修复
Phase 文件路径不匹配dump 全程为空C++ 写的路径和 shell 读的路径必须完全一致
hdc shell 用单引号包裹多命令no closing quote 错误拆成多条独立 hdc 命令
.so 推到错误路径服务加载旧 .so,绕过不生效路径是 /system/lib/,不是 platformsdk/ 或 chipset-pub-sdk/
忘记 mount -o rw,remount /file send 到 /system/ 失败先 remount
DumpG sleep 太短外部脚本来不及捕获至少 3 秒(脚本 1 秒轮询一次)
忘记 testonly=true正式构建报错或拉入测试代码BUILD.gn 中加 testonly=true
忘记还原 server 绕过安全风险测试完必须从 .bak 还原
未经授权直接 remount/替换 .so/关闭 SELinux改变真机系统状态且难以追责先取得明确授权并记录备份、还原和最终验证
SELinux 未关闭uinput/IPC 被拒绝,报权限错误setenforce 0
InitNativeToken 放在 InputManager 调用之后权限检查失败必须在 main() 最开始调用

Signals

GitHub stars
34
Forks
7
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
ohos-dev-distributed-mmi-device-test-harness
Source
github.com/openharmonyinsight/openharmony-skills