300 lines
14 KiB
C++
300 lines
14 KiB
C++
#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");
|
||
}
|