Files
MaaAssistantArknights/src/MaaCore/Controller/AdbController.cpp
Loong b2fa9a1c3e fix: 显式声明 adb-lite client 对应的 serial (#16853)
## 概述

- 在 `AdbController::connect` 确定目标 address/serial 后,通过
`PlatformIO::set_adb_serial` 显式声明当前 adb 设备。
- NativeIO 默认忽略该调用;AdbLiteIO 使用该 serial 初始化或切换内部 adb-lite client。
- adb-lite 的 `shell`、`exec-out`、`push` 和交互式 shell 只使用已声明的
client,不再从命令路径里隐式初始化 client。
- AdbLiteIO 内部集中校验 client/serial,并用 mutex 覆盖 `m_adb_client` /
`m_adb_serial` 以及对 adb-lite client 的调用,避免 `call_command` 与
`interactive_shell` 并发访问同一状态。

## 背景

#16850 已修复 `adb connect` 被跳过后 adb-lite client 未初始化的问题,并补充了内部锁保护。这个 PR
是后续调整:把 client 初始化入口从命令执行路径挪到连接生命周期中,让状态声明更明确。

## 修复方式

连接阶段已经知道当前 address/serial,所以在设置 connect/release 命令后调用:

```cpp
m_platform_io->set_adb_serial(address);
```

对 NativeIO 来说这是 no-op;对 AdbLiteIO 来说,这是初始化或切换 `adb::client` 的入口。

后续 adb-lite 命令路径通过 `lock_adb_client(serial)` 统一校验命令中的 `-s <serial>` 与当前
client 对应的 serial 一致,并在同一把锁仍然持有时调用 adb-lite client。这样避免在
`interactive_shell` 中隐式写入 client/serial 状态,也避免把 `m_adb_client` 暴露给无锁调用。

`set_adb_serial` 也改为先创建临时 `adb::client`,创建成功后再提交 `m_adb_serial` /
`m_adb_client` 状态;如果创建失败则记录错误并清空 adb-lite client 状态,避免 serial 已切换但
client 仍指向旧对象的不一致状态。

## 验证

- `git diff --check --cached`
- `cmake --build /private/tmp/maa-build-8599-core --parallel 8`
- `cmake --install /private/tmp/maa-build-8599-core --config
RelWithDebInfo`
- 本机 `maa` CLI + Android 35 arm64 AVD:
  - `maa run connect-only -p adb-lite-test --no-summary -vvv`
  - 日志显示 `Loading MaaCore from: /private/tmp/maa-test-data-8599/lib`
  - 日志显示 `Setting adb lite enabled to true`
  - 日志到达 `Connected`
  - 日志到达 `FastestWayToScreencap Encode`
  - 未出现 `adb client not initialized`
  - 未出现 `failed to create adb-lite client`
  - 未出现 `fallback to NativeIO`
2026-05-29 00:46:30 +08:00

1212 lines
41 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "AdbController.h"
#include "Assistant.h"
#include "Controller.h"
#include "MaaUtils/NoWarningCV.hpp"
#include <cstdint>
#include <numeric>
#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable: 4068)
#endif
#include <zlib/decompress.hpp>
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#include "Common/AsstTypes.h"
#include "Config/GeneralConfig.h"
#include "Utils/Logger.hpp"
#include "Utils/Platform.hpp"
#include "Utils/StringMisc.hpp"
#include <boost/regex.hpp>
asst::AdbController::AdbController(const AsstCallback& callback, Assistant* inst, PlatformType type) :
InstHelper(inst),
m_callback(callback)
{
LogTraceFunction;
m_platform_io = PlatformFactory::create_platform(inst, type);
if (!m_platform_io) {
Log.error("platform not supported");
throw std::runtime_error("platform not supported");
}
m_support_socket = m_platform_io->m_support_socket;
if (!m_support_socket) {
Log.error("socket not supported");
}
}
asst::AdbController::~AdbController()
{
LogTraceFunction;
m_inited = false;
release();
}
std::optional<std::string> asst::AdbController::reconnect(const std::string& cmd, int64_t timeout, bool recv_by_socket)
{
LogTraceFunction;
json::value reconnect_info = json::object {
{ "uuid", m_uuid },
{ "what", "Reconnecting" },
{ "why", "" },
{ "details",
json::object {
{ "reconnect", m_adb.connect },
{ "cmd", cmd },
} },
};
static constexpr int ReconnectTimes = 5;
for (int i = 0; i < ReconnectTimes; ++i) {
if (need_exit()) {
break;
}
reconnect_info["details"]["times"] = i;
callback(AsstMsg::ConnectionInfo, reconnect_info);
sleep(10 * 1000);
if (need_exit()) {
break;
}
auto reconnect_ret = call_command(m_adb.connect, 60LL * 1000, false /* 禁止重连避免无限递归 */);
if (need_exit()) {
break;
}
bool is_reconnect_success = false;
if (reconnect_ret) {
auto& reconnect_str = reconnect_ret.value();
is_reconnect_success = reconnect_str.find("error") == std::string::npos;
}
if (is_reconnect_success) {
auto recall_ret = call_command(cmd, timeout, false /* 禁止重连避免无限递归 */, recv_by_socket);
if (recall_ret) {
// 重连并成功执行了
reconnect_info["what"] = "Reconnected";
callback(AsstMsg::ConnectionInfo, reconnect_info);
return recall_ret;
}
}
}
json::value info = json::object {
{ "uuid", m_uuid },
{ "what", "Disconnect" },
{ "why", "Reconnect failed" },
{ "details",
json::object {
{ "cmd", m_adb.connect },
} },
};
m_inited = false; // 重连失败,释放
release();
callback(AsstMsg::ConnectionInfo, info);
return std::nullopt;
}
std::optional<std::string> asst::AdbController::call_command(
const std::string& cmd,
int64_t timeout,
bool allow_reconnect,
bool recv_by_socket)
{
using namespace std::chrono_literals;
using namespace std::chrono;
// LogTraceScope(std::string(__FUNCTION__) + " | `" + cmd + "`");
std::string pipe_data;
std::string sock_data;
asst::platform::single_page_buffer<char> pipe_buffer;
asst::platform::single_page_buffer<char> sock_buffer;
auto start_time = steady_clock::now();
std::unique_lock<std::mutex> callcmd_lock(m_callcmd_mutex);
std::optional<int> exit_res;
exit_res = m_platform_io->call_command(cmd, recv_by_socket, pipe_data, sock_data, timeout, start_time);
if (!exit_res) {
Log.warn("Call `", cmd, "` failed");
return std::nullopt;
}
const int exit_ret = exit_res.value();
callcmd_lock.unlock();
m_last_command_duration = duration_cast<milliseconds>(steady_clock::now() - start_time).count();
Log.info(
"Call `",
cmd,
"` ret",
exit_ret,
", cost",
m_last_command_duration,
"ms , stdout size:",
pipe_data.size(),
", socket size:",
sock_data.size());
if (!pipe_data.empty() && pipe_data.size() < 4096) {
m_pipe_data_size = pipe_data.size();
Log.trace("stdout output:", Logger::separator::newline, pipe_data);
}
if (recv_by_socket && !sock_data.empty() && sock_data.size() < 4096) {
Log.trace("socket output:", Logger::separator::newline, sock_data);
}
// 直接 return避免走到下面的 else if 里的 m_inited = false) 关闭 adb 连接,
// 导致停止后再开始任务还需要重连一次
if (need_exit()) {
return std::nullopt;
}
if (!exit_ret) {
return recv_by_socket ? sock_data : pipe_data;
}
else if (inited() && allow_reconnect) {
// 之前可以运行,突然运行不了了,这种情况多半是 adb 炸了。所以重新连接一下
return reconnect(cmd, timeout, recv_by_socket);
}
return std::nullopt;
}
size_t asst::AdbController::get_pipe_data_size() const noexcept
{
return m_pipe_data_size;
}
size_t asst::AdbController::get_version() const noexcept
{
return m_version;
}
void asst::AdbController::callback(AsstMsg msg, const json::value& details)
{
if (m_callback) {
m_callback(msg, details, m_inst);
}
}
int asst::AdbController::get_mumu_index(const std::string& address)
{
LogTrace << VAR(address);
auto pos = address.find(":");
if (pos == std::string::npos) {
Log.error("address is invalid", address);
return 0;
}
std::string port_str = address.substr(pos + 1);
if (port_str.empty() || !std::ranges::all_of(port_str, [](const char& c) -> bool { return std::isdigit(c); })) {
Log.error("port is invalid", port_str);
return 0;
}
int port = std::stoi(port_str);
int mumu_index = 0;
if (port >= 16384) {
mumu_index = (port - 16384) / 32;
}
else if (port == 7555) {
mumu_index = 0;
LogWarn << "Port 7555 is deprecated for MuMu6, please use 16384 or above.";
}
else if (port >= 5555) {
mumu_index = (port - 5555) / 2;
}
LogInfo << VAR(port_str) << VAR(port) << VAR(mumu_index);
return mumu_index;
}
void asst::AdbController::init_mumu_extras(const AdbCfg& adb_cfg, const std::string& address)
{
#if !ASST_WITH_EMULATOR_EXTRAS
std::ignore = adb_cfg;
std::ignore = address;
Log.error("MaaCore is not compiled with ASST_WITH_EMULATOR_EXTRAS");
#else
if (adb_cfg.extras.empty()) {
LogWarn << "adb_cfg.extras is empty";
return;
}
set_mumu_package(adb_cfg.extras.get("client_type", ""));
std::filesystem::path mumu_path = utils::path(adb_cfg.extras.get("path", ""));
if (adb_cfg.extras.contains("index")) { // MuMu index is provided directly
m_mumu_extras.init(mumu_path, adb_cfg.extras.get("index", 0));
}
else {
m_mumu_extras.init(mumu_path, get_mumu_index(address));
}
#endif
}
void asst::AdbController::set_mumu_package(const std::string& client_type)
{
#if !ASST_WITH_EMULATOR_EXTRAS
std::ignore = client_type;
Log.error("MaaCore is not compiled with ASST_WITH_EMULATOR_EXTRAS");
#else
std::string package_name = Config.get_package_name(client_type).value_or("");
m_mumu_extras.set_package_name(package_name);
#endif
}
int asst::AdbController::get_ld_index(const std::string& address)
{
LogTrace << VAR(address);
// emulator-5554
if (address.starts_with("emulator-")) {
constexpr int base_emulator_port = 5554;
std::string port_str = address.substr(9); // after "emulator-"
if (port_str.empty() || !std::ranges::all_of(port_str, [](char c) { return std::isdigit(c); })) {
Log.error("emulator port is invalid", port_str);
return 0;
}
int port = std::stoi(port_str);
int index = (port - base_emulator_port) / 2;
LogInfo << VAR(port_str) << VAR(port) << VAR(index);
return index;
}
// 127.0.0.1:5555
auto pos = address.find(':');
if (pos != std::string::npos && address.substr(0, pos) == "127.0.0.1") {
constexpr int base_adb_port = 5555;
std::string port_str = address.substr(pos + 1);
if (port_str.empty() || !std::ranges::all_of(port_str, [](char c) { return std::isdigit(c); })) {
Log.error("adb port is invalid", port_str);
return 0;
}
int port = std::stoi(port_str);
int index = (port - base_adb_port) / 2;
LogInfo << VAR(port_str) << VAR(port) << VAR(index);
return index;
}
Log.error("address is invalid or unsupported", address);
return 0;
}
void asst::AdbController::init_ld_extras(const AdbCfg& adb_cfg, const std::string& address)
{
#if !ASST_WITH_EMULATOR_EXTRAS
std::ignore = adb_cfg;
std::ignore = address;
Log.error("MaaCore is not compiled with ASST_WITH_EMULATOR_EXTRAS");
#else
if (adb_cfg.extras.empty()) {
LogWarn << "adb_cfg.extras is empty";
return;
}
std::filesystem::path ld_path = utils::path(adb_cfg.extras.get("path", ""));
int ld_index;
if (adb_cfg.extras.contains("index")) {
ld_index = adb_cfg.extras.get("index", 0);
}
else {
ld_index = get_ld_index(address);
}
int ld_pid = adb_cfg.extras.get("pid", 0);
m_ld_extras.init(ld_path, ld_index, ld_pid, m_width, m_height);
#endif
}
void asst::AdbController::close_socket() noexcept
{
m_platform_io->close_socket();
}
std::optional<unsigned short> asst::AdbController::init_socket(const std::string& local_address)
{
return m_platform_io->init_socket(local_address);
}
void asst::AdbController::clear_info() noexcept
{
m_inited = false;
m_adb = decltype(m_adb)();
m_uuid.clear();
m_width = 0;
m_height = 0;
m_screen_size = { 0, 0 };
}
bool asst::AdbController::inited() const noexcept
{
return m_inited;
}
bool asst::AdbController::start_game(const std::string& client_type)
{
if (client_type.empty()) {
return false;
}
auto package_name = Config.get_package_name(client_type);
if (!package_name) {
return false;
}
std::string cur_cmd = utils::string_replace_all(m_adb.start, "[PackageName]", package_name.value());
bool ret = call_command(cur_cmd).has_value();
return ret;
}
bool asst::AdbController::stop_game(const std::string& client_type)
{
if (client_type.empty()) {
return false;
}
auto package_name = Config.get_package_name(client_type);
if (!package_name) {
return false;
}
std::string cur_cmd = utils::string_replace_all(m_adb.stop, "[PackageName]", package_name.value());
bool ret = call_command(cur_cmd).has_value();
return ret;
}
bool asst::AdbController::click(const Point& p)
{
if (p.x < 0 || p.x >= m_width || p.y < 0 || p.y >= m_height) {
Log.error("click point out of range");
}
std::string cur_cmd =
utils::string_replace_all(m_adb.click, { { "[x]", std::to_string(p.x) }, { "[y]", std::to_string(p.y) } });
return call_command(cur_cmd).has_value();
}
bool asst::AdbController::input(const std::string& text)
{
if (text == "") {
Log.error("empty text");
}
std::string cur_cmd = utils::string_replace_all(m_adb.input, { { "[text]", text } });
return call_command(cur_cmd).has_value();
}
bool asst::AdbController::swipe(
const Point& p1,
const Point& p2,
int duration,
bool extra_swipe,
[[maybe_unused]] double slope_in,
[[maybe_unused]] double slope_out,
[[maybe_unused]] bool with_pause)
{
int x1 = p1.x, y1 = p1.y;
int x2 = p2.x, y2 = p2.y;
// 起点不能在屏幕外,但是终点可以
if (x1 < 0 || x1 >= m_width || y1 < 0 || y1 >= m_height) {
Log.warn("swipe point1 is out of range", x1, y1);
x1 = std::clamp(x1, 0, m_width - 1);
y1 = std::clamp(y1, 0, m_height - 1);
}
const auto& opt = Config.get_options();
std::string duration_str =
duration <= 0 ? "" : std::to_string(static_cast<int>(duration * opt.adb_swipe_duration_multiplier));
std::string cur_cmd = utils::string_replace_all(
m_adb.swipe,
{
{ "[x1]", std::to_string(x1) },
{ "[y1]", std::to_string(y1) },
{ "[x2]", std::to_string(x2) },
{ "[y2]", std::to_string(y2) },
{ "[duration]", duration_str },
});
bool ret = call_command(cur_cmd).has_value();
// 额外的滑动adb有bug同样的参数偶尔会划得非常远。额外做一个短程滑动把之前的停下来
if (extra_swipe && opt.adb_extra_swipe_duration > 0) {
std::string extra_cmd = utils::string_replace_all(
m_adb.swipe,
{
{ "[x1]", std::to_string(x2) },
{ "[y1]", std::to_string(y2) },
{ "[x2]", std::to_string(x2) },
{ "[y2]", std::to_string(y2 - opt.adb_extra_swipe_dist /* * m_control_scale*/) },
{ "[duration]", std::to_string(opt.adb_extra_swipe_duration) },
});
ret &= call_command(extra_cmd).has_value();
}
return ret;
}
bool asst::AdbController::press_esc()
{
LogTraceFunction;
return call_command(m_adb.press_esc).has_value();
}
std::pair<int, int> asst::AdbController::get_screen_res() const noexcept
{
return m_screen_size;
}
void asst::AdbController::release()
{
close_socket();
if (m_kill_adb_on_exit && !m_adb.release.empty()) {
m_platform_io->release_adb(m_adb.release, 20000);
}
}
const std::string& asst::AdbController::get_uuid() const
{
return m_uuid;
}
bool asst::AdbController::convert_lf(std::string& data)
{
if (data.empty() || data.size() < 2) {
return false;
}
auto pred = [](const std::string::iterator& cur) -> bool {
return *cur == '\r' && *(cur + 1) == '\n';
};
// find the first of "\r\n"
auto first_iter = data.end();
for (auto iter = data.begin(); iter != data.end() - 1; ++iter) {
if (pred(iter)) {
first_iter = iter;
break;
}
}
if (first_iter == data.end()) {
return false;
}
// move forward all non-crlf elements
auto end_r1_iter = data.end() - 1;
auto next_iter = first_iter;
while (++first_iter != end_r1_iter) {
if (!pred(first_iter)) {
*next_iter = *first_iter;
++next_iter;
}
}
*next_iter = *end_r1_iter;
++next_iter;
data.erase(next_iter, data.end());
return true;
}
bool asst::AdbController::screencap(cv::Mat& image_payload, bool allow_reconnect)
{
using namespace std::chrono;
DecodeFunc decode_raw = [&](const std::string& data) -> bool {
if (data.size() < 8) {
return false;
}
// assuming little endian
uint32_t w = static_cast<uint32_t>(static_cast<unsigned char>(data[0])) << 0 |
static_cast<uint32_t>(static_cast<unsigned char>(data[1])) << 8 |
static_cast<uint32_t>(static_cast<unsigned char>(data[2])) << 16 |
static_cast<uint32_t>(static_cast<unsigned char>(data[3])) << 24;
uint32_t h = static_cast<uint32_t>(static_cast<unsigned char>(data[4])) << 0 |
static_cast<uint32_t>(static_cast<unsigned char>(data[5])) << 8 |
static_cast<uint32_t>(static_cast<unsigned char>(data[6])) << 16 |
static_cast<uint32_t>(static_cast<unsigned char>(data[7])) << 24;
if (int(w) != m_width || int(h) != m_height) {
Log.error("Size from image header", w, h, "does not match the size of screen", m_width, m_height);
return false;
}
size_t std_size = 4ULL * m_width * m_height;
if (data.size() < std_size) {
return false;
}
const size_t header_size = data.size() - std_size; // 12 or 16. ref:
// https://android.googlesource.com/platform/frameworks/base/+/26a2b97dbe48ee45e9ae70110714048f2f360f97%5E%21/cmds/screencap/screencap.cpp
auto img_data_beg = data.cbegin() + header_size;
cv::Mat temp(m_height, m_width, CV_8UC4, const_cast<char*>(&*img_data_beg));
if (temp.empty()) {
return false;
}
const auto& br = *(temp.end<cv::Vec4b>() - 1);
if (br[3] != 255) { // only check alpha
return false;
}
cv::cvtColor(temp, temp, cv::COLOR_RGBA2BGR);
image_payload = temp;
return true;
};
DecodeFunc decode_raw_with_gzip = [&](const std::string& data) -> bool {
const std::string raw_data = gzip::decompress(data.data(), data.size());
return decode_raw(raw_data);
};
DecodeFunc decode_encode = [&](const std::string& data) -> bool {
cv::Mat temp = cv::imdecode({ data.data(), int(data.size()) }, cv::IMREAD_COLOR);
if (temp.empty()) {
return false;
}
image_payload = temp;
return true;
};
image_payload = cv::Mat(); // 清空缓存
if (m_adb.screencap_method == AdbProperty::ScreencapMethod::UnknownYet) {
std::vector<std::pair<AdbProperty::ScreencapMethod, std::string>> all_methods_cost;
Log.info("Try to find the fastest way to screencap");
auto min_cost = milliseconds(LLONG_MAX);
clear_lf_info();
auto start_time = steady_clock::now();
if (m_support_socket && m_server_started &&
screencap(m_adb.screencap_raw_by_nc, decode_raw, allow_reconnect, true, 5000)) {
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time);
if (duration < min_cost) {
m_adb.screencap_method = AdbProperty::ScreencapMethod::RawByNc;
m_inited = true;
min_cost = duration;
}
Log.info("RawByNc cost", duration.count(), "ms");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::RawByNc, std::to_string(duration.count()));
}
else {
Log.info("RawByNc is not supported");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::RawByNc, "???");
}
clear_lf_info();
start_time = steady_clock::now();
if (screencap(m_adb.screencap_raw_with_gzip, decode_raw_with_gzip, allow_reconnect)) {
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time);
if (duration < min_cost) {
m_adb.screencap_method = AdbProperty::ScreencapMethod::RawWithGzip;
m_inited = true;
min_cost = duration;
}
Log.info("RawWithGzip cost", duration.count(), "ms");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::RawWithGzip, std::to_string(duration.count()));
}
else {
Log.info("RawWithGzip is not supported");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::RawWithGzip, "???");
}
clear_lf_info();
start_time = steady_clock::now();
if (screencap(m_adb.screencap_encode, decode_encode, allow_reconnect)) {
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time);
if (duration < min_cost) {
m_adb.screencap_method = AdbProperty::ScreencapMethod::Encode;
m_inited = true;
min_cost = duration;
}
Log.info("Encode cost", duration.count(), "ms");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::Encode, std::to_string(duration.count()));
}
else {
Log.info("Encode is not supported");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::Encode, "???");
}
#if ASST_WITH_EMULATOR_EXTRAS
if (m_mumu_extras.inited()) {
start_time = steady_clock::now();
if (m_mumu_extras.screencap()) {
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time);
if (duration < min_cost) {
m_adb.screencap_method = AdbProperty::ScreencapMethod::MumuExtras;
m_inited = true;
min_cost = duration;
}
Log.info("MumuExtras cost", duration.count(), "ms");
all_methods_cost.emplace_back(
AdbProperty::ScreencapMethod::MumuExtras,
std::to_string(duration.count()));
}
else {
Log.info("MumuExtras is not supported");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::MumuExtras, "???");
}
}
if (m_ld_extras.inited()) {
start_time = steady_clock::now();
if (m_ld_extras.screencap()) {
auto duration = duration_cast<milliseconds>(steady_clock::now() - start_time);
if (duration < min_cost) {
m_adb.screencap_method = AdbProperty::ScreencapMethod::LDExtras;
m_inited = true;
min_cost = duration;
}
Log.info("LDExtras cost", duration.count(), "ms");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::LDExtras, std::to_string(duration.count()));
}
else {
Log.info("LDExtras is not supported");
all_methods_cost.emplace_back(AdbProperty::ScreencapMethod::LDExtras, "???");
}
}
#endif
static const std::unordered_map<AdbProperty::ScreencapMethod, std::string> MethodName = {
{ AdbProperty::ScreencapMethod::UnknownYet, "UnknownYet" },
{ AdbProperty::ScreencapMethod::RawByNc, "RawByNc" },
{ AdbProperty::ScreencapMethod::RawWithGzip, "RawWithGzip" },
{ AdbProperty::ScreencapMethod::Encode, "Encode" },
#if ASST_WITH_EMULATOR_EXTRAS
{ AdbProperty::ScreencapMethod::MumuExtras, "MumuExtras" },
{ AdbProperty::ScreencapMethod::LDExtras, "LDExtras" },
#endif
};
Log.info("The fastest way is", MethodName.at(m_adb.screencap_method), ", cost:", min_cost.count(), "ms");
if (m_adb.screencap_method != AdbProperty::ScreencapMethod::UnknownYet) {
json::value info = json::object {
{ "uuid", m_uuid },
{ "what", "FastestWayToScreencap" },
{ "details",
json::object {
{ "method", MethodName.at(m_adb.screencap_method) },
{ "cost", min_cost.count() },
} },
};
json::array alt;
for (auto& [method, cost] : all_methods_cost) {
alt.push_back(json::object { { "method", MethodName.at(method) }, { "cost", cost } });
}
auto details_obj = info.at("details").as_object();
details_obj["alternatives"] = std::move(alt);
info.as_object()["details"] = std::move(details_obj);
callback(AsstMsg::ConnectionInfo, info);
}
clear_lf_info();
return m_adb.screencap_method != AdbProperty::ScreencapMethod::UnknownYet;
}
else {
auto start_time = high_resolution_clock::now();
bool screencap_ret = false;
switch (m_adb.screencap_method) {
case AdbProperty::ScreencapMethod::RawByNc:
screencap_ret = screencap(m_adb.screencap_raw_by_nc, decode_raw, allow_reconnect, true);
break;
case AdbProperty::ScreencapMethod::RawWithGzip:
screencap_ret = screencap(m_adb.screencap_raw_with_gzip, decode_raw_with_gzip, allow_reconnect);
break;
case AdbProperty::ScreencapMethod::Encode:
screencap_ret = screencap(m_adb.screencap_encode, decode_encode, allow_reconnect);
break;
#if ASST_WITH_EMULATOR_EXTRAS
case AdbProperty::ScreencapMethod::MumuExtras: {
auto img_opt = m_mumu_extras.screencap();
screencap_ret = img_opt.has_value();
if (!screencap_ret && allow_reconnect) {
m_mumu_extras.reload();
img_opt = m_mumu_extras.screencap();
screencap_ret = img_opt.has_value();
}
if (screencap_ret) {
image_payload = img_opt.value();
}
} break;
case AdbProperty::ScreencapMethod::LDExtras: {
auto img_opt = m_ld_extras.screencap();
screencap_ret = img_opt.has_value();
if (!screencap_ret && allow_reconnect) {
m_ld_extras.reload();
img_opt = m_ld_extras.screencap();
screencap_ret = img_opt.has_value();
}
if (screencap_ret) {
image_payload = img_opt.value();
}
} break;
#endif
default:
break;
}
auto duration = duration_cast<milliseconds>(high_resolution_clock::now() - start_time);
// 记录截图耗时每10次截图回传一次最值+平均值
m_screencap_cost.emplace_back(screencap_ret ? duration.count() : -1); // 记录截图耗时
++m_screencap_times;
if (m_screencap_cost.size() > 30) {
m_screencap_cost.pop_front();
}
if (m_screencap_times > 9) { // 每 10 次截图计算一次平均耗时
m_screencap_times = 0;
auto filtered_cost = m_screencap_cost | std::views::filter([](auto num) { return num > 0; });
if (filtered_cost.empty()) {
return screencap_ret;
}
// 过滤后的有效截图用时次数
auto filtered_count = m_screencap_cost.size() - std::ranges::count(m_screencap_cost, -1);
auto [screencap_cost_min, screencap_cost_max] = std::ranges::minmax(filtered_cost);
json::value info = json::object {
{ "uuid", m_uuid },
{ "what", "ScreencapCost" },
{ "details",
json::object {
{ "min", screencap_cost_min },
{ "max", screencap_cost_max },
{ "avg",
filtered_count > 0
? std::accumulate(filtered_cost.begin(), filtered_cost.end(), 0ll) / filtered_count
: -1 },
} },
};
if (m_screencap_cost.size() > filtered_count) {
info["details"]["fault_times"] = m_screencap_cost.size() - filtered_count;
}
callback(AsstMsg::ConnectionInfo, info);
}
return screencap_ret;
}
}
bool asst::AdbController::screencap(
const std::string& cmd,
const DecodeFunc& decode_func,
bool allow_reconnect,
bool by_socket,
int timeout)
{
if ((!m_support_socket || !m_server_started) && by_socket) [[unlikely]] {
return false;
}
auto ret = call_command(cmd, timeout, allow_reconnect, by_socket);
if (!ret || ret.value().empty()) [[unlikely]] {
Log.warn("data is empty!");
return false;
}
auto& data = ret.value();
bool tried_conversion = false;
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::CRLF) {
tried_conversion = true;
if (!convert_lf(data)) [[unlikely]] { // 没找到 "\r\n"
Log.info("screencap_end_of_line is set to CRLF but no `\\r\\n` found, set it to LF");
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::LF;
}
}
if (decode_func(data)) [[likely]] {
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::UnknownYet) [[unlikely]] {
Log.info("screencap_end_of_line is LF");
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::LF;
}
}
else {
Log.info("data is not empty, but image is empty");
if (tried_conversion) { // 已经转换过行尾,再次转换 data 不会变化,不必重试
Log.error("skip retry decoding and decode failed!");
return false;
}
Log.info("try to cvt lf");
if (!convert_lf(data)) { // 没找到 "\r\n"data 没有变化,不必重试
Log.error("no `\\r\\n` found, skip retry decode");
return false;
}
if (!decode_func(data)) {
Log.error("convert lf and retry decode failed!");
return false;
}
if (m_adb.screencap_end_of_line == AdbProperty::ScreencapEndOfLine::UnknownYet) {
Log.info("screencap_end_of_line is CRLF");
}
else {
Log.info("screencap_end_of_line is changed to CRLF");
}
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::CRLF;
}
return true;
}
bool asst::AdbController::connect(const std::string& adb_path, const std::string& address, const std::string& config)
{
LogTraceFunction;
clear_info();
#ifdef ASST_DEBUG
if (config == "DEBUG") {
m_inited = true;
return true;
}
#endif
auto get_info_json = [&]() -> json::value {
return json::object {
{ "uuid", m_uuid },
{ "details",
json::object {
{ "adb", adb_path },
{ "address", address },
{ "config", config },
} },
};
};
std::string client_type = ctrler() ? ctrler()->get_client_type() : "";
m_conn_ctx.reset(adb_path, address, client_type);
auto adb_ret = Config.get_adb_cfg(config);
if (!adb_ret) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "ConfigNotFound" },
};
callback(AsstMsg::ConnectionInfo, info);
#ifdef ASST_DEBUG
return false;
#else
Log.error("config ", config, "not found");
adb_ret = Config.get_adb_cfg("General");
#endif
}
m_conn_ctx.adb_cfg = adb_ret.value();
const auto& adb_cfg = m_conn_ctx.adb_cfg;
if (need_exit()) {
return false;
}
/* connect */
{
// 先用 devices 读取输出
m_adb.devices = m_conn_ctx.replace_cmd(adb_cfg.devices);
m_adb.address_regex = m_conn_ctx.replace_cmd(adb_cfg.address_regex);
auto devices_ret = call_command(m_adb.devices, 60LL * 1000, false);
bool need_connect = true;
if (devices_ret) {
const auto& devices_str = devices_ret.value();
const boost::regex address_regex(m_adb.address_regex);
for (boost::sregex_iterator iter(devices_str.begin(), devices_str.end(), address_regex), end; iter != end;
++iter) {
if (iter->size() > 1 && iter->str(1) == address) {
need_connect = false;
break;
}
}
}
// 设置配置 connect、release 命令,即使这里不连接,后续也会需要用到
m_adb.connect = m_conn_ctx.replace_cmd(adb_cfg.connect);
m_adb.release = m_conn_ctx.replace_cmd(adb_cfg.release);
m_platform_io->set_adb_serial(address);
if (need_connect) {
// 如果不包含 `:` 且需要连接connect 命令也不会成功
if (address.find(':') == std::string::npos) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Cannot connect: address appears to be serial number but device not found" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
auto connect_ret = call_command(m_adb.connect, 60LL * 1000, false /* adb 连接时不允许重试 */);
if (connect_ret) {
auto& connect_str = connect_ret.value();
// 检查连接字符串是否包含 "connected"
if (connect_str.find("connected") == std::string::npos) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Connection command did not report \"connected\"" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
}
else {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Connection command failed to exec" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
}
}
if (need_exit()) {
return false;
}
/* get uuid (imei) */
{
auto uuid_ret = call_command(m_conn_ctx.replace_cmd(adb_cfg.uuid), 20000, false /* adb 连接时不允许重试 */);
if (!uuid_ret) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Uuid command failed to exec" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
std::string raw_output = std::move(uuid_ret.value());
std::string uuid_str = raw_output;
std::erase_if(uuid_str, [](char c) { return !std::isdigit(c) && !std::isalpha(c); });
boost::regex hex_regex("^[0-9a-fA-F]{8,}$");
if (uuid_str.empty() || !boost::regex_match(uuid_str, hex_regex)) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Uuid invalid or shell command not available" },
};
info["details"]["raw_output"] = std::move(raw_output);
callback(AsstMsg::ConnectionInfo, info);
return false;
}
m_uuid = std::move(uuid_str);
json::value info = get_info_json() | json::object {
{ "what", "UuidGot" },
{ "why", "" },
};
info["details"]["uuid"] = m_uuid;
callback(AsstMsg::ConnectionInfo, info);
}
/* get android version */
{
auto version_ret = call_command(m_conn_ctx.replace_cmd(adb_cfg.version));
if (!version_ret) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Android version command failed to exec" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
std::string raw_output = version_ret.value();
convert_lf(raw_output);
std::erase_if(raw_output, [](char c) { return !std::isalnum(static_cast<unsigned char>(c)); });
bool all_digit = !raw_output.empty() && std::ranges::all_of(raw_output, [](char c) {
return std::isdigit(static_cast<unsigned char>(c));
});
if (!all_digit) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Android version invalid or shell command not available" },
};
info["details"]["raw_output"] = std::move(version_ret.value());
callback(AsstMsg::ConnectionInfo, info);
return false;
}
m_version = std::stoul(raw_output);
}
if (need_exit()) {
return false;
}
// 按需获取display ID 信息
if (!adb_cfg.display_id.empty()) {
// [PackageName] 不参与 replace_cmd 的统一替换(保留为运行时参数)。
// 只有 connect 阶段命令确实依赖包名的配置,才需要在连接前调用
// AsstSetInstanceOption(ClientType, ...)。当前内置配置仅 Androws / WSA 的
// displayId 查询走这里,因此其他连接配置不应被迫传入 client type。
auto display_id_cmd = utils::string_replace_all(
m_conn_ctx.replace_cmd(adb_cfg.display_id),
"[PackageName]",
m_conn_ctx.package_name);
auto display_id_ret = call_command(display_id_cmd);
if (!display_id_ret) {
return false;
}
auto& display_id_pipe_str = display_id_ret.value();
convert_lf(display_id_pipe_str);
auto last = display_id_pipe_str.rfind(':');
if (last == std::string::npos) {
return false;
}
m_conn_ctx.display_id = display_id_pipe_str.substr(last + 1);
// 去掉换行
m_conn_ctx.display_id.pop_back();
}
if (need_exit()) {
return false;
}
// 按需获取 event ID 信息(用于 minitouch 的 /dev/input/eventN
if (!adb_cfg.event_id.empty()) {
auto event_id_ret = call_command(m_conn_ctx.replace_cmd(adb_cfg.event_id));
if (!event_id_ret) {
Log.warn("Failed to get event_id, skip");
}
else {
auto& event_id_pipe_str = event_id_ret.value();
convert_lf(event_id_pipe_str);
// 去掉空白字符
std::erase_if(event_id_pipe_str, [](char c) { return !std::isdigit(c); });
if (event_id_pipe_str.empty()) {
Log.warn("event_id is empty, skip");
}
else {
m_conn_ctx.event_id = event_id_pipe_str;
Log.info("event_id:", m_conn_ctx.event_id);
}
}
}
if (need_exit()) {
return false;
}
/* display */
{
auto display_ret = call_command(m_conn_ctx.replace_cmd(adb_cfg.display));
if (!display_ret) {
json::value info = get_info_json() | json::object {
{ "what", "ConnectFailed" },
{ "why", "Display command failed to exec" },
};
callback(AsstMsg::ConnectionInfo, info);
return false;
}
std::stringstream display_ss(display_ret.value());
int size_value1 = 0;
int size_value2 = 0;
display_ss >> size_value1 >> size_value2;
m_width = (std::max)(size_value1, size_value2);
m_height = (std::min)(size_value1, size_value2);
json::value info = get_info_json() | json::object {
{ "what", "ResolutionGot" },
{ "why", "" },
};
info["details"] |= json::object {
{ "width", m_width },
{ "height", m_height },
};
callback(AsstMsg::ConnectionInfo, info);
if (m_width == 0 || m_height == 0) {
info["what"] = "ResolutionError";
info["why"] = "Get resolution failed";
callback(AsstMsg::ConnectionInfo, info);
return false;
}
m_screen_size = { m_width, m_height };
}
if (need_exit()) {
return false;
}
{
json::value info = get_info_json() | json::object {
{ "what", "Connected" },
{ "why", "" },
};
callback(AsstMsg::ConnectionInfo, info);
}
m_adb.click = m_conn_ctx.replace_cmd(adb_cfg.click);
m_adb.input = m_conn_ctx.replace_cmd(adb_cfg.input);
m_adb.swipe = m_conn_ctx.replace_cmd(adb_cfg.swipe);
m_adb.press_esc = m_conn_ctx.replace_cmd(adb_cfg.press_esc);
m_adb.screencap_raw_with_gzip = m_conn_ctx.replace_cmd(adb_cfg.screencap_raw_with_gzip);
m_adb.screencap_encode = m_conn_ctx.replace_cmd(adb_cfg.screencap_encode);
m_adb.start = m_conn_ctx.replace_cmd(adb_cfg.start);
m_adb.stop = m_conn_ctx.replace_cmd(adb_cfg.stop);
m_adb.back_to_home = m_conn_ctx.replace_cmd(adb_cfg.back_to_home);
if (m_support_socket && !m_server_started) {
std::string bind_address;
if (size_t pos = address.rfind(':'); pos != std::string::npos) {
bind_address = address.substr(0, pos);
}
else {
bind_address = "127.0.0.1";
}
// reference from
// https://github.com/ArknightsAutoHelper/ArknightsAutoHelper/blob/master/automator/connector/ADBConnector.py#L436
auto nc_address_ret = call_command(m_conn_ctx.replace_cmd(adb_cfg.nc_address));
if (nc_address_ret && !m_server_started) {
auto& nc_result_str = nc_address_ret.value();
if (auto pos = nc_result_str.find(' '); pos != std::string::npos) {
m_conn_ctx.nc_address = nc_result_str.substr(0, pos);
}
}
auto socket_opt = init_socket(bind_address);
if (socket_opt) {
m_conn_ctx.nc_port = socket_opt.value();
m_adb.screencap_raw_by_nc = m_conn_ctx.replace_cmd(adb_cfg.screencap_raw_by_nc);
m_server_started = true;
}
else {
m_server_started = false;
}
}
if (need_exit()) {
return false;
}
if (config == "MuMuEmulator12") {
init_mumu_extras(adb_cfg, address);
}
else if (config == "LDPlayer") {
init_ld_extras(adb_cfg, address);
}
if (need_exit()) {
return false;
}
return true;
}
void asst::AdbController::set_kill_adb_on_exit(bool enable) noexcept
{
m_kill_adb_on_exit = enable;
}
void asst::AdbController::clear_lf_info()
{
m_adb.screencap_end_of_line = AdbProperty::ScreencapEndOfLine::UnknownYet;
}
void asst::AdbController::back_to_home() noexcept
{
call_command(m_adb.back_to_home);
return;
}