#include "NetworkTestDialog.hpp" #include "I18N.hpp" #include "libslic3r/Utils.hpp" #include "GUI.hpp" #include "GUI_App.hpp" #include "I18N.hpp" #include "slic3r/Utils/Http.hpp" #include "libslic3r/AppConfig.hpp" #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #endif namespace Slic3r { namespace GUI { wxDECLARE_EVENT(EVT_UPDATE_RESULT, wxCommandEvent); wxDEFINE_EVENT(EVT_UPDATE_RESULT, wxCommandEvent); static wxString NA_STR = _L("N/A"); NetworkTestDialog::NetworkTestDialog(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style) : DPIDialog(parent,wxID_ANY,from_u8((boost::format(_utf8(L("Network Test")))).str()),wxDefaultPosition, wxSize(1000, 700), /*wxCAPTION*/wxDEFAULT_DIALOG_STYLE|wxMAXIMIZE_BOX|wxMINIMIZE_BOX|wxRESIZE_BORDER) { // Create a self-managing shared_ptr for weak_ptr support // Note: This creates a self-reference, so we need to break it in destructor self_ptr = std::shared_ptr(this, [](NetworkTestDialog*) { /* custom deleter - do nothing, object is stack-allocated */ }); weak_this = self_ptr; this->SetBackgroundColour(wxColour(255, 255, 255)); this->SetSizeHints(wxDefaultSize, wxDefaultSize); wxBoxSizer* main_sizer; main_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* top_sizer = create_top_sizer(this); main_sizer->Add(top_sizer, 0, wxEXPAND, 5); wxBoxSizer* info_sizer = create_info_sizer(this); main_sizer->Add(info_sizer, 0, wxEXPAND, 5); wxBoxSizer* content_sizer = create_content_sizer(this); main_sizer->Add(content_sizer, 0, wxEXPAND, 5); wxBoxSizer* result_sizer = create_result_sizer(this); main_sizer->Add(result_sizer, 1, wxEXPAND, 5); set_default(); init_bind(); this->SetSizer(main_sizer); this->Layout(); this->Centre(wxBOTH); wxGetApp().UpdateDlgDarkUI(this); } wxBoxSizer* NetworkTestDialog::create_top_sizer(wxWindow* parent) { auto sizer = new wxBoxSizer(wxVERTICAL); auto line_sizer = new wxBoxSizer(wxHORIZONTAL); btn_start = new Button(this, _L("Start Test Multi-Thread")); btn_start->SetStyle(ButtonStyle::Confirm, ButtonType::Window); line_sizer->Add(btn_start, 0, wxALL, 5); btn_start_sequence = new Button(this, _L("Start Test Single-Thread")); btn_start_sequence->SetStyle(ButtonStyle::Regular, ButtonType::Window); line_sizer->Add(btn_start_sequence, 0, wxALL, 5); btn_download_log = new Button(this, _L("Export Log")); btn_download_log->SetStyle(ButtonStyle::Regular, ButtonType::Window); line_sizer->Add(btn_download_log, 0, wxALL, 5); btn_download_log->Hide(); btn_clear_log = new Button(this, _L("Clear Log")); btn_clear_log->SetStyle(ButtonStyle::Regular, ButtonType::Window); line_sizer->Add(btn_clear_log, 0, wxALL, 5); btn_start->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent &evt) { if (auto self = weak_this.lock()) { self->start_all_job(); } }); btn_start_sequence->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent &evt) { if (auto self = weak_this.lock()) { self->start_all_job_sequence(); } }); btn_clear_log->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent &evt) { if (auto self = weak_this.lock()) { if (self->txt_log) { self->txt_log->Clear(); } } }); sizer->Add(line_sizer, 0, wxEXPAND, 5); return sizer; } wxBoxSizer* NetworkTestDialog::create_info_sizer(wxWindow* parent) { auto sizer = new wxBoxSizer(wxVERTICAL); text_basic_info = new wxStaticText(this, wxID_ANY, _L("Basic Info"), wxDefaultPosition, wxDefaultSize, 0); text_basic_info->Wrap(-1); sizer->Add(text_basic_info, 0, wxALL, 5); wxBoxSizer* version_sizer = new wxBoxSizer(wxHORIZONTAL); text_version_title = new wxStaticText(this, wxID_ANY, _L("Snapmaker Orca Version:"), wxDefaultPosition, wxDefaultSize, 0); text_version_title->Wrap(-1); version_sizer->Add(text_version_title, 0, wxALL, 5); wxString text_version = get_studio_version(); text_version_val = new wxStaticText(this, wxID_ANY, text_version, wxDefaultPosition, wxDefaultSize, 0); text_version_val->Wrap(-1); version_sizer->Add(text_version_val, 0, wxALL, 5); sizer->Add(version_sizer, 1, wxEXPAND, 5); wxBoxSizer* sys_sizer = new wxBoxSizer(wxHORIZONTAL); txt_sys_info_title = new wxStaticText(this, wxID_ANY, _L("System Version:"), wxDefaultPosition, wxDefaultSize, 0); txt_sys_info_title->Wrap(-1); sys_sizer->Add(txt_sys_info_title, 0, wxALL, 5); txt_sys_info_value = new wxStaticText(this, wxID_ANY, get_os_info(), wxDefaultPosition, wxDefaultSize, 0); txt_sys_info_value->Wrap(-1); sys_sizer->Add(txt_sys_info_value, 0, wxALL, 5); sizer->Add(sys_sizer, 1, wxEXPAND, 5); wxBoxSizer* line_sizer = new wxBoxSizer(wxHORIZONTAL); txt_dns_info_title = new wxStaticText(this, wxID_ANY, _L("DNS Server:"), wxDefaultPosition, wxDefaultSize, 0); txt_dns_info_title->Wrap(-1); txt_dns_info_title->Hide(); line_sizer->Add(txt_dns_info_title, 0, wxALL, 5); txt_dns_info_value = new wxStaticText(this, wxID_ANY, get_dns_info(), wxDefaultPosition, wxDefaultSize, 0); txt_dns_info_value->Hide(); line_sizer->Add(txt_dns_info_value, 0, wxALL, 5); sizer->Add(line_sizer, 1, wxEXPAND, 5); return sizer; } wxBoxSizer* NetworkTestDialog::create_content_sizer(wxWindow* parent) { auto sizer = new wxBoxSizer(wxVERTICAL); wxFlexGridSizer* grid_sizer; grid_sizer = new wxFlexGridSizer(0, 3, 0, 0); grid_sizer->SetFlexibleDirection(wxBOTH); grid_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED); btn_link = new Button(this, _L("Test Snapmaker Orca(GitHub)")); btn_link->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_link, 0, wxEXPAND | wxALL, 5); text_link_title = new wxStaticText(this, wxID_ANY, _L("Test Snapmaker Orca(GitHub):"), wxDefaultPosition, wxDefaultSize, 0); text_link_title->Wrap(-1); grid_sizer->Add(text_link_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_link_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_link_val->Wrap(-1); grid_sizer->Add(text_link_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); btn_bing = new Button(this, _L("Test bing.com")); btn_bing->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_bing, 0, wxEXPAND | wxALL, 5); text_bing_title = new wxStaticText(this, wxID_ANY, _L("Test bing.com:"), wxDefaultPosition, wxDefaultSize, 0); text_bing_title->Wrap(-1); grid_sizer->Add(text_bing_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_bing_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_bing_val->Wrap(-1); grid_sizer->Add(text_bing_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); // LAN Device Test btn_lan_mqtt = new Button(this, _L("Test LAN Device")); btn_lan_mqtt->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_lan_mqtt, 0, wxEXPAND | wxALL, 5); text_lan_mqtt_title = new wxStaticText(this, wxID_ANY, _L("Test LAN Device:"), wxDefaultPosition, wxDefaultSize, 0); text_lan_mqtt_title->Wrap(-1); grid_sizer->Add(text_lan_mqtt_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_lan_mqtt_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_lan_mqtt_val->Wrap(-1); grid_sizer->Add(text_lan_mqtt_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); // Cloud Server Test btn_cloud_mqtt = new Button(this, _L("Test Cloud Server")); btn_cloud_mqtt->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_cloud_mqtt, 0, wxEXPAND | wxALL, 5); text_cloud_mqtt_title = new wxStaticText(this, wxID_ANY, _L("Test Cloud Server:"), wxDefaultPosition, wxDefaultSize, 0); text_cloud_mqtt_title->Wrap(-1); grid_sizer->Add(text_cloud_mqtt_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_cloud_mqtt_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_cloud_mqtt_val->Wrap(-1); grid_sizer->Add(text_cloud_mqtt_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); // Login API Test btn_login_api = new Button(this, _L("Test Login API")); btn_login_api->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_login_api, 0, wxEXPAND | wxALL, 5); text_login_api_title = new wxStaticText(this, wxID_ANY, _L("Test Login API:"), wxDefaultPosition, wxDefaultSize, 0); text_login_api_title->Wrap(-1); grid_sizer->Add(text_login_api_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_login_api_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_login_api_val->Wrap(-1); grid_sizer->Add(text_login_api_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); // Upload API Test btn_upload_api = new Button(this, _L("Test Upload API")); btn_upload_api->SetStyle(ButtonStyle::Regular, ButtonType::Window); grid_sizer->Add(btn_upload_api, 0, wxEXPAND | wxALL, 5); text_upload_api_title = new wxStaticText(this, wxID_ANY, _L("Test Upload API:"), wxDefaultPosition, wxDefaultSize, 0); text_upload_api_title->Wrap(-1); grid_sizer->Add(text_upload_api_title, 0, wxALIGN_RIGHT | wxALL | wxALIGN_CENTER_VERTICAL, 5); text_upload_api_val = new wxStaticText(this, wxID_ANY, _L("N/A"), wxDefaultPosition, wxDefaultSize, 0); text_upload_api_val->Wrap(-1); grid_sizer->Add(text_upload_api_val, 0, wxALL | wxALIGN_CENTER_VERTICAL, 5); sizer->Add(grid_sizer, 1, wxEXPAND, 5); btn_link->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_github_thread(); } }); btn_bing->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_bing_thread(); } }); btn_lan_mqtt->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_lan_mqtt_thread(); } }); btn_cloud_mqtt->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_cloud_mqtt_thread(); } }); btn_login_api->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_login_api_thread(); } }); btn_upload_api->Bind(wxEVT_BUTTON, [weak_this = weak_this](wxCommandEvent& evt) { if (auto self = weak_this.lock()) { self->start_test_upload_api_thread(); } }); return sizer; } wxBoxSizer* NetworkTestDialog::create_result_sizer(wxWindow* parent) { auto sizer = new wxBoxSizer(wxVERTICAL); text_result = new wxStaticText(this, wxID_ANY, _L("Log Info"), wxDefaultPosition, wxDefaultSize, 0); text_result->Wrap(-1); sizer->Add(text_result, 0, wxALL, 5); txt_log = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE); sizer->Add(txt_log, 1, wxALL | wxEXPAND, 5); return sizer; } NetworkTestDialog::~NetworkTestDialog() { m_closing.store(true); m_download_cancel = true; cleanup_threads(); // Small delay to allow any in-flight update_status calls to complete // before destroying the shared_ptr control block boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); // Break the self-reference to avoid issues self_ptr.reset(); } void NetworkTestDialog::init_bind() { Bind(EVT_UPDATE_RESULT, [weak_this = weak_this](wxCommandEvent& evt) { auto self = weak_this.lock(); if (!self || self->m_closing.load()) return; if (evt.GetInt() == TEST_ORCA_JOB) { self->text_link_val->SetLabelText(evt.GetString()); } else if (evt.GetInt() == TEST_BING_JOB) { self->text_bing_val->SetLabelText(evt.GetString()); } else if (evt.GetInt() == TEST_LAN_MQTT_JOB) { self->text_lan_mqtt_val->SetLabelText(evt.GetString()); } else if (evt.GetInt() == TEST_CLOUD_MQTT_JOB) { self->text_cloud_mqtt_val->SetLabelText(evt.GetString()); } else if (evt.GetInt() == TEST_LOGIN_API_JOB) { self->text_login_api_val->SetLabelText(evt.GetString()); } else if (evt.GetInt() == TEST_UPLOAD_API_JOB) { self->text_upload_api_val->SetLabelText(evt.GetString()); } std::time_t t = std::time(0); std::tm* now_time = std::localtime(&t); std::stringstream buf; buf << std::put_time(now_time, "%a %b %d %H:%M:%S"); wxString info = wxString(buf.str()) + ": " + evt.GetString() + "\n"; try { if (!self->m_closing.load() && self->txt_log) { self->txt_log->AppendText(info); } } catch (std::exception& e) { BOOST_LOG_TRIVIAL(error) << "Unkown Exception in print_log, exception=" << e.what(); return; } catch (...) { BOOST_LOG_TRIVIAL(error) << "Unkown Exception in print_log"; return; } return; }); Bind(wxEVT_CLOSE_WINDOW, &NetworkTestDialog::on_close, this); } wxString NetworkTestDialog::get_os_info() { int major = 0, minor = 0, micro = 0; wxGetOsVersion(&major, &minor, µ); std::string os_version = (boost::format("%1%.%2%.%3%") % major % minor % micro).str(); wxString text_sys_version = wxGetOsDescription() + wxString::Format("%d.%d.%d", major, minor, micro); return text_sys_version; } wxString NetworkTestDialog::get_dns_info() { return NA_STR; } void NetworkTestDialog::start_all_job() { start_test_github_thread(); start_test_bing_thread(); start_test_lan_mqtt_thread(); start_test_cloud_mqtt_thread(); start_test_login_api_thread(); start_test_upload_api_thread(); } void NetworkTestDialog::start_all_job_sequence() { if (m_sequence_job != nullptr) { update_status(-1, "Sequence test already running, please wait..."); return; } // 在序列测试开始前,先弹出输入框获取局域网设备IP wxTextEntryDialog dlg(this, _L("Please enter the LAN device IP address for testing (leave empty to skip):"), _L("LAN Device Test - Sequence Mode"), "192.168.1.1", wxOK | wxCANCEL); wxString device_ip; if (dlg.ShowModal() == wxID_OK) { device_ip = dlg.GetValue().Trim(); } m_sequence_job = new boost::thread([weak_this = weak_this, device_ip] { auto self = weak_this.lock(); if (!self) return; self->update_status(-1, "========================================"); self->update_status(-1, "Start sequence test (single-thread mode)"); self->update_status(-1, "========================================"); self->update_status(-1, ""); self->start_test_url(TEST_BING_JOB, "Bing", "http://www.bing.com"); if (self->m_closing.load()) return; self->update_status(-1, ""); self->start_test_url(TEST_ORCA_JOB, "Snapmaker Orca(GitHub)", "https://github.com/Snapmaker/OrcaSlicer"); if (self->m_closing.load()) return; // 如果用户输入了局域网设备IP,则进行测试 if (!device_ip.IsEmpty()) { self->update_status(-1, ""); self->start_test_telnet(TEST_LAN_MQTT_JOB, "LAN Device", device_ip, 1884); if (self->m_closing.load()) return; } // 测试云服务器 wxString cloud_server = self->get_cloud_server_address(); if (!cloud_server.IsEmpty()) { self->update_status(-1, ""); self->start_test_telnet(TEST_CLOUD_MQTT_JOB, "Cloud Server", cloud_server, 8883); } if (self->m_closing.load()) return; // 测试登录API self->update_status(-1, ""); auto app_config = wxGetApp().app_config; std::string region = app_config->get("region"); wxString login_api_url = (region == "Chinese Mainland" || region == "China") ? "https://id.snapmaker.cn" : "https://id.snapmaker.com"; self->start_test_url(TEST_LOGIN_API_JOB, "Login API", login_api_url); if (self->m_closing.load()) return; // 测试上传API self->update_status(-1, ""); wxString upload_api_url = (region == "Chinese Mainland" || region == "China") ? "https://public.resource.snapmaker.cn" : "https://public.resource.snapmaker.com"; self->start_test_url(TEST_UPLOAD_API_JOB, "Upload API", upload_api_url); if (self->m_closing.load()) return; self->update_status(-1, ""); self->update_status(-1, "========================================"); self->update_status(-1, "Sequence test completed"); self->update_status(-1, "========================================"); }); } void NetworkTestDialog::start_test_url(TestJob job, wxString name, wxString url) { m_in_testing[job].store(true); update_status(-1, ""); update_status(-1, "========================================"); wxString info = "test " + name + " start..."; update_status(job, info); Slic3r::Http http = Slic3r::Http::get(url.ToStdString()); info = "[test " + name + "]: url=" + url; update_status(-1, info); update_status(-1, ""); int result = -1; auto weak_self = weak_this; http.timeout_max(10) .on_complete([weak_self, &result, job](std::string body, unsigned status) { auto self = weak_self.lock(); if (!self || self->m_closing.load()) return; try { if (status == 200) { result = 0; } // Upload API: 403 is OK (HTTPS resource with permission check) else if (job == TEST_UPLOAD_API_JOB && status == 403) { result = 0; } } catch (...) { ; } }) .on_ip_resolve([weak_self, name, job](std::string ip) { auto self = weak_self.lock(); if (!self || self->m_closing.load()) return; wxString ip_report = "test " + name + " ip resolved = " + wxString::FromUTF8(ip); self->update_status(job, ip_report); }) .on_error([weak_self, name, job](std::string body, std::string error, unsigned int status) { auto self = weak_self.lock(); if (!self || self->m_closing.load()) return; // Upload API: 403 is OK (HTTPS resource with permission check) if (job == TEST_UPLOAD_API_JOB && status == 403) { self->update_status(job, "test " + name + " ok (403 - access restricted, but server reachable)"); return; } wxString info = wxString::Format("status=%u, body=", status) + wxString::FromUTF8(body) + ", error=" + wxString::FromUTF8(error); self->update_status(job, "test " + name + " failed"); self->update_status(-1, info); }).perform_sync(); if (result == 0) { update_status(job, "test " + name + " ok"); } update_status(-1, "========================================"); update_status(-1, ""); m_in_testing[job].store(false); } void NetworkTestDialog::start_test_ping_thread() { test_job[TEST_PING_JOB] = new boost::thread([weak_this = weak_this] { auto self = weak_this.lock(); if (!self) return; self->m_in_testing[TEST_PING_JOB].store(true); self->m_in_testing[TEST_PING_JOB].store(false); }); } void NetworkTestDialog::start_test_ping(wxString server, TestJob job) { update_status(-1, ""); update_status(-1, "Starting ping test to " + server + "..."); try { #ifdef _WIN32 // Windows: ping -n 4 wxString ping_cmd = "ping -n 4 " + server; // Windows 可以使用 wxExecute wxArrayString output; wxArrayString errors; long exec_flags = wxEXEC_SYNC | wxEXEC_NODISABLE | wxEXEC_HIDE_CONSOLE; long result = wxExecute(ping_cmd, output, errors, exec_flags); #else // Linux/Mac: ping -c 4 // 使用 popen() 而不是 wxExecute,因为 wxExecute 在 macOS 上不能在异步线程中使用 std::string ping_cmd = "ping -c 4 " + server.ToStdString(); wxArrayString output; long result = -1; FILE* pipe = popen(ping_cmd.c_str(), "r"); if (pipe != nullptr) { char buffer[1024]; while (fgets(buffer, sizeof(buffer), pipe) != nullptr) { // 转换为 wxString 并添加到 output wxString line = wxString::FromUTF8(buffer); line.Trim(); // 移除换行符 if (!line.IsEmpty()) { output.Add(line); } } int pclose_result = pclose(pipe); // pclose 返回的是命令的退出状态,需要检查 WEXITSTATUS if (WIFEXITED(pclose_result)) { result = WEXITSTATUS(pclose_result); } else { result = -1; } } #endif if (result == 0 && output.GetCount() > 0) { // 解析ping输出(不输出每一行,减少UI更新) bool found_rtt = false; wxString rtt_info; int received = 0; int sent = 4; // macOS需要收集所有time值来计算平均值 std::vector time_values; for (size_t i = 0; i < output.GetCount(); i++) { wxString line = output[i]; // 完全不输出ping详细日志,只解析数据 #ifdef _WIN32 // Windows: 通用解析(支持所有语言) // 策略1: 查找统计行(包含多个 "= 数字" 的模式) // 这一行通常是:数据包: 已发送 = 4,已接收 = 4,丢失 = 0 // 或:Packets: Sent = 4, Received = 4, Lost = 0 // 特征:一行中有至少3个 "= 数字" 模式 if (received == 0 && line.Contains("=") && !line.Contains("TTL")) { // 提取所有 "= 数字" 模式 wxArrayString numbers; size_t pos = 0; while (pos < line.Length()) { // 查找等号 size_t eq_pos = line.find('=', pos); if (eq_pos == wxString::npos) break; // 跳过等号后的空格 size_t num_start = eq_pos + 1; while (num_start < line.Length() && (line[num_start] == ' ' || line[num_start] == '\t')) { num_start++; } // 提取数字 wxString num_str; size_t num_pos = num_start; while (num_pos < line.Length() && wxIsdigit(line[num_pos])) { num_str += line[num_pos]; num_pos++; } if (!num_str.IsEmpty()) { numbers.Add(num_str); } pos = num_pos + 1; } // 统计行特征:至少有3个数字(sent, received, lost) // 第2个数字是received(已接收) if (numbers.GetCount() >= 3) { long val; if (numbers[1].ToLong(&val)) { received = val; } } } // 策略2: 查找平均RTT行(包含 "平均" 或 "Average" 或多个ms) // 格式:最短 = 42ms,最长 = 45ms,平均 = 43ms // 或:Minimum = 42ms, Maximum = 45ms, Average = 43ms if (!found_rtt) { // 检查是否是平均/Average行(包含多个ms的行) int ms_count = 0; size_t search_pos = 0; while ((search_pos = line.find("ms", search_pos)) != wxString::npos) { ms_count++; search_pos += 2; } // 如果一行中有3个ms,很可能是RTT统计行 // 提取最后一个数字作为平均RTT(避免显示乱码) if (ms_count >= 3) { wxString avg_rtt; // 从后往前找最后一个 "数字ms" 模式 for (int i = line.Length() - 1; i >= 1; i--) { if (line[i] == 's' && line[i-1] == 'm') { // 找到ms,往前提取数字 wxString num_str; int j = i - 2; while (j >= 0 && (wxIsdigit(line[j]) || line[j] == '.')) { num_str = line[j] + num_str; j--; } if (!num_str.IsEmpty()) { avg_rtt = num_str; break; } } } if (!avg_rtt.IsEmpty()) { found_rtt = true; rtt_info = avg_rtt + "ms (average)"; } } } #else // macOS格式: "64 bytes from 13.35.32.150: icmp_seq=519 ttl=244 time=57.030 ms" // 需要从每行提取 time=XX.XXX ms 并收集所有值 if (line.Contains("time=")) { int time_pos = line.Find("time="); if (time_pos != wxNOT_FOUND) { wxString after_time = line.Mid(time_pos + 5); // 跳过 "time=" wxString time_str; // 提取数字(可能包含小数点) for (size_t j = 0; j < after_time.Length(); j++) { wxChar c = after_time[j]; if (wxIsdigit(c) || c == '.') { time_str += c; } else if (c == ' ' || c == 'm') { // 遇到空格或 'm' (ms) 停止 break; } } if (!time_str.IsEmpty()) { double time_val; if (time_str.ToDouble(&time_val)) { time_values.push_back(time_val); found_rtt = true; } } } } // Linux格式: "rtt min/avg/max/mdev = 1.234/5.678/9.012/1.234 ms" if (!found_rtt && line.Contains("rtt") && line.Contains("avg")) { found_rtt = true; // 提取平均值 int avg_pos = line.Find("avg"); if (avg_pos != wxNOT_FOUND) { wxString after_avg = line.Mid(avg_pos + 3); wxString num_str; size_t i = 0; // 跳过空格和等号 while (i < after_avg.Length() && (after_avg[i] == ' ' || after_avg[i] == '\t' || after_avg[i] == '=')) { i++; } // 提取数字直到遇到斜杠或空格 while (i < after_avg.Length() && (wxIsdigit(after_avg[i]) || after_avg[i] == '.')) { num_str += after_avg[i]; i++; } if (!num_str.IsEmpty()) { rtt_info = num_str + "ms (average)"; } else { rtt_info = line; } } else { rtt_info = line; } } // 统计格式: "4 packets transmitted, 4 received" (Linux/macOS通用) if (line.Contains("packets transmitted") && line.Contains("received")) { int pos_received = line.Find(" received"); if (pos_received != wxNOT_FOUND) { wxString before = line.Mid(0, pos_received); wxString num_str; for (int j = before.Length() - 1; j >= 0; j--) { if (wxIsdigit(before[j])) { num_str = before[j] + num_str; } else if (!num_str.IsEmpty()) { break; } } long val; if (!num_str.IsEmpty() && num_str.ToLong(&val)) { received = val; } } } // macOS 可能还有统计行: "round-trip min/avg/max/stddev = 1.234/5.678/9.012/1.234 ms" if (!found_rtt && line.Contains("round-trip") && line.Contains("avg")) { found_rtt = true; int avg_pos = line.Find("avg"); if (avg_pos != wxNOT_FOUND) { wxString after_avg = line.Mid(avg_pos + 3); wxString num_str; size_t i = 0; while (i < after_avg.Length() && (after_avg[i] == ' ' || after_avg[i] == '\t' || after_avg[i] == '=')) { i++; } while (i < after_avg.Length() && (wxIsdigit(after_avg[i]) || after_avg[i] == '.')) { num_str += after_avg[i]; i++; if (i < after_avg.Length() && after_avg[i] == '/') break; // 遇到斜杠停止 } if (!num_str.IsEmpty()) { rtt_info = num_str + "ms (average)"; } else { rtt_info = line; } } else { rtt_info = line; } } #endif } // 如果收集了多个time值(macOS),计算平均值 if (!time_values.empty()) { double sum = 0.0; for (double val : time_values) { sum += val; } double avg = sum / time_values.size(); rtt_info = wxString::Format("%.3fms (average from %zu samples)", avg, time_values.size()); } else if (found_rtt && rtt_info.IsEmpty()) { rtt_info = "N/A"; } // 计算丢包率 int packet_loss = ((sent - received) * 100) / sent; // 一次性输出所有结果,减少UI更新次数 wxString summary = "\n"; // 检查是否成功解析 bool parse_success = (found_rtt || received > 0); if (parse_success) { // 成功解析了ping结果 if (found_rtt && !rtt_info.IsEmpty()) { summary += "[OK] Ping round-trip time: " + rtt_info + "\n"; } summary += wxString::Format("Packet loss: %d%% (%d/%d received)\n", packet_loss, received, sent); if (received > 0) { summary += wxString::Format("[OK] Ping test successful (%d/%d packets)", received, sent); } else { summary += "[FAIL] Ping test failed - 100% packet loss"; } } else { // 无法解析ping输出 summary += "[WARN] Cannot parse ping output - unusual format\n"; summary += "[INFO] TCP connection test can still verify network connectivity"; } update_status(-1, summary); } else { wxString error_summary = "\n[FAIL] Ping command failed or timed out"; #ifdef _WIN32 for (size_t i = 0; i < errors.GetCount(); i++) { error_summary += "\nError: " + errors[i]; } #else if (output.GetCount() > 0) { error_summary += "\nOutput:\n"; for (size_t i = 0; i < output.GetCount() && i < 5; i++) { error_summary += " " + output[i] + "\n"; } } else { error_summary += "\nNo output from ping command"; } #endif update_status(-1, error_summary); } } catch (const std::exception& e) { update_status(-1, wxString("\nPing exception: ") + wxString(e.what())); } catch (...) { update_status(-1, "\nPing test failed: unknown error"); } } void NetworkTestDialog::start_test_telnet(TestJob job, wxString name, wxString server, int port) { m_in_testing[job].store(true); // 添加分隔空行 update_status(-1, ""); update_status(-1, "========================================"); wxString info = "test " + name + " start..."; update_status(job, info); try { info = "[test " + name + "]: server=" + server + wxString::Format(", port=%d", port); update_status(-1, info); update_status(-1, ""); // 空行 // ============================================ // 第一步: Ping测试 - 测量网络层RTT // ============================================ update_status(-1, "--- Step 1: Network Layer Test (ICMP Ping) ---"); start_test_ping(server, job); if (m_closing.load()) { m_in_testing[job].store(false); return; } // 添加步骤间空行 update_status(-1, ""); // ============================================ // 第二步: TCP连接测试 - 验证服务可用性 // ============================================ update_status(-1, "--- Step 2: Transport Layer Test (TCP Connection) ---"); // 记录开始时间 auto start_time = std::chrono::high_resolution_clock::now(); boost::asio::io_context io_context; boost::asio::ip::tcp::socket socket(io_context); boost::asio::ip::tcp::resolver resolver(io_context); bool success = false; std::string error_msg; try { // 解析主机名 auto resolve_start = std::chrono::high_resolution_clock::now(); boost::asio::ip::tcp::resolver::results_type endpoints; try { endpoints = resolver.resolve(server.ToStdString(), std::to_string(port)); auto resolve_end = std::chrono::high_resolution_clock::now(); auto resolve_time = std::chrono::duration_cast(resolve_end - resolve_start).count(); update_status(-1, wxString::Format("DNS resolve time: %lld ms", resolve_time)); } catch (const boost::system::system_error& e) { error_msg = (wxString("DNS resolve failed: ") + wxString(e.what())).ToStdString(); throw; } // 连接到服务器 auto connect_start = std::chrono::high_resolution_clock::now(); boost::system::error_code ec; long long connect_time = 0; // 尝试连接到所有解析出的endpoint bool connected = false; for (auto& endpoint : endpoints) { if (m_closing.load()) break; socket.close(ec); socket.connect(endpoint, ec); if (!ec) { connected = true; auto connect_end = std::chrono::high_resolution_clock::now(); connect_time = std::chrono::duration_cast(connect_end - connect_start).count(); update_status(job, "test " + name + " connected"); update_status(-1, wxString::Format("[OK] TCP connection established in %lld ms", connect_time)); break; } } if (!connected) { error_msg = (wxString("Connection failed: ") + wxString(ec.message())).ToStdString(); throw boost::system::system_error(ec); } // 计算总时间 auto end_time = std::chrono::high_resolution_clock::now(); auto total_time = std::chrono::duration_cast(end_time - start_time).count(); update_status(-1, wxString::Format("Total test time: %lld ms", total_time)); // Calculate connection quality grade based on RTT int grade; wxString grade_desc; wxString grade_icon; if (connect_time <= 50) { grade = 1; grade_desc = "Excellent"; grade_icon = "[Level 1]"; } else if (connect_time <= 100) { grade = 2; grade_desc = "Good"; grade_icon = "[Level 2]"; } else if (connect_time <= 400) { grade = 3; grade_desc = "Fair"; grade_icon = "[Level 3]"; } else { grade = 4; grade_desc = "Poor"; grade_icon = "[Level 4]"; } // 添加空行 update_status(-1, ""); update_status(-1, "--- Test Summary ---"); update_status(-1, "[OK] Network Layer: Ping test completed (see RTT above)"); update_status(-1, wxString::Format("[OK] Transport Layer: TCP port %d is open and accepting connections", port)); update_status(-1, wxString::Format("Connection Quality: %s %s (RTT: %lld ms)", grade_icon, grade_desc, connect_time)); update_status(job, wxString::Format("test %s ok (%s - %s, RTT: %lld ms)", name, grade_icon, grade_desc, connect_time)); success = true; // 关闭连接 socket.close(ec); } catch (const boost::system::system_error& e) { if (error_msg.empty()) { error_msg = e.what(); } update_status(-1, ""); update_status(-1, "[FAIL] TCP connection error: " + wxString::FromUTF8(error_msg)); update_status(-1, "Connection Quality: [Level 5] Failed (timeout or unreachable)"); update_status(job, "test " + name + " failed ([Level 5] - timeout or unreachable)"); } catch (const std::exception& e) { update_status(-1, ""); update_status(-1, wxString("Exception: ") + wxString(e.what())); update_status(-1, "Connection Quality: [Level 5] Failed (exception)"); update_status(job, "test " + name + " failed ([Level 5] - exception)"); } } catch (...) { update_status(-1, ""); update_status(-1, "Connection Quality: [Level 5] Failed (unknown error)"); update_status(job, "test " + name + " failed ([Level 5] - unknown error)"); } update_status(-1, "========================================"); update_status(-1, ""); // 测试结束后的空行 m_in_testing[job].store(false); } void NetworkTestDialog::start_test_lan_mqtt_thread() { if (m_in_testing[TEST_LAN_MQTT_JOB].load()) { return; } // 弹出对话框让用户输入IP地址 wxTextEntryDialog dlg(this, _L("Please enter the device IP address:"), _L("LAN Device Test"), "192.168.1.1", wxOK | wxCANCEL); if (dlg.ShowModal() != wxID_OK) { return; } wxString device_ip = dlg.GetValue().Trim(); if (device_ip.IsEmpty()) { update_status(TEST_LAN_MQTT_JOB, "Invalid IP address"); return; } if (test_job[TEST_LAN_MQTT_JOB] != nullptr && test_job[TEST_LAN_MQTT_JOB]->joinable()) { test_job[TEST_LAN_MQTT_JOB]->join(); delete test_job[TEST_LAN_MQTT_JOB]; test_job[TEST_LAN_MQTT_JOB] = nullptr; } test_job[TEST_LAN_MQTT_JOB] = new boost::thread([weak_this = weak_this, device_ip] { auto self = weak_this.lock(); if (!self) return; // 测试局域网设备 - 端口默认1884 self->start_test_telnet(TEST_LAN_MQTT_JOB, "LAN Device", device_ip, 1884); }); } wxString NetworkTestDialog::get_cloud_server_address() { auto app_config = wxGetApp().app_config; std::string region = app_config->get("region"); if (region == "Chinese Mainland") return "a1su7rk2r6cmbq.ats.iot.cn-north-1.amazonaws.com.cn"; else return "a1pr8yczi3n0se-ats.iot.us-west-1.amazonaws.com"; } void NetworkTestDialog::start_test_cloud_mqtt_thread() { if (m_in_testing[TEST_CLOUD_MQTT_JOB].load()) { return; } wxString cloud_server = get_cloud_server_address(); if (cloud_server.IsEmpty()) { update_status(TEST_CLOUD_MQTT_JOB, "Cloud server not configured"); update_status(-1, "Please configure cloud server address in get_cloud_server_address()"); return; } if (test_job[TEST_CLOUD_MQTT_JOB] != nullptr && test_job[TEST_CLOUD_MQTT_JOB]->joinable()) { test_job[TEST_CLOUD_MQTT_JOB]->join(); delete test_job[TEST_CLOUD_MQTT_JOB]; test_job[TEST_CLOUD_MQTT_JOB] = nullptr; } test_job[TEST_CLOUD_MQTT_JOB] = new boost::thread([weak_this = weak_this, cloud_server] { auto self = weak_this.lock(); if (!self) return; // 测试云服务器 - 使用telnet方式,端口8883 self->start_test_telnet(TEST_CLOUD_MQTT_JOB, "Cloud Server", cloud_server, 8883); }); } void NetworkTestDialog::start_test_github_thread() { if (m_in_testing[TEST_ORCA_JOB].load()) return; if (test_job[TEST_ORCA_JOB] != nullptr && test_job[TEST_ORCA_JOB]->joinable()) { test_job[TEST_ORCA_JOB]->join(); delete test_job[TEST_ORCA_JOB]; test_job[TEST_ORCA_JOB] = nullptr; } test_job[TEST_ORCA_JOB] = new boost::thread([weak_this = weak_this] { auto self = weak_this.lock(); if (!self) return; self->start_test_url(TEST_ORCA_JOB, "Snapmaker Orca(GitHub)", "https://github.com/Snapmaker/OrcaSlicer"); }); } void NetworkTestDialog::start_test_bing_thread() { if (m_in_testing[TEST_BING_JOB].load()) return; if (test_job[TEST_BING_JOB] != nullptr && test_job[TEST_BING_JOB]->joinable()) { test_job[TEST_BING_JOB]->join(); delete test_job[TEST_BING_JOB]; test_job[TEST_BING_JOB] = nullptr; } test_job[TEST_BING_JOB] = new boost::thread([weak_this = weak_this] { auto self = weak_this.lock(); if (!self) return; self->start_test_url(TEST_BING_JOB, "Bing", "http://www.bing.com"); }); } void NetworkTestDialog::start_test_login_api_thread() { if (m_in_testing[TEST_LOGIN_API_JOB].load()) return; if (test_job[TEST_LOGIN_API_JOB] != nullptr && test_job[TEST_LOGIN_API_JOB]->joinable()) { test_job[TEST_LOGIN_API_JOB]->join(); delete test_job[TEST_LOGIN_API_JOB]; test_job[TEST_LOGIN_API_JOB] = nullptr; } test_job[TEST_LOGIN_API_JOB] = new boost::thread([weak_this = weak_this] { auto self = weak_this.lock(); if (!self) return; auto app_config = wxGetApp().app_config; std::string region = app_config->get("region"); wxString login_api_url = (region == "Chinese Mainland") ? "https://id.snapmaker.cn" : "https://id.snapmaker.com"; self->start_test_url(TEST_LOGIN_API_JOB, "Login API", login_api_url); }); } void NetworkTestDialog::start_test_upload_api_thread() { if (m_in_testing[TEST_UPLOAD_API_JOB].load()) return; if (test_job[TEST_UPLOAD_API_JOB] != nullptr && test_job[TEST_UPLOAD_API_JOB]->joinable()) { test_job[TEST_UPLOAD_API_JOB]->join(); delete test_job[TEST_UPLOAD_API_JOB]; test_job[TEST_UPLOAD_API_JOB] = nullptr; } test_job[TEST_UPLOAD_API_JOB] = new boost::thread([weak_this = weak_this] { auto self = weak_this.lock(); if (!self) return; auto app_config = wxGetApp().app_config; std::string region = app_config->get("region"); wxString upload_api_url = (region == "Chinese Mainland") ? "https://public.resource.snapmaker.cn" : "https://public.resource.snapmaker.com"; self->start_test_url(TEST_UPLOAD_API_JOB, "Upload API", upload_api_url); }); } void NetworkTestDialog::on_close(wxCloseEvent& event) { m_download_cancel = true; m_closing.store(true); cleanup_threads(); event.Skip(); } wxString NetworkTestDialog::get_studio_version() { return wxString(Snapmaker_VERSION); } void NetworkTestDialog::set_default() { for (int i = 0; i < TEST_JOB_MAX; i++) { test_job[i] = nullptr; m_in_testing[i].store(false); } m_sequence_job = nullptr; text_version_val->SetLabelText(get_studio_version()); txt_sys_info_value->SetLabelText(get_os_info()); txt_dns_info_value->SetLabelText(get_dns_info()); text_link_val->SetLabelText(NA_STR); text_bing_val->SetLabelText(NA_STR); text_lan_mqtt_val->SetLabelText(NA_STR); text_cloud_mqtt_val->SetLabelText(NA_STR); text_login_api_val->SetLabelText(NA_STR); text_upload_api_val->SetLabelText(NA_STR); m_download_cancel = false; m_closing.store(false); } void NetworkTestDialog::on_dpi_changed(const wxRect &suggested_rect) { ; } void NetworkTestDialog::update_status(int job_id, wxString info) { // Early exit if dialog is closing - don't access any members if (m_closing.load()) return; // Use try-catch to protect against accessing destroyed dialog try { // Double check before creating event if (m_closing.load()) return; auto evt = new wxCommandEvent(EVT_UPDATE_RESULT, this->GetId()); evt->SetString(info); evt->SetInt(job_id); // Triple check before queuing - race condition window is very small if (!m_closing.load()) { wxQueueEvent(this, evt); } else { delete evt; } } catch (...) { // Silently ignore if dialog was destroyed during operation } } void NetworkTestDialog::cleanup_threads() { // Clean up test job threads for (int i = 0; i < TEST_JOB_MAX; i++) { if (test_job[i] != nullptr) { if (test_job[i]->joinable()) { // Try to join with longer timeout (1000ms) to reduce chance of detach // Threads should check m_closing and exit promptly if (!test_job[i]->try_join_for(boost::chrono::milliseconds(1000))) { // Thread didn't finish in time, detach it to avoid blocking // The thread will check m_closing and should exit safely test_job[i]->detach(); BOOST_LOG_TRIVIAL(warning) << "Thread " << i << " didn't finish in time, detached"; } } delete test_job[i]; test_job[i] = nullptr; } } // Clean up sequence job thread if (m_sequence_job != nullptr) { if (m_sequence_job->joinable()) { // Try to join with longer timeout (1000ms) if (!m_sequence_job->try_join_for(boost::chrono::milliseconds(1000))) { // Thread didn't finish in time, detach it m_sequence_job->detach(); BOOST_LOG_TRIVIAL(warning) << "Sequence job thread didn't finish in time, detached"; } } delete m_sequence_job; m_sequence_job = nullptr; } } } // namespace GUI } // namespace Slic3r