diff --git a/.gitignore b/.gitignore index 11af80d0..0845592f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # Some files +compile_commands.json .DS_Store *.pro.user *.user diff --git a/.gitmodules b/.gitmodules index 504174b6..744f6505 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,9 @@ [submodule "libs/QJsonStruct"] path = libs/QJsonStruct url = https://github.com/Qv2ray/QJsonStruct +[submodule "3rdparty/libuv"] + path = 3rdparty/libuv + url = https://github.com/libuv/libuv.git +[submodule "3rdparty/uvw"] + path = 3rdparty/uvw + url = https://github.com/DuckVador/uvw.git diff --git a/3rdparty/libuv b/3rdparty/libuv new file mode 160000 index 00000000..1ab9ea37 --- /dev/null +++ b/3rdparty/libuv @@ -0,0 +1 @@ +Subproject commit 1ab9ea3790378f9f25c4e78e9e2b511c75f9c9ed diff --git a/3rdparty/uvw b/3rdparty/uvw new file mode 160000 index 00000000..4c414f19 --- /dev/null +++ b/3rdparty/uvw @@ -0,0 +1 @@ +Subproject commit 4c414f19a234003727766cf41d1f10ea7ed8679e diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e8f714c..2f3f8a5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,11 @@ list(GET VERSION_LIST 0 CMAKE_PROJECT_VERSION_MAJOR) list(GET VERSION_LIST 1 CMAKE_PROJECT_VERSION_MINOR) list(GET VERSION_LIST 2 CPACK_PACKAGE_VERSION_PATCH) +# Tweaks and other defaults +# Setting CMAKE to use loose block and search for find modules in source directory +set ( CMAKE_ALLOW_LOOSE_LOOP_CONSTRUCTS true ) +set ( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH} ) + # For Windows RC file. add_definitions(-DQV2RAY_VERSION_MAJOR=${CMAKE_PROJECT_VERSION_MAJOR}) add_definitions(-DQV2RAY_VERSION_MINOR=${CMAKE_PROJECT_VERSION_MINOR}) @@ -80,7 +85,7 @@ message(" ") if(WIN32) add_compile_options("/std:c++17") - add_definitions(-DUNICODE -D_UNICODE) + add_definitions(-DUNICODE -D_UNICODE -DNOMINMAX) add_definitions(-D_WIN32_WINNT=0x600 -D_SCL_SECURE_NO_WARNINGS -D_CRT_SECURE_NO_WARNINGS) set(GUI_TYPE WIN32) if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) @@ -140,6 +145,7 @@ endif() # ================================================================================== # 3rdparty Sources # ================================================================================== +include(cmake/libuv.cmake) include(cmake/translations.cmake) include(cmake/qnodeeditor.cmake) if (ANDROID) @@ -238,6 +244,7 @@ target_link_libraries(qv2ray-baselib ${ZXING_LIBRARY} Threads::Threads ${QV2RAY_QT_LIBS} + ${LibUV_LIBRARIES} ) target_link_libraries(qv2ray @@ -261,6 +268,7 @@ target_include_directories(qv2ray-baselib PUBLIC ${CMAKE_BINARY_DIR} ${ZXING_INCLUDE_PATH} ${Protobuf_INCLUDE_DIRS} + ${LibUV_INCLUDE_DIR} ) if (BUILD_TESTING) diff --git a/cmake/FindLibUV.cmake b/cmake/FindLibUV.cmake new file mode 100644 index 00000000..2745a751 --- /dev/null +++ b/cmake/FindLibUV.cmake @@ -0,0 +1,139 @@ +#[=======================================================================[.rst: +FindLibUV +--------- + +Find libuv includes and library. + +Imported Targets +^^^^^^^^^^^^^^^^ + +An :ref:`imported target ` named +``LibUV::LibUV`` is provided if libuv has been found. + +Result Variables +^^^^^^^^^^^^^^^^ + +This module defines the following variables: + +``LibUV_FOUND`` + True if libuv was found, false otherwise. +``LibUV_INCLUDE_DIRS`` + Include directories needed to include libuv headers. +``LibUV_LIBRARIES`` + Libraries needed to link to libuv. +``LibUV_VERSION`` + The version of libuv found. +``LibUV_VERSION_MAJOR`` + The major version of libuv. +``LibUV_VERSION_MINOR`` + The minor version of libuv. +``LibUV_VERSION_PATCH`` + The patch version of libuv. + +Cache Variables +^^^^^^^^^^^^^^^ + +This module uses the following cache variables: + +``LibUV_LIBRARY`` + The location of the libuv library file. +``LibUV_INCLUDE_DIR`` + The location of the libuv include directory containing ``uv.h``. + +The cache variables should not be used by project code. +They may be set by end users to point at libuv components. +#]=======================================================================] + +#============================================================================= +# Copyright 2014-2016 Kitware, Inc. +# +# Distributed under the OSI-approved BSD License (the "License"); +# see accompanying file Copyright.txt for details. +# +# This software is distributed WITHOUT ANY WARRANTY; without even the +# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +# See the License for more information. +#============================================================================= +# (To distribute this file outside of CMake, substitute the full +# License text for the above reference.) + +#----------------------------------------------------------------------------- +if(STATIC_LINK_LIBUV) + find_library(LibUV_LIBRARY + NAMES libuv.a + ) +else() + find_library(LibUV_LIBRARY + NAMES uv + ) +endif() +mark_as_advanced(LibUV_LIBRARY) + +find_path(LibUV_INCLUDE_DIR + NAMES uv.h + ) +mark_as_advanced(LibUV_INCLUDE_DIR) + +#----------------------------------------------------------------------------- +# Extract version number if possible. +set(_LibUV_H_REGEX "#[ \t]*define[ \t]+UV_VERSION_(MAJOR|MINOR|PATCH)[ \t]+[0-9]+") +if(LibUV_INCLUDE_DIR AND EXISTS "${LibUV_INCLUDE_DIR}/uv-version.h") + file(STRINGS "${LibUV_INCLUDE_DIR}/uv-version.h" _LibUV_H REGEX "${_LibUV_H_REGEX}") +elseif(LibUV_INCLUDE_DIR AND EXISTS "${LibUV_INCLUDE_DIR}/uv.h") + file(STRINGS "${LibUV_INCLUDE_DIR}/uv.h" _LibUV_H REGEX "${_LibUV_H_REGEX}") +else() + set(_LibUV_H "") +endif() +foreach(c MAJOR MINOR PATCH) + if(_LibUV_H MATCHES "#[ \t]*define[ \t]+UV_VERSION_${c}[ \t]+([0-9]+)") + set(_LibUV_VERSION_${c} "${CMAKE_MATCH_1}") + else() + unset(_LibUV_VERSION_${c}) + endif() +endforeach() +if(DEFINED _LibUV_VERSION_MAJOR AND DEFINED _LibUV_VERSION_MINOR) + set(LibUV_VERSION_MAJOR "${_LibUV_VERSION_MAJOR}") + set(LibUV_VERSION_MINOR "${_LibUV_VERSION_MINOR}") + set(LibUV_VERSION "${LibUV_VERSION_MAJOR}.${LibUV_VERSION_MINOR}") + if(DEFINED _LibUV_VERSION_PATCH) + set(LibUV_VERSION_PATCH "${_LibUV_VERSION_PATCH}") + set(LibUV_VERSION "${LibUV_VERSION}.${LibUV_VERSION_PATCH}") + else() + unset(LibUV_VERSION_PATCH) + endif() +else() + set(LibUV_VERSION_MAJOR "") + set(LibUV_VERSION_MINOR "") + set(LibUV_VERSION_PATCH "") + set(LibUV_VERSION "") +endif() +unset(_LibUV_VERSION_MAJOR) +unset(_LibUV_VERSION_MINOR) +unset(_LibUV_VERSION_PATCH) +unset(_LibUV_H_REGEX) +unset(_LibUV_H) + +#----------------------------------------------------------------------------- +include(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibUV + FOUND_VAR LibUV_FOUND + REQUIRED_VARS LibUV_LIBRARY LibUV_INCLUDE_DIR + VERSION_VAR LibUV_VERSION + ) +set(LIBUV_FOUND ${LibUV_FOUND}) + +#----------------------------------------------------------------------------- +# Provide documented result variables and targets. +if(LibUV_FOUND) + set(LibUV_INCLUDE_DIRS ${LibUV_INCLUDE_DIR}) + set(LibUV_LIBRARIES ${LibUV_LIBRARY}) + message("libuv library:" ${LibUV_LIBRARY}) + if(NOT TARGET LibUV::LibUV) + add_library(LibUV::LibUV UNKNOWN IMPORTED) + set_target_properties(LibUV::LibUV PROPERTIES + IMPORTED_LOCATION "${LibUV_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${LibUV_INCLUDE_DIRS}" + ) + endif() +endif() + diff --git a/cmake/components/qv2ray-lib.cmake b/cmake/components/qv2ray-lib.cmake index 4e25b09c..c0863690 100644 --- a/cmake/components/qv2ray-lib.cmake +++ b/cmake/components/qv2ray-lib.cmake @@ -37,6 +37,7 @@ set(QV2RAY_LIB_SOURCES ${CMAKE_SOURCE_DIR}/src/components/latency/TCPing.cpp ${CMAKE_SOURCE_DIR}/src/components/latency/TCPing.hpp ${CMAKE_SOURCE_DIR}/src/components/latency/win/ICMPPing.cpp + ${CMAKE_SOURCE_DIR}/src/components/latency/win/ICMPPingWork.cpp ${CMAKE_SOURCE_DIR}/src/components/latency/win/ICMPPing.hpp ${CMAKE_SOURCE_DIR}/src/components/latency/unix/ICMPPing.cpp ${CMAKE_SOURCE_DIR}/src/components/latency/unix/ICMPPing.hpp diff --git a/cmake/libuv.cmake b/cmake/libuv.cmake new file mode 100644 index 00000000..d9ce6505 --- /dev/null +++ b/cmake/libuv.cmake @@ -0,0 +1,53 @@ +option(USE_SYSTEM_LIBUV "use system libuv" OFF) +set(UVW_SOURCES + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/async.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/check.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/dns.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/emitter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/fs.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/fs_event.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/fs_poll.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/idle.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/lib.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/loop.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/pipe.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/poll.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/prepare.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/process.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/signal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/stream.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/tcp.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/thread.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/timer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/tty.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/util.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/work.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src/uvw/udp.cpp +) +set(UVW_INCLUDE_DIR + ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/uvw/src + ) +if(NOT USE_SYSTEM_LIBUV) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libuv EXCLUDE_FROM_ALL) + set_target_properties(uv PROPERTIES EXCLUDE_FROM_ALL TRUE) + set_target_properties(uv_a PROPERTIES POSITION_INDEPENDENT_CODE 1) + add_library(uv::uv-static ALIAS uv_a) + set(LibUV_INCLUDE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rdparty/libuv/include + ) + add_library(uv::uv-static ALIAS uv_a) + set_target_properties(uv_a PROPERTIES POSITION_INDEPENDENT_CODE 1) + set(LibUV_LIBRARIES uv::uv-static) +else() + if(NOT WIN32) + find_package(LibUV REQUIRED) + else() + find_package(unofficial-libuv CONFIG REQUIRED) + set(${LibUV_LIBRARIES} unofficial::libuv::libuv) + endif() +endif() +add_library(UVW_LIB ${UVW_SOURCES}) +target_compile_definitions(UVW_LIB PUBLIC UVW_AS_LIB) +target_include_directories(UVW_LIB PUBLIC ${LibUV_INCLUDE_DIR}) +target_link_libraries(UVW_LIB ${LibUV_LIBRARIES}) +set(LibUV_INCLUDE_DIR ${UVW_INCLUDE_DIR} ${LibUV_INCLUDE_DIR}) +set(LibUV_LIBRARIES UVW_LIB ${LibUV_LIBRARIES}) diff --git a/makespec/BUILDVERSION b/makespec/BUILDVERSION index 9e6b8d37..f94bf0c6 100644 --- a/makespec/BUILDVERSION +++ b/makespec/BUILDVERSION @@ -1 +1 @@ -5790 +5791 \ No newline at end of file diff --git a/src/components/latency/LatencyTest.cpp b/src/components/latency/LatencyTest.cpp index 6c60cc8c..141fb46a 100644 --- a/src/components/latency/LatencyTest.cpp +++ b/src/components/latency/LatencyTest.cpp @@ -1,50 +1,52 @@ +#include "uvw.hpp" #include "LatencyTest.hpp" #include "LatencyTestThread.hpp" #include "core/handler/ConfigHandler.hpp" -constexpr auto LATENCY_PROPERTY_KEY = "__QvLatencyTest__"; - namespace Qv2ray::components::latency { + int isAddr(const char *host, int port, struct sockaddr_storage *storage, int ipv6first) + { + if (uv_ip4_addr(host, port, reinterpret_cast(storage)) == 0) + { + return AF_INET; + } + if (uv_ip6_addr(host, port, reinterpret_cast(storage)) == 0) + { + return AF_INET6; + } + return -1; + } LatencyTestHost::LatencyTestHost(const int defaultCount, QObject *parent) : QObject(parent) { + qRegisterMetaType(); + qRegisterMetaType(); totalTestCount = defaultCount; + latencyThread = new LatencyTestThread(this); + latencyThread->start(); + } + + LatencyTestHost::~LatencyTestHost() + { + latencyThread->stopLatencyTest(); + latencyThread->wait(); } void LatencyTestHost::StopAllLatencyTest() { - for (const auto &thread : latencyThreads) - { - thread->terminate(); - } - latencyThreads.clear(); + latencyThread->stopLatencyTest(); + latencyThread->wait(); + latencyThread->start(); } + void LatencyTestHost::TestLatency(const ConnectionId &id, Qv2rayLatencyTestingMethod method) { - const auto &[protocol, host, port] = GetConnectionInfo(id); - auto thread = new LatencyTestThread(host, port, method, totalTestCount, this); - connect(thread, &QThread::finished, this, &LatencyTestHost::OnLatencyThreadProcessCompleted); - thread->setProperty(LATENCY_PROPERTY_KEY, QVariant::fromValue(id)); - latencyThreads.push_back(thread); - thread->start(); + latencyThread->pushRequest(id, totalTestCount, method); } - - void LatencyTestHost::OnLatencyThreadProcessCompleted() + void LatencyTestHost::TestLatency(const QList &ids, Qv2rayLatencyTestingMethod method) { - const auto senderThread = qobject_cast(sender()); - latencyThreads.removeOne(senderThread); - auto result = senderThread->GetResult(); - - if (!result.errorMessage.isEmpty()) - { - LOG(MODULE_NETWORK, "Ping --> " + result.errorMessage) - result.avg = LATENCY_TEST_VALUE_ERROR; - result.best = LATENCY_TEST_VALUE_ERROR; - result.worst = LATENCY_TEST_VALUE_ERROR; - } - - emit OnLatencyTestCompleted(senderThread->property(LATENCY_PROPERTY_KEY).value(), result); + latencyThread->pushRequest(ids, totalTestCount, method); } } // namespace Qv2ray::components::latency diff --git a/src/components/latency/LatencyTest.hpp b/src/components/latency/LatencyTest.hpp index fea763b6..886b68c6 100644 --- a/src/components/latency/LatencyTest.hpp +++ b/src/components/latency/LatencyTest.hpp @@ -1,6 +1,10 @@ #pragma once #include "base/Qv2rayBase.hpp" - +namespace uvw +{ + class Loop; +} +struct sockaddr_storage; namespace Qv2ray::components::latency { class LatencyTestThread; @@ -14,6 +18,16 @@ namespace Qv2ray::components::latency long avg = LATENCY_TEST_VALUE_ERROR; Qv2rayLatencyTestingMethod method; }; + struct LatencyTestRequest + { + ConnectionId id; + QString host; + int port; + int totalCount; + Qv2rayLatencyTestingMethod method; + }; + + int isAddr(const char *host, int port, struct sockaddr_storage *storage, int ipv6first); class LatencyTestHost : public QObject { @@ -21,21 +35,22 @@ namespace Qv2ray::components::latency public: explicit LatencyTestHost(const int defaultCount = 3, QObject *parent = nullptr); void TestLatency(const ConnectionId &connectionId, Qv2rayLatencyTestingMethod); + void TestLatency(const QList &connectionIds, Qv2rayLatencyTestingMethod); void StopAllLatencyTest(); - ~LatencyTestHost() - { - StopAllLatencyTest(); - } - signals: - void OnLatencyTestCompleted(const ConnectionId &id, const LatencyTestResult &data); - private slots: - void OnLatencyThreadProcessCompleted(); + ~LatencyTestHost() override; + + signals: + void OnLatencyTestCompleted(ConnectionId id, LatencyTestResult data); private: int totalTestCount; - QList latencyThreads; + // we're not introduce multi latency test thread for now, + // cause it's easy to use a scheduler like round-robin scheme + // and libuv event loop is fast. + LatencyTestThread *latencyThread; }; } // namespace Qv2ray::components::latency using namespace Qv2ray::components::latency; +Q_DECLARE_METATYPE(LatencyTestResult) diff --git a/src/components/latency/LatencyTestThread.cpp b/src/components/latency/LatencyTestThread.cpp index efc48ea0..2487cd38 100644 --- a/src/components/latency/LatencyTestThread.cpp +++ b/src/components/latency/LatencyTestThread.cpp @@ -1,84 +1,101 @@ #include "LatencyTestThread.hpp" #include "TCPing.hpp" +#include "core/CoreUtils.hpp" #ifdef Q_OS_UNIX #include "unix/ICMPPing.hpp" #else #include "win/ICMPPing.hpp" #endif +#include "uvw.hpp" namespace Qv2ray::components::latency { - LatencyTestThread::LatencyTestThread(const QString &host, int port, Qv2rayLatencyTestingMethod method, int count, QObject *parent) - : QThread(parent) + LatencyTestThread::LatencyTestThread(QObject *parent) : QThread(parent) { - this->count = count; - this->host = host; - this->port = port; - this->method = method; - this->resultData = {}; - this->resultData.method = method; } + + void LatencyTestThread::pushRequest(const ConnectionId &id, int totalTestCount, Qv2rayLatencyTestingMethod method) + { + if(isStop) + return; + std::unique_lock lockGuard{ m }; + const auto &[protocol, host, port] = GetConnectionInfo(id); + requests.emplace_back(LatencyTestRequest{ id, host, port, totalTestCount, method }); + } + void LatencyTestThread::run() { - resultData.avg = 0; - resultData.best = 0; - resultData.worst = 0; - switch (method) - { - case ICMPING: + loop = uvw::Loop::create(); + stopTimer = loop->resource(); + stopTimer->on([this](auto &, auto &handle) { + if (isStop) { - icmping::ICMPPing pingHelper(30); - for (auto i = 0; i < count; i++) + if(!requests.empty()) + requests.clear(); + int timer_count=0; + //LOG(MODULE_NETWORK,"fuck") + loop->walk([&timer_count,this](uvw::BaseHandle&h) + { + if(!h.closing()) + timer_count++; + }); + if(timer_count==1)//only current timer { - resultData.totalCount++; - const auto value = pingHelper.ping(host); - const auto _latency = value.first; - const auto errMessage = value.second; - if (!errMessage.isEmpty()) + handle.stop(); + handle.close(); + loop->clear(); + loop->close(); + loop->stop(); + } + } + else + { + if (requests.empty()) + return; + std::unique_lock lockGuard{ m }; + auto parent = qobject_cast(this->parent()); + for (auto &req : requests) + { + switch (req.method) { - resultData.errorMessage.append(NEWLINE + errMessage); - resultData.failedCount++; - } - else - { -#ifdef Q_OS_WIN - // Is it Windows? - #undef min - #undef max + case ICMPING: + { +#ifdef Q_OS_UNIX + auto ptr = std::make_shared(30,loop,req,parent); + ptr->start(); +#else + auto ptr = std::make_shared(30); + ptr->start(loop,req,parent); #endif - resultData.avg += _latency; -#define _qvmin_(x, y) ((x) < (y) ? (x) : (y)) -#define _qvmax_(x, y) ((x) > (y) ? (x) : (y)) - resultData.best = _qvmin_(resultData.best, _latency); - resultData.worst = _qvmax_(resultData.worst, _latency); -#undef _qvmax_ -#undef _qvmin_ + } + break; + case TCPING: + default: + { + auto ptr = std::make_shared(loop, req, parent); + ptr->start(); + break; + } } } - if (resultData.totalCount > 0 && resultData.failedCount != resultData.totalCount) - { - resultData.errorMessage.clear(); - // ms to s - resultData.avg = resultData.avg / (resultData.totalCount - resultData.failedCount) / 1000; - } - else - { - resultData.avg = LATENCY_TEST_VALUE_ERROR; - LOG(MODULE_NETWORK, resultData.errorMessage) - } - // - // - break; - } - case TCPING: - default: - { - this->resultData = tcping::TestTCPLatency(host, port, count); - break; + requests.clear(); } + }); + stopTimer->start(uvw::TimerHandle::Time{ 500 }, uvw::TimerHandle::Time{ 500 }); + loop->run(); + } + void LatencyTestThread::pushRequest(const QList &ids, int totalTestCount, Qv2rayLatencyTestingMethod method) + { + if(isStop) + return; + std::unique_lock lockGuard{ m }; + for (const auto &id : ids) + { + const auto &[protocol, host, port] = GetConnectionInfo(id); + requests.emplace_back(LatencyTestRequest{ id, host, port, totalTestCount, method }); } } } // namespace Qv2ray::components::latency diff --git a/src/components/latency/LatencyTestThread.hpp b/src/components/latency/LatencyTestThread.hpp index 45593d53..5a2c4c42 100644 --- a/src/components/latency/LatencyTestThread.hpp +++ b/src/components/latency/LatencyTestThread.hpp @@ -2,27 +2,36 @@ #include "LatencyTest.hpp" #include +#include +#include +namespace uvw +{ + class Loop; + class TimerHandle; +} // namespace uvw namespace Qv2ray::components::latency { class LatencyTestThread : public QThread { Q_OBJECT public: - explicit LatencyTestThread(const QString &host, int port, Qv2rayLatencyTestingMethod, int count, QObject *parent = nullptr); - LatencyTestResult GetResult() const + explicit LatencyTestThread(QObject *parent = nullptr); + void stopLatencyTest() { - return resultData; + isStop = true; } + void pushRequest(const QList &ids, int totalTestCount, Qv2rayLatencyTestingMethod method); + void pushRequest(const ConnectionId &id, int totalTestCount, Qv2rayLatencyTestingMethod method); protected: void run() override; private: - LatencyTestResult resultData; - QString host; - int port; - int count; - Qv2rayLatencyTestingMethod method; + std::shared_ptr loop; + bool isStop = false; + std::shared_ptr stopTimer; + std::vector requests; + std::mutex m; // static LatencyTestResult TestLatency_p(const ConnectionId &id, const int count); }; diff --git a/src/components/latency/TCPing.cpp b/src/components/latency/TCPing.cpp index 789a192d..d726f9b6 100644 --- a/src/components/latency/TCPing.cpp +++ b/src/components/latency/TCPing.cpp @@ -1,364 +1,135 @@ #include "TCPing.hpp" -#ifdef _WIN32 - #include - #include -#else - #include - #include - #include - #include - #include - #include -#endif +#include "uvw.hpp" namespace Qv2ray::components::latency::tcping { + constexpr int conn_timeout_sec=5; -#ifdef Q_OS_WIN - using qv_socket_t = SOCKET; - struct TranslateWSAError{ - ~TranslateWSAError(){ - errno = translate_sys_error(WSAGetLastError()); - } - int translate_sys_error(int sys_errno) { - LOG(MODULE_NETWORK, "translate_sys_error:WSAGetLastError()==" + QSTRN(sys_errno)) - switch (sys_errno) { - case ERROR_NOACCESS: return EACCES; - case WSAEACCES: return EACCES; - case ERROR_ELEVATION_REQUIRED: return EACCES; - case ERROR_CANT_ACCESS_FILE: return EACCES; - case ERROR_ADDRESS_ALREADY_ASSOCIATED: return EADDRINUSE; - case WSAEADDRINUSE: return EADDRINUSE; - case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL; - case WSAEAFNOSUPPORT: return EAFNOSUPPORT; - case WSAEWOULDBLOCK: return EAGAIN; - case WSAEALREADY: return EALREADY; - case ERROR_INVALID_FLAGS: return EBADF; - case ERROR_INVALID_HANDLE: return EBADF; - case ERROR_LOCK_VIOLATION: return EBUSY; - case ERROR_PIPE_BUSY: return EBUSY; - case ERROR_SHARING_VIOLATION: return EBUSY; - case ERROR_OPERATION_ABORTED: return ECANCELED; - case WSAEINTR: return ECANCELED; - /*case ERROR_NO_UNICODE_TRANSLATION: return ECHARSET;*/ - case ERROR_CONNECTION_ABORTED: return ECONNABORTED; - case WSAECONNABORTED: return ECONNABORTED; - case ERROR_CONNECTION_REFUSED: return ECONNREFUSED; - case WSAECONNREFUSED: return ECONNREFUSED; - case ERROR_NETNAME_DELETED: return ECONNRESET; - case WSAECONNRESET: return ECONNRESET; - case ERROR_ALREADY_EXISTS: return EEXIST; - case ERROR_FILE_EXISTS: return EEXIST; - case ERROR_BUFFER_OVERFLOW: return EFAULT; - case WSAEFAULT: return EFAULT; - case ERROR_HOST_UNREACHABLE: return EHOSTUNREACH; - case WSAEHOSTUNREACH: return EHOSTUNREACH; - case ERROR_INSUFFICIENT_BUFFER: return EINVAL; - case ERROR_INVALID_DATA: return EINVAL; - case ERROR_INVALID_PARAMETER: return EINVAL; - case ERROR_SYMLINK_NOT_SUPPORTED: return EINVAL; - case WSAEINVAL: return EINVAL; - case WSAEPFNOSUPPORT: return EINVAL; - case WSAESOCKTNOSUPPORT: return EINVAL; - case ERROR_BEGINNING_OF_MEDIA: return EIO; - case ERROR_BUS_RESET: return EIO; - case ERROR_CRC: return EIO; - case ERROR_DEVICE_DOOR_OPEN: return EIO; - case ERROR_DEVICE_REQUIRES_CLEANING: return EIO; - case ERROR_DISK_CORRUPT: return EIO; - case ERROR_EOM_OVERFLOW: return EIO; - case ERROR_FILEMARK_DETECTED: return EIO; - case ERROR_GEN_FAILURE: return EIO; - case ERROR_INVALID_BLOCK_LENGTH: return EIO; - case ERROR_IO_DEVICE: return EIO; - case ERROR_NO_DATA_DETECTED: return EIO; - case ERROR_NO_SIGNAL_SENT: return EIO; - case ERROR_OPEN_FAILED: return EIO; - case ERROR_SETMARK_DETECTED: return EIO; - case ERROR_SIGNAL_REFUSED: return EIO; - case WSAEISCONN: return EISCONN; - case ERROR_CANT_RESOLVE_FILENAME: return ELOOP; - case ERROR_TOO_MANY_OPEN_FILES: return EMFILE; - case WSAEMFILE: return EMFILE; - case WSAEMSGSIZE: return EMSGSIZE; - case ERROR_FILENAME_EXCED_RANGE: return ENAMETOOLONG; - case ERROR_NETWORK_UNREACHABLE: return ENETUNREACH; - case WSAENETUNREACH: return ENETUNREACH; - case WSAENOBUFS: return ENOBUFS; - case ERROR_BAD_PATHNAME: return ENOENT; - case ERROR_DIRECTORY: return ENOENT; - case ERROR_ENVVAR_NOT_FOUND: return ENOENT; - case ERROR_FILE_NOT_FOUND: return ENOENT; - case ERROR_INVALID_NAME: return ENOENT; - case ERROR_INVALID_DRIVE: return ENOENT; - case ERROR_INVALID_REPARSE_DATA: return ENOENT; - case ERROR_MOD_NOT_FOUND: return ENOENT; - case ERROR_PATH_NOT_FOUND: return ENOENT; - case WSAHOST_NOT_FOUND: return ENOENT; - case WSANO_DATA: return ENOENT; - case ERROR_NOT_ENOUGH_MEMORY: return ENOMEM; - case ERROR_OUTOFMEMORY: return ENOMEM; - case ERROR_CANNOT_MAKE: return ENOSPC; - case ERROR_DISK_FULL: return ENOSPC; - case ERROR_EA_TABLE_FULL: return ENOSPC; - case ERROR_END_OF_MEDIA: return ENOSPC; - case ERROR_HANDLE_DISK_FULL: return ENOSPC; - case ERROR_NOT_CONNECTED: return ENOTCONN; - case WSAENOTCONN: return ENOTCONN; - case ERROR_DIR_NOT_EMPTY: return ENOTEMPTY; - case WSAENOTSOCK: return ENOTSOCK; - case ERROR_NOT_SUPPORTED: return ENOTSUP; - case ERROR_BROKEN_PIPE: return EOF; - case ERROR_ACCESS_DENIED: return EPERM; - case ERROR_PRIVILEGE_NOT_HELD: return EPERM; - case ERROR_BAD_PIPE: return EPIPE; - case ERROR_NO_DATA: return EPIPE; - case ERROR_PIPE_NOT_CONNECTED: return EPIPE; - case WSAESHUTDOWN: return EPIPE; - case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; - case ERROR_WRITE_PROTECT: return EROFS; - case ERROR_SEM_TIMEOUT: return ETIMEDOUT; - case WSAETIMEDOUT: return ETIMEDOUT; - case ERROR_NOT_SAME_DEVICE: return EXDEV; - case ERROR_INVALID_FUNCTION: return EISDIR; - case ERROR_META_EXPANSION_TOO_LONG: return E2BIG; - default: return EIO;/*unknown*/ - } - } - }; -#else - using qv_socket_t = int; + int getSocket(int af, int socktype, int proto) { + uv_os_sock_t fd; +#ifndef INVALID_SOCKET +# define INVALID_SOCKET -1 #endif - - inline int setnonblocking(qv_socket_t sockno, int &opt) - { -#ifdef _WIN32 - ULONG block = 1; - if (ioctlsocket(sockno, FIONBIO, &block) == SOCKET_ERROR) - { - return -1; - } -#else - if ((opt = fcntl(sockno, F_GETFL, NULL)) < 0) - { - // get socket flags - return -1; - } - if (fcntl(sockno, F_SETFL, opt | O_NONBLOCK) < 0) - { - // set socket non-blocking - return -1; + if ((fd = socket(af, socktype, proto)) == INVALID_SOCKET) { + return 0; } + // Set TCP connection timeout per-socket level. + // See [https://github.com/libuv/help/issues/54] for details. +#if defined( _WIN32 ) && !defined( __SYMBIAN32__ ) + setsockopt(fd, IPPROTO_TCP, TCP_MAXRT, (char*)&conn_timeout_sec, sizeof(conn_timeout_sec)); +#elif defined(__APPLE__) + // (billhoo) MacOS uses TCP_CONNECTIONTIMEOUT to do so. + setsockopt(fd, IPPROTO_TCP, TCP_CONNECTIONTIMEOUT, (char*)&conn_timeout_sec, sizeof(conn_timeout_sec)); +#else // Linux like systems + uint32_t conn_timeout_ms = conn_timeout_sec * 1000; + setsockopt(fd, IPPROTO_TCP, TCP_USER_TIMEOUT, (char*)&conn_timeout_ms, sizeof(conn_timeout_ms)); #endif - return 0; + return (int)fd; } - inline int setblocking(qv_socket_t sockno, int &opt) + TCPing::TCPing(std::shared_ptr loopin, LatencyTestRequest &reqin, LatencyTestHost *testHost) + : loop(std::move(loopin)), req(std::move(reqin)), testHost(testHost) { -#ifdef _WIN32 - ULONG block = 0; - if (ioctlsocket(sockno, FIONBIO, &block) == SOCKET_ERROR) + data.totalCount = 1; + data.failedCount = 0; + data.worst = 0; + data.avg = 0; + af=isAddr(req.host.toStdString().data(), req.port, &storage, 0); + if (af == -1) { - return -1; + getAddrHandle = loop->resource(); + sprintf(digitBuffer, "%d", req.port); } -#else - if (fcntl(sockno, F_SETFL, opt) < 0) - { - // reset socket flags - return -1; - } -#endif - return 0; } - - int connect_wait(qv_socket_t sockno, struct sockaddr *addr, size_t addrlen, int timeout_sec = 5) + TCPing::~TCPing() { - int res; - int opt; - timeval tv = { 0, 0 }; -#ifdef _WIN32 - TranslateWSAError _translateWSAError_; -#endif - tv.tv_sec = timeout_sec; - tv.tv_usec = 0; - if ((res = setnonblocking(sockno, opt)) != 0) - return -1; - if ((res = ::connect(sockno, addr, addrlen)) < 0) + if (getAddrHandle) + getAddrHandle->clear(); + } + int TCPing::getAddrInfoRes(uvw::AddrInfoEvent &e) + { + struct addrinfo *rp = nullptr; + for (rp = e.data.get(); rp != nullptr; rp = rp->ai_next) + if (rp->ai_family == AF_INET) + { + if (rp->ai_family == AF_INET) + { + af=AF_INET; + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in)); + } + else if (rp->ai_family == AF_INET6) + { + af=AF_INET6; + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in6)); + } + break; + } + if (rp == nullptr) { -#ifdef _WIN32 - if (WSAGetLastError() == WSAEWOULDBLOCK) + // fallback: if we can't find prefered AF, then we choose alternative. + for (rp = e.data.get(); rp != nullptr; rp = rp->ai_next) { -#else - if (errno == EINPROGRESS) - { -#endif - // connecting - fd_set wait_set; - FD_ZERO(&wait_set); - FD_SET(sockno, &wait_set); - res = select(sockno + 1, NULL, &wait_set, NULL, &tv); + if (rp->ai_family == AF_INET) + { + af=AF_INET; + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in)); + } + else if (rp->ai_family == AF_INET6) + { + af=AF_INET6; + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in6)); + } + break; } } - else - // connect immediately - res = 1; - - if (setblocking(sockno, opt) != 0) - return -1; - - if (res < 0) - // an error occured - return -1; - else if (res == 0) - { - // timeout - errno = ETIMEDOUT; - return 1; - } - else - { - socklen_t len = sizeof(opt); - if (getsockopt(sockno, SOL_SOCKET, SO_ERROR, (char *) (&opt), &len) < 0) - return -1; - } - return 0; - } - - int resolveHost(const QString &host, int port, addrinfo **res) - { - addrinfo hints; -#ifdef Q_OS_WIN - WSADATA wsadata; - WSAStartup(0x0202, &wsadata); - memset(&hints, 0, sizeof(struct addrinfo)); -#else - hints.ai_flags = AI_NUMERICSERV; -#endif - hints.ai_family = AF_UNSPEC; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = 0; - return getaddrinfo(host.toStdString().c_str(), std::to_string(port).c_str(), &hints, res); - } - - int testLatency(struct addrinfo *addr, system_clock::time_point *start, system_clock::time_point *end) - { - qv_socket_t fd; - - const int on = 1; - - /* try to connect for each of the entries: */ - while (addr != nullptr) - { - if (isExiting) - return 0; - - // create socket - if (!(fd = socket(addr->ai_family, addr->ai_socktype, addr->ai_protocol))) - { - goto next_addr0; - } - - // Windows needs special conversion. -#ifdef _WIN32 - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const char *) &on, sizeof(on)) < 0) -#else - if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) -#endif - goto next_addr1; - *start = system_clock::now(); - if (connect_wait(fd, addr->ai_addr, addr->ai_addrlen) == 0) - { - *end = system_clock::now(); -#ifdef Q_OS_WIN - closesocket(fd); -#else - close(fd); -#endif - return 0; - } - - next_addr1: -#ifdef _WIN32 - closesocket(fd); -#else - close(fd); -#endif - next_addr0: - addr = addr->ai_next; - } - + if (rp) + return 0; return -1; } - LatencyTestResult TestTCPLatency(const QString &host, int port, int testCount) + void TCPing::start() { - LatencyTestResult data; - int successCount = 0; - addrinfo *resolved; - int errcode; - - if ((errcode = resolveHost(host, port, &resolved)) != 0) + async_DNS_lookup(0, 0); + } + void TCPing::notifyTestHost() + { + if (data.failedCount + successCount == req.totalCount) { -#ifdef Q_OS_WIN - data.errorMessage = QString::fromStdWString(gai_strerror(errcode)); -#else - data.errorMessage = gai_strerror(errcode); -#endif - return data; - } - - int currentCount = 0; - - data.avg = 0; - data.worst = 0; - data.best = 0; - - while (currentCount < testCount) - { - system_clock::time_point start; - system_clock::time_point end; - - if ((errcode = testLatency(resolved, &start, &end)) != 0) - { - LOG(MODULE_NETWORK, "error connecting to host: " + host + ":" + QSTRN(port) + " " + strerror(errno)) - } + if (data.failedCount == req.totalCount) + data.avg = LATENCY_TEST_VALUE_ERROR; else - { - successCount++; - auto milliseconds = std::chrono::duration_cast(end - start); - long ms = milliseconds.count(); - data.avg += ms; -#ifdef Q_OS_WIN - // Is it Windows? - #undef min - #undef max -#endif - data.worst = std::min(data.worst, ms); - data.best = std::max(data.best, ms); - - if (ms > 1000) - { - LOG(MODULE_NETWORK, "Stop the test on the first long connect()") - break; /* Stop the test on the first long connect() */ - } - } - - currentCount++; - QThread::msleep(200); + data.errorMessage.clear(), data.avg = data.avg / successCount; + testHost->OnLatencyTestCompleted(req.id, data); } - freeaddrinfo(resolved); - if (successCount > 0) + } + void TCPing::tcp_ping() + { + for (; data.totalCount <= req.totalCount; ++data.totalCount) { - data.avg = data.avg / successCount; + auto tcpClient = loop->resource(); + tcpClient->open(getSocket(af,SOCK_STREAM,IPPROTO_TCP)); + tcpClient->once([ptr = shared_from_this(), this](const uvw::ErrorEvent &e, uvw::TCPHandle &h) { + LOG(MODULE_NETWORK, "error connecting to host: " + req.host + ":" + QSTRN(req.port) + " " + e.what()) + data.failedCount += 1; + data.errorMessage = e.what(); + notifyTestHost(); + h.clear(); + h.close(); + }); + tcpClient->once([ptr = shared_from_this(), start = system_clock::now(), this](auto &, auto &h) { + ++successCount; + system_clock::time_point end = system_clock::now(); + auto milliseconds = std::chrono::duration_cast(end - start); + long ms = milliseconds.count(); + data.avg += ms; + data.worst = std::max(data.worst, ms); + data.best = std::min(data.best, ms); + notifyTestHost(); + h.clear(); + h.close(); + }); + tcpClient->connect(reinterpret_cast(storage)); } - else - { - data.avg = LATENCY_TEST_VALUE_ERROR; - data.worst = LATENCY_TEST_VALUE_ERROR; - data.best = LATENCY_TEST_VALUE_ERROR; - data.errorMessage = QObject::tr("Timeout"); - } - return data; } } // namespace Qv2ray::components::latency::tcping diff --git a/src/components/latency/TCPing.hpp b/src/components/latency/TCPing.hpp index 134c8df8..d8e69958 100644 --- a/src/components/latency/TCPing.hpp +++ b/src/components/latency/TCPing.hpp @@ -1,7 +1,70 @@ #pragma once #include "LatencyTest.hpp" #include "base/Qv2rayBase.hpp" +#include "coroutine.hpp" +#include "uvw.hpp" + +#include namespace Qv2ray::components::latency::tcping { - LatencyTestResult TestTCPLatency(const QString &host, int port, int testCount); + class TCPing + : public std::enable_shared_from_this + , public coroutine + { + public: + TCPing(std::shared_ptr loop, LatencyTestRequest &req, LatencyTestHost *testHost); + void start(); + ~TCPing(); + + private: + int getAddrInfoRes(uvw::AddrInfoEvent &e); + void tcp_ping(); + template + void async_DNS_lookup(E &&e, H &&h) + { + co_enter(*this) + { + if (getAddrHandle) + { + getAddrHandle->once(coro(async_DNS_lookup)); + getAddrHandle->once(coro(async_DNS_lookup)); + co_yield return getAddrHandle->addrInfo(req.host.toStdString(), digitBuffer); + co_yield if constexpr (std::is_same_v>) + { + if (getAddrInfoRes(e) != 0) + { + data.errorMessage = QObject::tr("DNS not resolved"); + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + h.clear(); + return; + } + h.clear(); + } + else + { + if constexpr (std::is_same_v>) + { + data.errorMessage = QObject::tr("DNS not resolved"); + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + h.clear(); + return; + } + } + } + } + tcp_ping(); + } + void notifyTestHost(); + int successCount = 0; + LatencyTestRequest req; + LatencyTestResult data; + LatencyTestHost *testHost; + struct sockaddr_storage storage; + char digitBuffer[20] = { 0 }; + int af=AF_INET; + std::shared_ptr loop; + std::shared_ptr getAddrHandle; + }; } // namespace Qv2ray::components::latency::tcping diff --git a/src/components/latency/coroutine.hpp b/src/components/latency/coroutine.hpp new file mode 100644 index 00000000..9408abd2 --- /dev/null +++ b/src/components/latency/coroutine.hpp @@ -0,0 +1,93 @@ +#pragma once +namespace detail +{ + + struct coroutine_ref; + +} // namespace detail + +struct coroutine +{ + coroutine() : line_(0) + { + } + bool await_ready() const + { + return line_ == -1; + } + + private: + friend struct detail::coroutine_ref; + int line_; +}; + +namespace detail +{ + + struct coroutine_ref + { + coroutine_ref(coroutine &c) : line_(c.line_), modified_(false) + { + } + ~coroutine_ref() + { + if (!modified_) + line_ = -1; + } + operator int() const + { + return line_; + } + int &operator=(int v) + { + modified_ = true; + return line_ = v; + } + + private: + void operator=(coroutine_ref const &); + + int &line_; + bool modified_; + }; + +} // namespace detail + +#define co_enter(c) \ + switch (::detail::coroutine_ref _coro_value = c) \ + case -1: \ + if (_coro_value) \ + { \ + goto terminate_coroutine; \ + terminate_coroutine: \ + _coro_value = -1; \ + goto bail_out_of_coroutine; \ + bail_out_of_coroutine: \ + break; \ + } \ + else \ + case 0: + +#define __co_yield_impl(n) \ + for (_coro_value = (n);;) \ + if (_coro_value == 0) \ + { \ + case (n):; break; \ + } \ + else \ + switch (_coro_value ? 0 : 1) \ + for (;;) case -1: \ + if (_coro_value) \ + goto terminate_coroutine; \ + else \ + for (;;) case 1: \ + if (_coro_value) \ + goto bail_out_of_coroutine; \ + else \ + case 0: + +#define co_yield __co_yield_impl(__LINE__) +#define coro(f) \ + [this, ptr = (shared_from_this())](auto &&e, auto &&h) { \ + f(std::forward(e), std::forward(h)); \ + } diff --git a/src/components/latency/unix/ICMPPing.cpp b/src/components/latency/unix/ICMPPing.cpp index 66be6214..7906c576 100644 --- a/src/components/latency/unix/ICMPPing.cpp +++ b/src/components/latency/unix/ICMPPing.cpp @@ -7,16 +7,13 @@ #include #ifdef Q_OS_UNIX - #include - // - #include - #include #include - #include + #include //macos need that #include - #include #include #include + #include + #ifdef Q_OS_MAC #define SOL_IP 0 #endif @@ -55,12 +52,12 @@ namespace Qv2ray::components::latency::icmping } } - ICMPPing::ICMPPing(int ttl) + ICMPPing::ICMPPing(int ttl,std::shared_ptr loop_in,LatencyTestRequest& req_in,LatencyTestHost* testHost): + loop(std::move(loop_in)), + req(std::move(req_in)), + testHost(testHost) { - auto timeout_s = 5; - // create socket - if ( //((sd = socket(PF_INET, SOCK_RAW, IPPROTO_ICMP)) < 0) && - ((socketId = socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)) < 0)) + if (((socketId = socket(PF_INET, SOCK_DGRAM, IPPROTO_ICMP)) < 0)) { initErrorMessage = "EPING_SOCK: " + QObject::tr("Socket creation failed"); return; @@ -73,91 +70,207 @@ namespace Qv2ray::components::latency::icmping return; } - // set timeout in secs (do not use secs - BUGGY) - timeval timeout; - timeout.tv_sec = timeout_s; - timeout.tv_usec = 0; - if (setsockopt(socketId, SOL_SOCKET, SO_RCVTIMEO, (char *) &timeout, sizeof(timeout)) != 0) - { - deinit(); - initErrorMessage = "EPING_SETTO: " + QObject::tr("Setting timeout failed"); - return; - } initialized = true; + data.totalCount = req.totalCount; + data.failedCount = 0; + data.worst = 0; + data.avg = 0; + if (isAddr(req.host.toStdString().data(), req.port, &storage, 0) == -1) + { + getAddrHandle = loop->resource(); + sprintf(digitBuffer, "%d", req.port); + } } - /// @return value < 0 on error, response time in ms on success - QPair ICMPPing::ping(const QString &address) + int ICMPPing::getAddrInfoRes(uvw::AddrInfoEvent &e) + { + struct addrinfo *rp = nullptr; + for (rp = e.data.get(); rp != nullptr; rp = rp->ai_next) + if (rp->ai_family == AF_INET) + { + if (rp->ai_family == AF_INET) + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in)); + else if (rp->ai_family == AF_INET6) + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in6)); + break; + } + if (rp == nullptr) + { + // fallback: if we can't find prefered AF, then we choose alternative. + for (rp = e.data.get(); rp != nullptr; rp = rp->ai_next) + { + if (rp->ai_family == AF_INET) + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in)); + else if (rp->ai_family == AF_INET6) + memcpy(&storage, rp->ai_addr, sizeof(struct sockaddr_in6)); + break; + } + } + if (rp) + return 0; + return -1; + } + void ICMPPing::start() { if (!initialized) { - return { 0, initErrorMessage }; + data.errorMessage = initErrorMessage; + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + return; } - timeval start, end; - socklen_t slen; - // not initialized if (socketId < 0) - return { 0, "EPING_SOCK:" + QObject::tr("Socket creation failed") }; - - // resolve hostname - hostent *resolvedAddress = gethostbyname(address.toStdString().c_str()); - if (!resolvedAddress) - return { 0, "EPING_HOST: " + QObject::tr("Unresolvable hostname") }; - - // set IP address to ping - sockaddr_in targetAddress; - memset(&targetAddress, 0, sizeof(targetAddress)); - targetAddress.sin_family = resolvedAddress->h_addrtype; - targetAddress.sin_port = 0; - - auto src = resolvedAddress->h_addr; - if (src == nullptr) { - // Buggy GCC detected - return { 0, "GCC: COMPILER BUG. Cannot even dereference a char**" }; + data.errorMessage="EPING_SOCK:" + QObject::tr("Socket creation failed"); + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + return; } + async_DNS_lookup(0,0); + } - memcpy(&targetAddress.sin_addr, src, resolvedAddress->h_length); - // prepare echo request packet - icmp _icmp_request; - memset(&_icmp_request, 0, sizeof(_icmp_request)); - _icmp_request.icmp_type = ICMP_ECHO; - _icmp_request.icmp_hun.ih_idseq.icd_id = 0; // SOCK_DGRAM & 0 => id will be set by kernel - unsigned short sent_seq; - _icmp_request.icmp_hun.ih_idseq.icd_seq = sent_seq = seq++; - _icmp_request.icmp_cksum = ping_checksum(reinterpret_cast(&_icmp_request), sizeof(_icmp_request)); - - // send echo request - gettimeofday(&start, NULL); - if (sendto(socketId, &_icmp_request, sizeof(icmp), 0, (struct sockaddr *) &targetAddress, sizeof(targetAddress)) <= 0) - return { 0, "EPING_SEND: " + QObject::tr("Sending echo request failed") }; - - // receive response (if any) - sockaddr_in remove_addr; - slen = sizeof(remove_addr); - int rlen; - icmp resp; - while ((rlen = recvfrom(socketId, &resp, sizeof(icmp), 0, (struct sockaddr *) &remove_addr, &slen)) > 0) + bool ICMPPing::notifyTestHost() + { + if (data.failedCount + successCount == data.totalCount) { - gettimeofday(&end, NULL); - - // skip malformed - if (rlen != sizeof(icmp)) - continue; - - // skip the ones we didn't send - if (resp.icmp_hun.ih_idseq.icd_seq != sent_seq) - continue; - - switch (resp.icmp_type) + if (data.failedCount == data.totalCount) + data.avg = LATENCY_TEST_VALUE_ERROR; + else + data.errorMessage.clear(), data.avg = data.avg / successCount; + testHost->OnLatencyTestCompleted(req.id, data); + if(timoutTimer) { - case ICMP_ECHOREPLY: return { 1000000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec), {} }; - case ICMP_UNREACH: return { 0, "EPING_DST: " + QObject::tr("Destination unreachable") }; - case ICMP_TIMXCEED: return { 0, "EPING_TIME: " + QObject::tr("Timeout") }; - default: return { 0, "EPING_UNK: " + QObject::tr("Unknown error") }; + timoutTimer->stop(); + timoutTimer->clear(); + timoutTimer->close(); } + if(pollHandle) + { + if(!pollHandle->closing()) + pollHandle->stop(); + pollHandle->clear(); + pollHandle->close(); + } + return true; + } + return false; + } + + void ICMPPing::ping() + { + timoutTimer = loop->resource(); + uvw::OSSocketHandle osSocketHandle{ socketId }; + pollHandle = loop->resource(osSocketHandle); + timoutTimer->once([this,ptr=std::weak_ptr{shared_from_this()}](auto&,uvw::TimerHandle&h) + { + if(ptr.expired()) + return; + else + { + auto p=ptr.lock(); + pollHandle->clear(); + if(!pollHandle->closing()) + pollHandle->stop(); + pollHandle->close(); + successCount = 0; + data.failedCount = data.totalCount = req.totalCount; + notifyTestHost(); + } + }); + timoutTimer->start(uvw::TimerHandle::Time{ 10000 }, uvw::TimerHandle::Time{ 0 }); + auto pollEvent = uvw::Flags::from(); + pollHandle->on([this, ptr = shared_from_this()](uvw::PollEvent &, uvw::PollHandle &h) { + timeval end; + sockaddr_in addr; + socklen_t slen = sizeof(sockaddr_in); + int rlen = 0; + char buf[1024]; + do + { + do + { + rlen = recvfrom(socketId,buf , 1024, 0, (struct sockaddr *) &addr, &slen); + } while (rlen == -1 && errno == EINTR); + + // skip malformed +#ifdef Q_OS_MAC + if(rlen(buf+20); +#else + auto& resp=*reinterpret_cast(buf); +#endif + // skip the ones we didn't send + auto cur_seq = resp.icmp_hun.ih_idseq.icd_seq; + if (cur_seq >= seq) + continue; + + switch (resp.icmp_type) + { + case ICMP_ECHOREPLY: + gettimeofday(&end, nullptr); + data.avg += + 1000 * (end.tv_sec - startTimevals[cur_seq - 1].tv_sec) + (end.tv_usec - startTimevals[cur_seq - 1].tv_usec)/1000; + successCount++; + notifyTestHost(); + continue; + case ICMP_UNREACH: + data.errorMessage = "EPING_DST: " + QObject::tr("Destination unreachable"); + data.failedCount++; + if (notifyTestHost()) + { + h.clear(); + h.close(); + return; + } + continue; + case ICMP_TIMXCEED: + data.errorMessage = "EPING_TIME: " + QObject::tr("Timeout"); + data.failedCount++; + if (notifyTestHost()) + { + h.clear(); + h.close(); + return; + } + continue; + default: + data.errorMessage = "EPING_UNK: " + QObject::tr("Unknown error"); + data.failedCount++; + if (notifyTestHost()) + { + h.clear(); + h.close(); + return; + } + continue; + } + } while (rlen > 0); + }); + pollHandle->start(pollEvent); + for (int i = 0; i < req.totalCount; ++i) + { + // prepare echo request packet + icmp _icmp_request; + memset(&_icmp_request, 0, sizeof(_icmp_request)); + _icmp_request.icmp_type = ICMP_ECHO; + _icmp_request.icmp_hun.ih_idseq.icd_id = 0; // SOCK_DGRAM & 0 => id will be set by kernel + _icmp_request.icmp_hun.ih_idseq.icd_seq = seq++; + _icmp_request.icmp_cksum = ping_checksum(reinterpret_cast(&_icmp_request), sizeof(_icmp_request)); + int n; + timeval start; + gettimeofday(&start, nullptr); + startTimevals.push_back(start); + do + { + n = ::sendto(socketId, &_icmp_request, sizeof(icmp), 0, (struct sockaddr *) &storage, sizeof(struct sockaddr)); + } while (n < 0 && errno == EINTR); } - return { 0, "EPING_TIME: " + QObject::tr("Timeout") }; } } // namespace Qv2ray::components::latency::icmping #endif diff --git a/src/components/latency/unix/ICMPPing.hpp b/src/components/latency/unix/ICMPPing.hpp index 987086fc..e310337a 100644 --- a/src/components/latency/unix/ICMPPing.hpp +++ b/src/components/latency/unix/ICMPPing.hpp @@ -1,19 +1,64 @@ #pragma once #include #ifdef Q_OS_UNIX + #include "components/latency/LatencyTest.hpp" + #include #include + #include "components/latency/coroutine.hpp" + #include "uvw.hpp" namespace Qv2ray::components::latency::icmping { - class ICMPPing + class ICMPPing : public std::enable_shared_from_this,public coroutine { public: - explicit ICMPPing(int ttl); + explicit ICMPPing(int ttl,std::shared_ptr loop,LatencyTestRequest& req,LatencyTestHost* testHost); ~ICMPPing() { deinit(); } - QPair ping(const QString &address); + void start(); + private: + int getAddrInfoRes(uvw::AddrInfoEvent &e); + void ping(); + bool notifyTestHost(); + template + void async_DNS_lookup(E &&e, H &&h) + { + co_enter(*this) + { + if (getAddrHandle) + { + getAddrHandle->once(coro(async_DNS_lookup)); + getAddrHandle->once(coro(async_DNS_lookup)); + co_yield return getAddrHandle->addrInfo(req.host.toStdString(), digitBuffer); + co_yield if constexpr (std::is_same_v>) + { + if (getAddrInfoRes(e) != 0) + { + data.errorMessage = QObject::tr("DNS not resolved"); + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + h.clear(); + return; + } + h.clear(); + } + else + { + if constexpr (std::is_same_v>) + { + data.errorMessage = QObject::tr("DNS not resolved"); + data.avg = LATENCY_TEST_VALUE_ERROR; + testHost->OnLatencyTestCompleted(req.id, data); + h.clear(); + return; + } + } + } + } + ping(); + } private: void deinit(); @@ -22,6 +67,17 @@ namespace Qv2ray::components::latency::icmping // socket int socketId = -1; bool initialized = false; + int successCount = 0; + LatencyTestRequest req; + LatencyTestHost* testHost; + LatencyTestResult data; + struct sockaddr_storage storage; + char digitBuffer[20] = { 0 }; + std::shared_ptr loop; + std::shared_ptr getAddrHandle; + std::shared_ptr timoutTimer; + std::shared_ptr pollHandle; + std::vector startTimevals; QString initErrorMessage; }; } // namespace Qv2ray::components::latency::icmping diff --git a/src/components/latency/win/ICMPPing.cpp b/src/components/latency/win/ICMPPing.cpp index 74342742..c1620b52 100644 --- a/src/components/latency/win/ICMPPing.cpp +++ b/src/components/latency/win/ICMPPing.cpp @@ -1,16 +1,12 @@ #include "ICMPPing.hpp" #ifdef Q_OS_WIN -// #include -// #include -// #include -// #include -// #include #include + namespace Qv2ray::components::latency::icmping { ICMPPing::ICMPPing(uint64_t timeout) @@ -83,5 +79,18 @@ namespace Qv2ray::components::latency::icmping const ICMP_ECHO_REPLY *r = (const ICMP_ECHO_REPLY *) reply_buf; return QPair(r->RoundTripTime * 1000, QString{}); } + bool ICMPPing::notifyTestHost(LatencyTestHost *testHost, const ConnectionId &id) + { + if (data.failedCount + successCount == data.totalCount) + { + if (data.failedCount == data.totalCount) + data.avg = LATENCY_TEST_VALUE_ERROR; + else + data.errorMessage.clear(), data.avg = data.avg / successCount / 1000; + testHost->OnLatencyTestCompleted(id, data); + return true; + } + return false; + } } // namespace Qv2ray::components::latency::icmping #endif diff --git a/src/components/latency/win/ICMPPing.hpp b/src/components/latency/win/ICMPPing.hpp index 9b9b922b..2466048f 100644 --- a/src/components/latency/win/ICMPPing.hpp +++ b/src/components/latency/win/ICMPPing.hpp @@ -8,15 +8,20 @@ #include #ifdef Q_OS_WIN + #include "components/latency/LatencyTest.hpp" + #include #include #include #include #include - +namespace uvw +{ + class Loop; +} namespace Qv2ray::components::latency::icmping { - class ICMPPing + class ICMPPing : public std::enable_shared_from_this { public: ICMPPing(uint64_t timeout = DEFAULT_TIMEOUT); @@ -24,12 +29,16 @@ namespace Qv2ray::components::latency::icmping public: static const uint64_t DEFAULT_TIMEOUT = 10000U; + void start(std::shared_ptr loop, LatencyTestRequest &req, LatencyTestHost *testHost); + bool notifyTestHost(LatencyTestHost *testHost, const ConnectionId &id); public: QPair ping(const QString &ipAddr); private: uint64_t timeout = DEFAULT_TIMEOUT; + int successCount = 0; + LatencyTestResult data; }; } // namespace Qv2ray::components::latency::icmping #endif diff --git a/src/components/latency/win/ICMPPingWork.cpp b/src/components/latency/win/ICMPPingWork.cpp new file mode 100644 index 00000000..e2007f64 --- /dev/null +++ b/src/components/latency/win/ICMPPingWork.cpp @@ -0,0 +1,35 @@ +#include "uvw.hpp" +#include "ICMPPing.hpp" +#ifdef Q_OS_WIN +namespace Qv2ray::components::latency::icmping +{ + void ICMPPing::start(std::shared_ptr loop, LatencyTestRequest &req, LatencyTestHost *testHost) + { + data.totalCount = req.totalCount; + data.failedCount = 0; + data.worst = 0; + data.avg = 0; + for (int i = 0; i < req.totalCount; ++i) + { + auto work = loop->resource([ptr = shared_from_this(), this, addr = req.host, id = req.id, testHost]() mutable { + auto pingres = ping(addr); + if (!pingres.second.isEmpty()) + { + data.errorMessage = pingres.second; + data.failedCount++; + } + else + { + data.avg += pingres.first; + data.best = std::min(pingres.first, data.best); + data.worst = std::max(pingres.first, data.worst); + successCount++; + } + notifyTestHost(testHost, id); + ptr.reset(); + }); + work->queue(); + } + } +} +#endif diff --git a/src/core/handler/ConfigHandler.cpp b/src/core/handler/ConfigHandler.cpp index c415cc69..f8320cf0 100644 --- a/src/core/handler/ConfigHandler.cpp +++ b/src/core/handler/ConfigHandler.cpp @@ -124,16 +124,18 @@ namespace Qv2ray::core::handler { for (const auto &connection : connections.keys()) { - StartLatencyTest(connection); + emit OnLatencyTestStarted(connection); } + tcpingHelper->TestLatency(connections.keys(), GlobalConfig.networkConfig.latencyTestingMethod); } void QvConfigHandler::StartLatencyTest(const GroupId &id) { for (const auto &connection : groups[id].connections) { - StartLatencyTest(connection); + emit OnLatencyTestStarted(connection); } + tcpingHelper->TestLatency(groups[id].connections, GlobalConfig.networkConfig.latencyTestingMethod); } void QvConfigHandler::StartLatencyTest(const ConnectionId &id) @@ -356,7 +358,6 @@ namespace Qv2ray::core::handler QvConfigHandler::~QvConfigHandler() { LOG(MODULE_CORE_HANDLER, "Triggering save settings from destructor") - tcpingHelper->StopAllLatencyTest(); delete kernelHandler; SaveConnectionConfig(); } @@ -367,7 +368,7 @@ namespace Qv2ray::core::handler return connectionRootCache.value(id); } - void QvConfigHandler::OnLatencyDataArrived_p(const ConnectionId &id, const LatencyTestResult &result) + void QvConfigHandler::OnLatencyDataArrived_p(ConnectionId id, LatencyTestResult result) { CheckValidId(id, nothing); connections[id].latency = result.avg; diff --git a/src/core/handler/ConfigHandler.hpp b/src/core/handler/ConfigHandler.hpp index 1b6a43b8..0c1f2777 100644 --- a/src/core/handler/ConfigHandler.hpp +++ b/src/core/handler/ConfigHandler.hpp @@ -163,7 +163,7 @@ namespace Qv2ray::core::handler // private slots: void OnKernelCrashed_p(const ConnectionGroupPair &id, const QString &errMessage); - void OnLatencyDataArrived_p(const ConnectionId &id, const LatencyTestResult &data); + void OnLatencyDataArrived_p(ConnectionId id, LatencyTestResult data); void OnStatsDataArrived_p(const ConnectionGroupPair &id, const QMap &data); protected: diff --git a/src/ui/windows/w_PreferencesWindow.cpp b/src/ui/windows/w_PreferencesWindow.cpp index a23fdd31..329e6545 100644 --- a/src/ui/windows/w_PreferencesWindow.cpp +++ b/src/ui/windows/w_PreferencesWindow.cpp @@ -1183,16 +1183,9 @@ void PreferencesWindow::on_latencyTCPingRB_clicked() void PreferencesWindow::on_latencyICMPingRB_clicked() { LOADINGCHECK -#ifdef Q_OS_MAC - #warning No ICMPing support on macOS - CurrentConfig.networkConfig.latencyTestingMethod = TCPING; - latencyICMPingRB->setChecked(false); - latencyTCPingRB->setChecked(true); -#else CurrentConfig.networkConfig.latencyTestingMethod = ICMPING; latencyICMPingRB->setChecked(true); latencyTCPingRB->setChecked(false); -#endif } void PreferencesWindow::on_qvNetworkUATxt_editTextChanged(const QString &arg1)