* Copyright (c) 2025 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "input_replay_command.h"
#include <atomic>
#include <charconv>
#include <csignal>
#include <cstring>
#include <getopt.h>
#include <iostream>
#include <thread>
#include <unistd.h>
#include "common.h"
#include "device_manager.h"
#include "event_recorder.h"
#include "event_replayer.h"
namespace OHOS {
namespace MMI {
namespace {
constexpr int32_t MIN_ARGC = 2;
}
std::atomic<bool> g_shutdown { false };
InputReplayCommand::InputReplayCommand(int32_t argc, char** argv)
: argc_(argc), argv_(argv)
{
programName_ = (argc > 0) ? argv[0] : "uinput";
}
bool InputReplayCommand::ParseOptions()
{
static struct option longOptions[] = {
{"help", no_argument, 0, 'h'},
{"list", no_argument, 0, 'l'},
{"all", no_argument, 0, 'a'},
{"map", required_argument, 0, 'm'},
{0, 0, 0, 0}
};
int32_t opt;
int32_t optionIndex = 0;
optind = 1;
while ((opt = getopt_long(argc_, argv_, "hlam:", longOptions, &optionIndex)) != -1) {
switch (opt) {
case 'h':
PrintUsage();
exit(0);
case 'l':
DeviceManager().PrintDeviceList();
exit(0);
case 'a':
useAllDevices_ = true;
break;
case 'm':
if (!ParseDeviceMapping(optarg)) {
return false;
}
break;
default:
return false;
}
}
return true;
}
bool InputReplayCommand::Parse()
{
if (argc_ < MIN_ARGC) {
PrintUsage();
return false;
}
if (!ParseOptions()) {
return false;
}
if (optind >= argc_) {
PrintError("Missing command (record/replay)");
return false;
}
command_ = argv_[optind++];
if (optind >= argc_) {
PrintError("Missing file path");
return false;
}
filePath_ = argv_[optind++];
if (command_ == "record") {
return ParseRecordCommand();
} else if (command_ == "replay") {
return ParseReplayCommand();
} else {
PrintError("Invalid command, supported: record/replay");
return false;
}
}
inline void SignalHandler(int32_t sig)
{
if (sig == SIGINT || sig == SIGTERM) {
g_shutdown.store(true);
std::cout << "\nShutdown signal received, cleaning up..." << std::endl;
}
}
int32_t InputReplayCommand::HandleRecordReplayCommand(int32_t argc, char** argv)
{
OHOS::MMI::InputReplayCommand parser(argc, argv);
if (geteuid() != 0) {
std::cerr << "Error: This program must be run as root" << std::endl;
return RET_ERR;
}
if (!parser.Parse()) {
std::cerr << "Failed to parse record/replay command" << std::endl;
return RET_ERR;
}
if (!parser.Execute()) {
return RET_ERR;
}
return RET_OK;
}
bool InputReplayCommand::Execute()
{
if (command_ == "record") {
return ExecuteRecordCommand();
} else if (command_ == "replay") {
return ExecuteReplayCommand();
}
return false;
}
bool InputReplayCommand::ParseDeviceMapping(const std::string& mappingStr)
{
deviceMapping_.clear();
const char* ptr = mappingStr.c_str();
const char* endPtr = ptr + mappingStr.length();
while (ptr < endPtr) {
uint16_t key;
auto keyResult = std::from_chars(ptr, endPtr, key);
if (keyResult.ec != std::errc() || keyResult.ptr >= endPtr || *keyResult.ptr != ':') {
return false;
}
ptr = keyResult.ptr + 1;
uint16_t value;
auto valueResult = std::from_chars(ptr, endPtr, value);
if (valueResult.ec != std::errc()) {
return false;
}
deviceMapping_[key] = value;
ptr = valueResult.ptr;
if (ptr < endPtr) {
if (*ptr != ',') {
return false;
}
ptr++;
}
}
return !deviceMapping_.empty();
}
void InputReplayCommand::SetupSignalHandlers()
{
struct sigaction sa;
sa.sa_handler = SignalHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGINT, &sa, nullptr);
sigaction(SIGTERM, &sa, nullptr);
}
bool InputReplayCommand::ParseRecordCommand()
{
if (!useAllDevices_) {
while (optind < argc_) {
devicePaths_.push_back(argv_[optind++]);
}
if (devicePaths_.empty()) {
PrintError("No devices specified for recording");
PrintError("Use --all to record from all devices or specify device paths");
return false;
}
}
if (optind < argc_) {
PrintError("Unexpected arguments for record command");
return false;
}
return true;
}
bool InputReplayCommand::ParseReplayCommand()
{
if (useAllDevices_) {
PrintError("Not use -a option for replay command!");
return false;
}
if (optind < argc_) {
PrintError("Unexpected arguments for replay command");
return false;
}
return true;
}
bool InputReplayCommand::ExecuteRecordCommand()
{
std::vector<InputDevice> devices;
if (useAllDevices_) {
DeviceManager deviceManager;
devices = deviceManager.DiscoverDevices();
} else {
const std::string PREFIX = "/dev/input/event";
for (size_t i = 0; i < devicePaths_.size(); i++) {
const std::string& path = devicePaths_[i];
if (path.substr(0, PREFIX.length()) != PREFIX) {
PrintError("Invalid input device path format: %s", path.c_str());
return false;
}
uint16_t deviceId = 0;
const char* start = path.c_str() + PREFIX.length();
const char* end = path.c_str() + path.length();
std::from_chars_result result = std::from_chars(start, end, deviceId);
if (result.ec != std::errc() || result.ptr != end) {
PrintError("Invalid device number in path: %s", path.c_str());
return false;
}
InputDevice device(path, deviceId);
if (device.IsOpen()) {
devices.push_back(std::move(device));
} else {
PrintWarning("Failed to open device: %s", path.c_str());
}
}
}
if (devices.empty()) {
PrintError("No valid input devices specified");
return false;
}
SetupSignalHandlers();
EventRecorder recorder(filePath_);
if (!recorder.Start(devices)) {
return false;
}
recorder.Stop();
return true;
}
bool InputReplayCommand::ExecuteReplayCommand()
{
SetupSignalHandlers();
PrintInfo("Press Enter to start replay...");
std::cin.get();
EventReplayer replayer(filePath_, deviceMapping_);
return replayer.Replay();
}
void InputReplayCommand::PrintUsage() const
{
std::cout << "Usage:" << std::endl
<< " " << programName_ << " [options] record <output-file> [device paths...]" << std::endl
<< " " << programName_ << " [options] replay <input-file>" << std::endl
<< std::endl
<< "Options:" << std::endl
<< " -h, --help Show this help message" << std::endl
<< " -l, --list List available input devices" << std::endl
<< " -a, --all Record from all available input devices" << std::endl
<< " -m, --map Specify device mapping for replay (e.g., \"0:0,1:2,4:2\")" << std::endl
<< " Format: sourceDeviceId:targetDeviceId,..." << std::endl
<< std::endl
<< "Examples:" << std::endl
<< " " << programName_ << " record -a events.bin # Record from all devices" << std::endl
<< " " << programName_ << " record events.bin /dev/input/event0 /dev/input/event1 "
"# Record from specific devices" << std::endl
<< " " << programName_ << " replay events.bin # Replay to original devices" << std::endl
<< " " << programName_
<< " replay -m \"0:1,1:0\" events.bin # Replay with custom device mapping" << std::endl;
}
}
}