1
0
forked from BRT/arc
arc/android/adb/WifiAdbManager.cpp

300 lines
14 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 "WifiAdbManager.h"
#include <QProcess>
#include <QRegularExpression>
#include <QtConcurrent>
#include <QTcpSocket>
#include <QDateTime>
#include <QHash>
#include <QDebug>
#include "adb/AdbUtils.h"
#include "dao/WifiConnectionDAO.h"
namespace {
// Быстрая TCP-проба достижимости эндпоинта МИМО adb: `adb connect` к мёртвому хосту
// висит до 10с, поэтому сперва открываем сокет с коротким таймаутом. Без event-loop —
// безопасно из фонового потока (как AdbUtils::httpGet).
constexpr int kReachProbeMs = 600;
bool tcpReachable(const QString &ip, int port, int timeoutMs) {
if (ip.isEmpty() || port <= 0) {
return false;
}
QTcpSocket sock;
sock.connectToHost(ip, static_cast<quint16>(port));
const bool ok = sock.waitForConnected(timeoutMs);
sock.abort();
return ok;
}
// Пер-эндпоинт backoff: офлайн-устройство не долбим каждый проход 10-секундными
// connect'ами. Задержка растёт 30→60→120→240→300с (потолок) по числу неудач подряд,
// сбрасывается при успехе/онлайне. Состояние живёт только в потоке DeviceScreener
// (reconnectKnown/kickEndpoint зовутся лишь оттуда) — без мьютекса.
constexpr qint64 kBackoffBaseSecs = 30;
constexpr qint64 kBackoffMaxSecs = 300;
QHash<QString, int> g_failCount; // androidId → неудач подряд
QHash<QString, qint64> g_nextRetryAt; // androidId → unix ts, до которого пропускаем
void noteFailure(const QString &androidId) {
const int n = ++g_failCount[androidId];
const qint64 delay = qMin(kBackoffBaseSecs * (1LL << qMin(n - 1, 4)), kBackoffMaxSecs);
g_nextRetryAt[androidId] = QDateTime::currentSecsSinceEpoch() + delay;
}
void noteSuccess(const QString &androidId) {
g_failCount.remove(androidId);
g_nextRetryAt.remove(androidId);
}
} // namespace
QMap<QString, WifiEndpoint> WifiAdbManager::loadEndpoints() {
QMap<QString, WifiEndpoint> result;
for (const WifiConnection &wc : WifiConnectionDAO::getAll()) {
if (wc.androidId.isEmpty() || wc.ip.isEmpty() || wc.port <= 0) {
continue;
}
WifiEndpoint ep;
ep.endpoint = wc.ip + ":" + QString::number(wc.port);
ep.mac = wc.mac;
result.insert(wc.androidId, ep);
}
return result;
}
void WifiAdbManager::saveEndpoint(const QString &androidId, const QString &endpoint, const QString &mac) {
if (androidId.isEmpty() || endpoint.isEmpty()) {
return;
}
const WifiConnection existing = WifiConnectionDAO::getByAndroidId(androidId);
// MAC мог не прочитаться при этом вызове — тогда не затираем уже сохранённый.
const QString mergedMac = !mac.isEmpty() ? mac : existing.mac;
const QString ip = ipOf(endpoint);
const QString port = portOf(endpoint);
if (ip.isEmpty() || port.isEmpty()) {
return;
}
const QString existingEndpoint = existing.ip.isEmpty()
? QString()
: existing.ip + ":" + QString::number(existing.port);
if (existingEndpoint == endpoint && existing.mac == mergedMac) {
return; // ничего не изменилось — не дёргаем БД
}
// code не трогаем (задел под pairing) — WifiAdbManager его не использует, пишем "".
if (WifiConnectionDAO::upsert(androidId, ip, port.toInt(), {}, mergedMac)) {
qInfo() << "[WifiAdbManager] saved endpoint" << endpoint << "mac" << mergedMac
<< "for device" << androidId;
}
}
int WifiAdbManager::reconnectKnown(const ReconnectOptions &opts) {
const QMap<QString, WifiEndpoint> endpoints = loadEndpoints();
const qint64 now = QDateTime::currentSecsSinceEpoch();
int connected = 0;
for (auto it = endpoints.constBegin(); it != endpoints.constEnd(); ++it) {
const QString &androidId = it.key();
const WifiEndpoint &ep = it.value();
// Устройство уже онлайн (по USB или по этому же Wi-Fi-эндпоинту) — не дёргаем,
// сбрасываем счётчик неудач (вернулось — backoff больше не нужен).
if (opts.alreadyOnline.contains(androidId)) {
noteSuccess(androidId);
continue;
}
// п.2 — пер-эндпоинт backoff: мёртвый девайс не долбим каждый проход.
if (now < g_nextRetryAt.value(androidId, 0)) {
continue;
}
// п.1 — быстрая TCP-проба сохранённого ip:port вместо 10-секундного adb connect
// к мёртвому хосту. Достижим → пробуем реальный connect (с disconnect для stale).
bool ok = false;
if (tcpReachable(ipOf(ep.endpoint), portOf(ep.endpoint).toInt(), kReachProbeMs)) {
// Протухший offline-эндпоинт: adb держит запись, но канал мёртв. Голый connect
// вернул бы "already connected" — сначала disconnect, затем чистый connect.
if (opts.staleEndpoints.contains(ep.endpoint)) {
AdbUtils::disconnectWireless(ep.endpoint);
}
ok = AdbUtils::connectWireless(ep.endpoint);
}
// Сохранённый IP недостижим/connect не поднялся — возможно, адрес сменился по DHCP.
// Если знаем MAC, ищем устройство в сети по нему и переподключаемся на новый IP.
if (!ok && opts.macRediscover && !ep.mac.isEmpty()) {
const QString newIp = findIpByMac(ep.mac, ipOf(ep.endpoint), opts.subnetSweep);
if (!newIp.isEmpty()) {
const QString newEndpoint = newIp + ":" + portOf(ep.endpoint);
if (newEndpoint != ep.endpoint
&& tcpReachable(newIp, portOf(ep.endpoint).toInt(), kReachProbeMs)
&& AdbUtils::connectWireless(newEndpoint)) {
saveEndpoint(androidId, newEndpoint, ep.mac);
qInfo() << "[WifiAdbManager] device" << androidId << "moved"
<< ep.endpoint << "" << newEndpoint << "(found by MAC)";
ok = true;
}
}
}
if (ok) {
noteSuccess(androidId);
++connected;
} else {
// Неудача не критична (устройство выключено / потеряло tcpip после ребута):
// эндпоинт оставляем, наращиваем backoff, чтобы не блокировать цикл опроса.
noteFailure(androidId);
}
}
return connected;
}
bool WifiAdbManager::kickEndpoint(const QString &androidId) {
if (androidId.isEmpty()) {
return false;
}
const WifiConnection wc = WifiConnectionDAO::getByAndroidId(androidId);
if (wc.ip.isEmpty() || wc.port <= 0) {
return false; // нет Wi-Fi-эндпоинта — офлайн не по нашей части (например, USB)
}
const QString endpoint = wc.ip + ":" + QString::number(wc.port);
// Быстрая проба (п.1): если порт недостижим — пересоздавать нечего, не вешаем цикл
// на 10с adb disconnect/connect. У «залипшего» эндпоинта adbd-порт обычно ещё слушает.
if (!tcpReachable(wc.ip, wc.port, kReachProbeMs)) {
return false;
}
// Полумёртвое соединение adb держит как "device", но команды виснут; голый connect
// вернул бы "already connected" и не вылечил. Принудительно пересоздаём TCP.
AdbUtils::disconnectWireless(endpoint);
const bool ok = AdbUtils::connectWireless(endpoint);
qInfo() << "[WifiAdbManager] kicked wedged endpoint" << endpoint
<< "for device" << androidId << "" << (ok ? "reconnected" : "still down");
return ok;
}
QString WifiAdbManager::tryBootstrap(const QString &usbSerial, const QString &androidId, const int port) {
if (usbSerial.isEmpty() || androidId.isEmpty() || port <= 0) {
return {};
}
// Сначала узнаём IP по Wi-Fi: если устройство не в Wi-Fi — нет смысла дёргать
// tcpip (он бы зря порвал adbd). Делаем это ДО tcpip.
const QString ip = AdbUtils::getDeviceWifiIp(usbSerial);
if (ip.isEmpty()) {
qInfo() << "[WifiAdbManager] device" << androidId << "(" << usbSerial
<< ") has no wlan0 IP — skip Wi-Fi bootstrap";
return {};
}
const QString endpoint = ip + ":" + QString::number(port);
// MAC снимаем по USB (до tcpip) — пригодится для поиска устройства при смене IP.
const QString mac = AdbUtils::getDeviceMac(usbSerial);
// Переключаем adbd в TCP-режим (рвёт текущее соединение — вызывающий гарантирует,
// что устройство простаивает) и подключаемся по сети.
if (!AdbUtils::enableTcpip(usbSerial, port)) {
return {};
}
if (!AdbUtils::connectWireless(endpoint)) {
return {};
}
saveEndpoint(androidId, endpoint, mac);
qInfo() << "[WifiAdbManager] Wi-Fi endpoint up for device" << androidId << "" << endpoint;
return endpoint;
}
void WifiAdbManager::forget(const QString &androidId) {
if (androidId.isEmpty()) {
return;
}
const WifiConnection wc = WifiConnectionDAO::getByAndroidId(androidId);
if (wc.ip.isEmpty() || wc.port <= 0) {
return;
}
AdbUtils::disconnectWireless(wc.ip + ":" + QString::number(wc.port));
WifiConnectionDAO::remove(androidId);
qInfo() << "[WifiAdbManager] forgot Wi-Fi endpoint for device" << androidId;
}
QString WifiAdbManager::findIpByMac(const QString &mac, const QString &subnetHintIp, const bool allowSweep) {
const QString norm = normalizeMac(mac);
if (norm.isEmpty()) {
return {};
}
// Быстрый путь: устройство уже есть в ARP-таблице ОС (недавно общались).
QString ip = arpLookupIp(norm);
if (!ip.isEmpty()) {
return ip;
}
// ARP-таблица не содержит устройство — опционально прогреваем её ping-сканом
// подсети /24 (медленно и шумно, поэтому только по явному флагу), затем повторяем.
if (allowSweep && !subnetHintIp.isEmpty()) {
pingSweepSubnet(subnetHintIp);
ip = arpLookupIp(norm);
}
return ip;
}
QString WifiAdbManager::arpLookupIp(const QString &normalizedMac) {
QProcess arp;
arp.start("arp", {"-a"});
if (!arp.waitForStarted(3000)) {
qWarning() << "[WifiAdbManager] failed to start arp:" << arp.errorString();
return {};
}
arp.waitForFinished(8000);
const QString out = QString::fromLocal8Bit(arp.readAllStandardOutput());
// Строки вида (Windows): " 192.168.1.50 aa-bb-cc-dd-ee-ff dynamic"
// (unix): "? (192.168.1.50) at aa:bb:cc:dd:ee:ff [ether] on en0"
static const QRegularExpression reRow(
R"((\d{1,3}(?:\.\d{1,3}){3})\D+?([0-9a-fA-F]{2}(?:[-:][0-9a-fA-F]{2}){5}))");
auto it = reRow.globalMatch(out);
while (it.hasNext()) {
const auto m = it.next();
if (normalizeMac(m.captured(2)) == normalizedMac) {
return m.captured(1);
}
}
return {};
}
void WifiAdbManager::pingSweepSubnet(const QString &subnetHintIp) {
// /24 из hint-IP: 192.168.1.X → пинги по 192.168.1.{1..254}.
const int lastDot = subnetHintIp.lastIndexOf('.');
if (lastDot <= 0) {
return;
}
const QString prefix = subnetHintIp.left(lastDot + 1);
QStringList hosts;
for (int i = 1; i <= 254; ++i) {
hosts << prefix + QString::number(i);
}
qInfo() << "[WifiAdbManager] ARP warm-up: ping sweep of" << prefix + "0/24";
// Параллельно (глобальный пул QtConcurrent), чтобы не блокировать цикл надолго.
// Ответ нам не важен — нужно лишь, чтобы ОС записала MAC соседей в ARP-таблицу.
QtConcurrent::blockingMap(hosts, [](const QString &host) {
QProcess p;
#ifdef Q_OS_WIN
p.start("ping", {"-n", "1", "-w", "300", host});
#else
p.start("ping", {"-c", "1", "-W", "1", host});
#endif
if (p.waitForStarted(2000)) {
p.waitForFinished(2000);
}
});
}
QString WifiAdbManager::normalizeMac(const QString &mac) {
QString m = mac.trimmed().toLower();
m.replace('-', ':');
return m;
}
QString WifiAdbManager::ipOf(const QString &endpoint) {
const int c = endpoint.lastIndexOf(':');
return c > 0 ? endpoint.left(c) : endpoint;
}
QString WifiAdbManager::portOf(const QString &endpoint) {
const int c = endpoint.lastIndexOf(':');
return c > 0 ? endpoint.mid(c + 1) : QStringLiteral("5555");
}