Complete infrastructure for running multimodal input subsystem test binaries on a DAYU200 (RK3568) real device.
When to Use
- When creating a new multimodal input system test binary, basic capabilities such as permission acquisition, build and deployment, and dump capture are required
- Existing tests need to be run and verified on a real device
- Need to understand the meaning of hidumper dump output
Device Connection Topology
Choose the command form based on the actual connection method first; do not assume every environment has a Windows jump host.
Direct Device Connection
When the local machine can directly access the USB device, only the device serial number needs to be parameterized:
export DEVICE="<device_serial>"
export HDC="hdc -t $DEVICE"
Windows Jump Host
When the device is connected to a Windows test machine and the current environment can only access it via SSH, then use the jump host variables. All commands must be parameterized to avoid hardcoded credentials:
# 设置一次,全 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"
In the subsequent documentation, $WIN_SSH、$WIN_SCP、$WIN_SCP_DST、$WIN_HOME、$HDC all reference these variables. In the direct connection scenario, remove the $WIN_SSH wrapper and run $HDC ...directly.
The SSH authentication method is determined by the environment; SSH keys, CI secrets, or other jump host mechanisms may be used. Skill output and eval results must not contain real passwords, hostnames, usernames, or device serials.
Safety Threshold for Device State Modification
The following operations will change the real device system state, and explicit user authorization must be obtained before execution:
- The
.sodeployment corresponding to server bypass code changes mount -o rw,remount /- Replacing
/system/lib/*.so kill $(pidof multimodalinput)or other service restart commandssetenforce 0/setenforce 1- Device reboot, system partition writes, or cleanup of backup files
Before authorization, first list for the user:
- The device and target process to be modified
- The file paths to be backed up, replaced, and restored
- Backup storage location and verification method
- Expected restoration state
- Final verification command
The report must record the backup result, replacement result, restoration result, and final verification result. Without authorization, only a command plan and checklist may be generated; commands that change device state must not be executed directly.
Permission Acquisition
A test process calling InputManager IPC requires system permissions:
#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();
}
It must be executed at the very beginning of main(), before other InputManager calls.
Adjust the permission list according to test needs:
INPUT_DEVICE_CONFIGURATOR— device binding/unbindingINPUT_MONITORING— event listening
Server-Side Bypass
The test process is not a WMS/DMS process, and the server side will verify the caller's token type. Only when local verification truly cannot be completed through NativeToken/permission configuration is a temporary server-side bypass allowed.
These bypasses are only allowed for local development verification. They must be reverted before submitting product code, PRs, or test reports, and the bypass must not be described as a formal test capability.
Narrowing Principle:
- Prefer test-only compilation macros, test process name whitelists, or local patch name restrictions; do not submit a generic bypass that "allows all token types."
- The patch must be marked with
[TEST BYPASS], the target test name, and the restoration method. - The test report may only state that "local verification used a temporary bypass"; the bypass must not be treated as product behavior or a long-term test capability.
- If the token check must be briefly relaxed, the report must clearly state that this is the most permissive fallback and record the restoration verification.
1. OnDisplayInfo — allows the test process to call UpdateDisplayInfo
File: 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 — allows the test process to call UpdateWindowInfo
File: 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 — allows the test process to bind devices
File: 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.
Deploy the bypassed .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)"
Note: The .so is in /system/lib/, not /system/lib/platformsdk/ or /system/lib/chipset-pub-sdk/。
Restore
$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)"
Must be restored after testing is complete. The bypass reduces system security.
Build and deployment pipeline
Build
The test binary is compiled via compile_test.py (cross-compiled using the OpenHarmony toolchain):
cd <openharmony-source-root>
python3 <compile_test_script>
compile_test.py key configuration:
SOURCE— path to the test .cpp source fileBUILD_DIR—<oh_root>/code/out/rk3568OUTPUT_DIR— build output subdirectory (e.g.multimodalinput/input)- output is located at
$BUILD_DIR/$OUTPUT_DIR/<target_name>
Deploy to device
BINARY="<oh_root>/code/out/rk3568/<output_dir>/<target>"
Direct device connection:
$HDC file send "$BINARY" /data/local/tmp/<target>
Windows jump host:
$WIN_SCP "$BINARY" "$WIN_SCP_DST/<target>"
$WIN_SSH "$HDC file send $WIN_HOME\\Desktop\\<target> /data/local/tmp/<target>"
Path note: Only when hdc runs on the Windows jump host is the source path offile send a Windows path (C:\\...)。
Run
$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 quoting pitfall: Do not wrap commands in single quotes after hdc shell (hdc shell 'cmd1 && cmd2'), it will report no closing quote. Split into multiple hdc commands.
Phase-Based Dump Capture
Principle
The test binary notifies the external script of the current phase by writing a phase file; after the external script detects a new phase, it executes hidumper to capture.
C++ side — 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)); // 等外部脚本捕获
}
The 3-second sleep is key — the external script polls once per second, so enough time must be allowed.
Shell side — background capture script
#!/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"
Key: The PHASE file path must match the C++ side. There was previously a bug where phase.txt vs dual_group_phase.txt mismatch caused the entire dump to be empty.
Pull dump to local
# 直连设备
$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
Device discovery polling
uinput UI_DEV_CREATE After that, the device needs to be scanned by libinput before it can be discovered through InputManager. Polling mode:
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;
}
Wait up to 15 seconds (30 times x 500ms). In practice, it is usually discovered within 1-3 seconds.
hidumper Dump field reference
For commonly used fields and interpretation details, see references/hidumper-dump-fields.md. The main process only requires citing the actual dump phase, key fields, and conclusions in the report; do not copy the complete dump table.
SELinux note
Real devices default to SELinux Enforcing, which needs to be temporarily disabled before testing:
$WIN_SSH "$HDC shell setenforce 0" # Permissive
# 测试完成后还原:
$WIN_SSH "$HDC shell setenforce 1" # Enforcing
When SELinux is not disabled,/dev/uinput access and some IPC will be denied.
GN build target configuration
Key points for the test binary's BUILD.gn configuration:
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 Ensure it will not be pulled into official product builds.
Complete execution order
准备阶段:
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
| Error | Consequence | Fix |
|---|---|---|
| Phase file path mismatch | The dump is empty throughout | The path written by C++ and the path read by shell must be exactly the same |
| hdc shell wraps multiple commands in single quotes | no closing quote error |
Split into multiple independent hdc commands |
| .so pushed to the wrong path | The service loads the old .so, and the bypass does not take effect | The path is /system/lib/, not platformsdk/ or chipset-pub-sdk/ |
| Forgot mount -o rw,remount / | file send to /system/ failed | remount first |
| DumpG sleep too short | external script does not have time to capture | at least 3 seconds (the script polls once per second) |
| Forgot testonly=true | official build errors or pulls in test code | add testonly=true in BUILD.gn |
| Forgot to restore the server bypass | security risk | must restore from .bak after testing |
| Directly remount/replace .so/disable SELinux without authorization | changes the real device system state and is hard to trace accountability | obtain explicit authorization first and record backup, restore, and final verification |
| SELinux not disabled | uinput/IPC is denied, reporting a permission error | setenforce 0 |
| InitNativeToken is placed after the InputManager call | permission check fails | must be called at the very beginning of main() |