diff --git a/connector/native/app_macos.mm b/connector/native/app_macos.mm index 0df8937..a1ea6df 100644 --- a/connector/native/app_macos.mm +++ b/connector/native/app_macos.mm @@ -79,13 +79,15 @@ using connector::ConnectorService; - (void)refreshStatus:(id)sender { (void)sender; - self.connectionButton.title = self.service && self.service->running() ? @"断开连接" : @"启动连接"; - if (self.startupError.length > 0) { + const BOOL ownsPort = self.service && self.service->running(); + const BOOL portOccupied = ownsPort || connector::connector_port_in_use(); + self.connectionButton.title = portOccupied ? @"停止连接" : @"启动连接"; + if (self.startupError.length > 0 && !portOccupied) { self.statusLabel.stringValue = [@"● 连接器未启动 · " stringByAppendingString:self.startupError]; self.statusLabel.textColor = NSColor.systemRedColor; return; } - if (!self.service || !self.service->running()) { + if (!portOccupied) { self.statusLabel.stringValue = @"● 连接器已断开"; self.statusLabel.textColor = NSColor.secondaryLabelColor; return; @@ -93,6 +95,11 @@ using connector::ConnectorService; const std::string spine = connector::find_spine_executable(); const BOOL showPathControls = spine.empty(); for (NSView* view in self.pathControls) view.hidden = !showPathControls; + if (!ownsPort) { + self.statusLabel.stringValue = @"● 端口 27843 正在运行 · 可停止连接"; + self.statusLabel.textColor = NSColor.systemGreenColor; + return; + } self.statusLabel.stringValue = spine.empty() ? @"● 连接器运行中 · 未找到 Spine" : @"● 连接器运行中 · Spine 已就绪"; self.statusLabel.textColor = spine.empty() ? NSColor.systemOrangeColor : NSColor.systemGreenColor; } @@ -102,6 +109,10 @@ using connector::ConnectorService; if (self.service && self.service->running()) { self.service->stop(); self.startupError = @""; + } else if (connector::connector_port_in_use()) { + std::string error; + connector::stop_connector_port(error); + self.startupError = [NSString stringWithUTF8String:error.c_str()]; } else if (self.service) { std::string error; self.service->start(error); diff --git a/connector/native/app_windows.cpp b/connector/native/app_windows.cpp index 44ec0ed..ef918cc 100644 --- a/connector/native/app_windows.cpp +++ b/connector/native/app_windows.cpp @@ -141,13 +141,18 @@ std::wstring field_text(HWND field) { } void refresh_status() { + const bool owns_port = service.running(); + const bool port_occupied = owns_port || connector::connector_port_in_use(); std::wstring status; - if (!startup_error.empty()) { + if (!startup_error.empty() && !port_occupied) { status = L"● 连接器未启动 · " + wide(startup_error); status_color = kErrorText; - } else if (!service.running()) { + } else if (!port_occupied) { status = L"● 连接器已断开"; status_color = kSecondaryText; + } else if (!owns_port) { + status = L"● 端口 27843 正在运行 · 可停止连接"; + status_color = kSuccessText; } else if (connector::find_spine_executable().empty()) { status = L"● 连接器运行中 · 未找到 Spine"; status_color = kWarningText; @@ -157,7 +162,7 @@ void refresh_status() { } SetWindowTextW(status_label, status.c_str()); InvalidateRect(status_label, nullptr, TRUE); - SetWindowTextW(connection_button, service.running() ? L"断开连接" : L"启动连接"); + SetWindowTextW(connection_button, port_occupied ? L"停止连接" : L"启动连接"); const bool show_path = connector::find_spine_executable().empty(); for (HWND control : {path_label, path_field, browse_button, automatic_button, save_button}) { if (control) ShowWindow(control, show_path ? SW_SHOW : SW_HIDE); @@ -246,6 +251,9 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wparam, LPARAM lp if (service.running()) { service.stop(); startup_error.clear(); + } else if (connector::connector_port_in_use()) { + startup_error.clear(); + connector::stop_connector_port(startup_error); } else { startup_error.clear(); service.start(startup_error); diff --git a/connector/native/platform.hpp b/connector/native/platform.hpp index bdae1ed..78b2bd6 100644 --- a/connector/native/platform.hpp +++ b/connector/native/platform.hpp @@ -20,6 +20,10 @@ std::filesystem::path to_path(const std::string& value); std::string platform_name(); std::string find_spine_executable(); bool valid_spine_executable(const std::string& path); +// 查询/释放连接器固定使用的本地端口。释放操作针对当前监听进程, +// 使用户可以从新打开的连接器窗口停止遗留实例或其他占用者。 +bool connector_port_in_use(); +bool stop_connector_port(std::string& error); ProcessResult run_process(const std::string& executable, const std::vector& arguments, const std::filesystem::path& working_directory, diff --git a/connector/native/process_posix.cpp b/connector/native/process_posix.cpp index 56c28cf..4c0bd77 100644 --- a/connector/native/process_posix.cpp +++ b/connector/native/process_posix.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include #include @@ -12,6 +14,38 @@ #include namespace connector { +namespace { + +constexpr unsigned short kConnectorPort = 27843; + +std::filesystem::path lsof_path() { + for (const char* candidate : {"/usr/sbin/lsof", "/usr/bin/lsof"}) { + if (std::filesystem::exists(candidate)) return candidate; + } + return {}; +} + +std::set connector_port_pids() { + const std::filesystem::path executable = lsof_path(); + if (executable.empty()) return {}; + const ProcessResult result = run_process(executable.string(), + {"-nP", "-tiTCP:" + std::to_string(kConnectorPort), "-sTCP:LISTEN"}, {}, 3); + if (result.timed_out || (result.exit_code != 0 && result.exit_code != 1)) return {}; + std::set pids; + std::istringstream lines(result.output); + std::string line; + while (std::getline(lines, line)) { + try { + const long value = std::stol(line); + if (value > 0) pids.insert(static_cast(value)); + } catch (...) { + // lsof 可能输出诊断信息;只接受纯数字 PID。 + } + } + return pids; +} + +} // namespace ProcessResult run_process(const std::string& executable, const std::vector& arguments, @@ -70,6 +104,46 @@ ProcessResult run_process(const std::string& executable, return result; } +bool connector_port_in_use() { + return !connector_port_pids().empty(); +} + +bool stop_connector_port(std::string& error) { + const std::set pids = connector_port_pids(); + if (pids.empty()) { + error.clear(); + return true; + } + for (const pid_t pid : pids) { + if (pid == getpid()) { + error = "无法从外部停止当前连接器进程"; + return false; + } + if (kill(pid, SIGTERM) != 0) { + error = "无法停止占用端口 27843 的进程,请检查系统权限"; + return false; + } + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (std::chrono::steady_clock::now() < deadline) { + if (!connector_port_in_use()) { + error.clear(); + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + for (const pid_t pid : pids) { + if (kill(pid, 0) == 0) kill(pid, SIGKILL); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (connector_port_in_use()) { + error = "已请求停止进程,但端口 27843 仍未释放"; + return false; + } + error.clear(); + return true; +} + } // namespace connector #endif diff --git a/connector/native/process_windows.cpp b/connector/native/process_windows.cpp index 6625d8c..1b3a034 100644 --- a/connector/native/process_windows.cpp +++ b/connector/native/process_windows.cpp @@ -1,17 +1,24 @@ #ifdef _WIN32 #define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include #include +#include #include "platform.hpp" #include +#include #include #include +#include namespace connector { namespace { +constexpr unsigned short kConnectorPort = 27843; + std::wstring wide(const std::string& value) { if (value.empty()) return {}; const int size = MultiByteToWideChar(CP_UTF8, 0, value.data(), static_cast(value.size()), nullptr, 0); @@ -35,6 +42,26 @@ std::wstring quote(const std::string& value) { return result + L'\"'; } +std::set connector_port_pids() { + DWORD size = 0; + DWORD result = GetExtendedTcpTable(nullptr, &size, FALSE, AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, 0); + if (result != ERROR_INSUFFICIENT_BUFFER || size == 0) return {}; + std::vector storage(size); + result = GetExtendedTcpTable(storage.data(), &size, FALSE, AF_INET, + TCP_TABLE_OWNER_PID_LISTENER, 0); + if (result != NO_ERROR) return {}; + const auto* table = reinterpret_cast(storage.data()); + std::set pids; + for (DWORD index = 0; index < table->dwNumEntries; ++index) { + const auto& row = table->table[index]; + if (ntohs(static_cast(row.dwLocalPort)) == kConnectorPort && row.dwOwningPid != 0) { + pids.insert(row.dwOwningPid); + } + } + return pids; +} + } // namespace ProcessResult run_process(const std::string& executable, @@ -93,6 +120,46 @@ ProcessResult run_process(const std::string& executable, return result; } +bool connector_port_in_use() { + return !connector_port_pids().empty(); +} + +bool stop_connector_port(std::string& error) { + const std::set pids = connector_port_pids(); + if (pids.empty()) { + error.clear(); + return true; + } + for (const DWORD pid : pids) { + if (pid == GetCurrentProcessId()) { + error = "无法从外部停止当前连接器进程"; + return false; + } + HANDLE process = OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, pid); + if (!process) { + error = "无法停止占用端口 27843 的进程,请以管理员身份运行后重试"; + return false; + } + const BOOL stopped = TerminateProcess(process, 0); + if (stopped) WaitForSingleObject(process, 3000); + CloseHandle(process); + if (!stopped) { + error = "停止占用端口 27843 的进程失败"; + return false; + } + } + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(3); + while (std::chrono::steady_clock::now() < deadline) { + if (!connector_port_in_use()) { + error.clear(); + return true; + } + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + error = "进程已停止,但端口 27843 仍未释放"; + return false; +} + } // namespace connector #endif