Compare commits

...
2 Commits
Author SHA1 Message Date
tianmo f810e712ec Merge branch 'main' of http://gitlab.yeomai.site/Yeomai/SpineParticlesWeb 2026-09-11 10:00:19 +08:00
tianmo 8ab3672c4e 粒子连接器修改 2026-09-11 10:00:16 +08:00
5 changed files with 170 additions and 6 deletions

No files matched your search

+14 -3
View File
@@ -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);
+11 -3
View File
@@ -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);
+4
View File
@@ -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<std::string>& arguments,
const std::filesystem::path& working_directory,
+74
View File
@@ -5,6 +5,8 @@
#include <chrono>
#include <csignal>
#include <fcntl.h>
#include <set>
#include <sstream>
#include <stdexcept>
#include <sys/types.h>
#include <sys/wait.h>
@@ -12,6 +14,38 @@
#include <unistd.h>
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<pid_t> 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<pid_t> 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<pid_t>(value));
} catch (...) {
// lsof 可能输出诊断信息;只接受纯数字 PID。
}
}
return pids;
}
} // namespace
ProcessResult run_process(const std::string& executable,
const std::vector<std::string>& 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<pid_t> 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
+67
View File
@@ -1,17 +1,24 @@
#ifdef _WIN32
#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <windows.h>
#include <iphlpapi.h>
#include "platform.hpp"
#include <chrono>
#include <set>
#include <stdexcept>
#include <thread>
#include <vector>
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<int>(value.size()), nullptr, 0);
@@ -35,6 +42,26 @@ std::wstring quote(const std::string& value) {
return result + L'\"';
}
std::set<DWORD> 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<unsigned char> 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<const MIB_TCPTABLE_OWNER_PID*>(storage.data());
std::set<DWORD> pids;
for (DWORD index = 0; index < table->dwNumEntries; ++index) {
const auto& row = table->table[index];
if (ntohs(static_cast<u_short>(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<DWORD> 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