#include "WifiAdbManager.h" #include #include #include #include #include #include #include #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(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 g_failCount; // androidId → неудач подряд QHash 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 WifiAdbManager::loadEndpoints() { QMap 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 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"); }