Files
SimpleRemoter/server/2015Remote/CLicenseDlg.cpp
yuanyuanxiang 740ec8baf3 Perf(license): mutex + write-suppression for licenses.ini hot path
licenses.ini was hit on every heartbeat -- 5s x clients x ~8 SetStr per
auth -- with no concurrency protection. Two consequences:
  1. 100 concurrent online would saturate the file (~160 writes/sec,
     full-file rewrite each via WritePrivateProfileString).
  2. Concurrent SetPendingRenewal / DecrementPendingQuota with no lock
     occasionally clobbered freshly-set renewal quotas (reported by
     user as "preset renewal silently disappears").

Add LicensesIniMutex() (Meyers singleton recursive_mutex, exposed in
CPasswordDlg.h so both CPasswordDlg.cpp and CLicenseDlg.cpp share it)
and wrap all 15 functions that touch licenses.ini.

Rewrite UpdateLicenseActivity around g_activityCache (in-memory state
keyed by "SN|IP|machine"): skip the entire write path when nothing
changed and the 30s LastActiveTime throttle window hasn't expired.
Passcode/HMAC are only flushed on actual change (renewal path); IP
list is only rewritten when the yyMMdd timestamp would roll a day.

Measured impact (local 2-client baseline):
  before: 0.60 writes/sec (4 writes per heartbeat cluster)
  after:  0.07 writes/sec (one write per client per 30s throttle)

Extrapolated to the 100-online target:
  before: ~160 writes/sec (saturation)
  after:  ~3.3 writes/sec  (100 clients / 30s throttle window)

Race elimination is the more important win: PendingQuota's
read-modify-write is now atomic, so the "preset renewal disappears"
race is closed.

Notes from audit (these landed during the same iteration):
- Cache key is (SN, IP, machine), not SN alone. A single SN can be
  shared by 100+ end machines in bulk-license deployments, so a
  per-SN cache flips on every heartbeat and defeats suppression.
  Per-(SN, IP, machine) throttling is what makes the 100/30 model
  actually hold; an SN-only key reproduced the original ~0.7 writes/s.
- DeleteLicense invalidates the per-SN activity cache via
  InvalidateLicenseActivityCache() (prefix scan since one SN maps to
  many cache entries). Without this, cache hits after delete would
  skip the auto-recreate path and leave the section permanently
  missing.
- OnLicenseViewIPs: m_ListLicense.SetItemText moved outside the lock
  so the critical section only covers disk I/O.
2026-05-22 00:31:54 +02:00

1255 lines
43 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CLicenseDlg.cpp: 实现文件
//
#include "stdafx.h"
#include "CLicenseDlg.h"
#include "afxdialogex.h"
#include "CPasswordDlg.h"
#include "common/iniFile.h"
#include "common/IniParser.h"
#include "2015RemoteDlg.h"
#include "InputDlg.h"
#include "IPHistoryDlg.h"
#include "FrpsForSubDlg.h"
#include "pwd_gen.h"
#include <algorithm>
// CLicenseDlg 对话框
IMPLEMENT_DYNAMIC(CLicenseDlg, CDialogEx)
CLicenseDlg::CLicenseDlg(CMy2015RemoteDlg* pParent /*=nullptr*/)
: CDialogLangEx(IDD_DIALOG_LICENSE, pParent)
, m_pParent(pParent)
{
}
CLicenseDlg::~CLicenseDlg()
{
}
void CLicenseDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LICENSE_LIST, m_ListLicense);
}
BEGIN_MESSAGE_MAP(CLicenseDlg, CDialogEx)
ON_WM_SIZE()
ON_NOTIFY(NM_RCLICK, IDC_LICENSE_LIST, &CLicenseDlg::OnNMRClickLicenseList)
ON_NOTIFY(LVN_COLUMNCLICK, IDC_LICENSE_LIST, &CLicenseDlg::OnColumnClick)
ON_COMMAND(ID_LICENSE_REVOKE, &CLicenseDlg::OnLicenseRevoke)
ON_COMMAND(ID_LICENSE_ACTIVATE, &CLicenseDlg::OnLicenseActivate)
ON_COMMAND(ID_LICENSE_RENEWAL, &CLicenseDlg::OnLicenseRenewal)
ON_COMMAND(ID_LICENSE_EDIT_REMARK, &CLicenseDlg::OnLicenseEditRemark)
ON_COMMAND(ID_LICENSE_VIEW_IPS, &CLicenseDlg::OnLicenseViewIPs)
ON_COMMAND(ID_LICENSE_DELETE, &CLicenseDlg::OnLicenseDelete)
ON_COMMAND(ID_LICENSE_AUTO_FRP, &CLicenseDlg::OnLicenseAutoFrp)
ON_COMMAND(ID_LICENSE_REVOKE_FRP, &CLicenseDlg::OnLicenseRevokeFrp)
END_MESSAGE_MAP()
// 前向声明实现位于本文件后段Auto FRP 相关工具)
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig);
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner);
// 获取所有授权信息
std::vector<LicenseInfo> GetAllLicenses()
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::vector<LicenseInfo> licenses;
std::string iniPath = GetLicensesPath();
// 注意CIniParser 走 ifstream 读取整文件,与 WritePrivateProfileString 的内核锁
// 不在同一域。必须靠这里的 g_licensesIniMutex 阻止与其它写入交错,否则可能读到
// 写入到一半的中间态。
CIniParser parser;
if (!parser.LoadFile(iniPath.c_str()))
return licenses;
const auto& sections = parser.GetAllSections();
for (const auto& sec : sections) {
const std::string& sectionName = sec.first;
const auto& kv = sec.second;
LicenseInfo info;
info.SerialNumber = sectionName;
auto it = kv.find("Passcode");
if (it != kv.end()) info.Passcode = it->second;
it = kv.find("HMAC");
if (it != kv.end()) info.HMAC = it->second;
it = kv.find("Remark");
if (it != kv.end()) info.Remark = it->second;
it = kv.find("CreateTime");
if (it != kv.end()) info.CreateTime = it->second;
it = kv.find("IP");
if (it != kv.end()) info.IP = it->second;
it = kv.find("Location");
if (it != kv.end()) info.Location = it->second;
it = kv.find("LastActiveTime");
if (it != kv.end()) info.LastActiveTime = it->second;
it = kv.find("Status");
if (it != kv.end()) info.Status = it->second;
else info.Status = LICENSE_STATUS_ACTIVE; // 默认为有效
it = kv.find("PendingExpireDate");
if (it != kv.end()) info.PendingExpireDate = it->second;
it = kv.find("PendingHostNum");
if (it != kv.end()) info.PendingHostNum = atoi(it->second.c_str());
it = kv.find("PendingQuota");
if (it != kv.end()) info.PendingQuota = atoi(it->second.c_str());
licenses.push_back(info);
}
return licenses;
}
void CLicenseDlg::InitListColumns()
{
m_ListLicense.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_LABELTIP | LVS_EX_INFOTIP);
m_ListLicense.InsertColumnL(0, _T("ID"), LVCFMT_LEFT, 35);
m_ListLicense.InsertColumnL(1, _T("序列号"), LVCFMT_LEFT, 125);
m_ListLicense.InsertColumnL(2, _T("状态"), LVCFMT_LEFT, 60);
m_ListLicense.InsertColumnL(3, _T("到期时间"), LVCFMT_LEFT, 80);
m_ListLicense.InsertColumnL(4, _T("备注"), LVCFMT_LEFT, 120);
m_ListLicense.InsertColumnL(5, _T("口令"), LVCFMT_LEFT, 260);
m_ListLicense.InsertColumnL(6, _T("版本"), LVCFMT_LEFT, 50);
m_ListLicense.InsertColumnL(7, _T("IP"), LVCFMT_LEFT, 190);
m_ListLicense.InsertColumnL(8, _T("位置"), LVCFMT_LEFT, 160);
m_ListLicense.InsertColumnL(9, _T("最后活跃"), LVCFMT_LEFT, 130);
m_ListLicense.InsertColumnL(10, _T("创建时间"), LVCFMT_LEFT, 130);
}
void CLicenseDlg::LoadLicenses()
{
m_Licenses = GetAllLicenses();
}
// 比较两个授权信息
static int CompareLicense(const LicenseInfo& a, const LicenseInfo& b, int nColumn)
{
switch (nColumn) {
case LIC_COL_SERIAL:
return a.SerialNumber.compare(b.SerialNumber);
case LIC_COL_STATUS:
return a.Status.compare(b.Status);
case LIC_COL_EXPIRE:
return a.PendingExpireDate.compare(b.PendingExpireDate);
case LIC_COL_REMARK:
return a.Remark.compare(b.Remark);
case LIC_COL_PASSCODE:
return a.Passcode.compare(b.Passcode);
case LIC_COL_HMAC:
return a.HMAC.compare(b.HMAC);
case LIC_COL_IP:
return a.IP.compare(b.IP);
case LIC_COL_LOCATION:
return a.Location.compare(b.Location);
case LIC_COL_LASTACTIVE:
return a.LastActiveTime.compare(b.LastActiveTime);
case LIC_COL_CREATETIME:
return a.CreateTime.compare(b.CreateTime);
default:
return 0;
}
}
void CLicenseDlg::RefreshList()
{
m_ListLicense.DeleteAllItems();
// 创建索引列表用于排序
std::vector<size_t> indices(m_Licenses.size());
for (size_t i = 0; i < indices.size(); ++i) {
indices[i] = i;
}
// 如果有排序列,进行排序
if (m_nSortColumn >= 0) {
int sortCol = m_nSortColumn;
BOOL ascending = m_bSortAscending;
std::sort(indices.begin(), indices.end(),
[this, sortCol, ascending](size_t a, size_t b) {
int result = CompareLicense(m_Licenses[a], m_Licenses[b], sortCol);
return ascending ? (result < 0) : (result > 0);
});
}
for (size_t i = 0; i < indices.size(); ++i) {
const auto& lic = m_Licenses[indices[i]];
CString strID;
strID.Format(_T("%d"), (int)(i + 1));
int idx = m_ListLicense.InsertItem((int)i, strID);
m_ListLicense.SetItemData(idx, indices[i]); // 保存原始索引
m_ListLicense.SetItemText(idx, 1, lic.SerialNumber.c_str());
// 显示到期时间
CString strPending;
std::string d = lic.PendingExpireDate;
if (d.empty()) {
// PendingExpireDate 为空,从 Passcode 中解析到期时间
d = ParseExpireDateFromPasscode(lic.Passcode);
}
if (d.length() == 8) {
// 格式化显示20270221 -> 2027-02-21
strPending.Format(_T("%s-%s-%s"), d.substr(0, 4).c_str(),
d.substr(4, 2).c_str(), d.substr(6, 2).c_str());
} else if (!d.empty()) {
strPending = d.c_str();
}
m_ListLicense.SetItemText(idx, 3, strPending);
// 动态计算状态:根据到期时间自动调整 Active/Expired 状态
std::string displayStatus = lic.Status;
if (d.length() == 8 && lic.Status != LICENSE_STATUS_REVOKED) {
CTime now = CTime::GetCurrentTime();
CString strToday;
strToday.Format(_T("%04d%02d%02d"), now.GetYear(), now.GetMonth(), now.GetDay());
std::string today = CT2A(strToday);
if (d < today && lic.Status == LICENSE_STATUS_ACTIVE) {
// 已过期,标记为 Expired
displayStatus = LICENSE_STATUS_EXPIRED;
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_EXPIRED);
m_Licenses[indices[i]].Status = LICENSE_STATUS_EXPIRED;
} else if (d >= today && lic.Status == LICENSE_STATUS_EXPIRED) {
// 未过期(可能设置了预设续期),恢复为 Active
displayStatus = LICENSE_STATUS_ACTIVE;
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE);
m_Licenses[indices[i]].Status = LICENSE_STATUS_ACTIVE;
}
}
m_ListLicense.SetItemText(idx, 2, displayStatus.c_str());
m_ListLicense.SetItemText(idx, 4, lic.Remark.c_str());
m_ListLicense.SetItemText(idx, 5, lic.Passcode.c_str());
// 版本列V1/V2
std::string versionDisplay = "V1";
if (lic.HMAC.length() >= 3 && lic.HMAC.substr(0, 3) == "v2:") {
versionDisplay = "V2";
}
m_ListLicense.SetItemText(idx, 6, versionDisplay.c_str());
// IP 列:多 IP 时显示 "[数量] 首个IP, ..."
m_ListLicense.SetItemText(idx, 7, FormatIPDisplay(lic.IP).c_str());
m_ListLicense.SetItemText(idx, 8, lic.Location.c_str());
m_ListLicense.SetItemText(idx, 9, lic.LastActiveTime.c_str());
m_ListLicense.SetItemText(idx, 10, lic.CreateTime.c_str());
}
}
void CLicenseDlg::OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
int nColumn = pNMLV->iSubItem;
// ID列不排序
if (nColumn == LIC_COL_ID) {
*pResult = 0;
return;
}
// 点击同一列切换排序方向
if (nColumn == m_nSortColumn) {
m_bSortAscending = !m_bSortAscending;
} else {
m_nSortColumn = nColumn;
m_bSortAscending = TRUE;
}
SortByColumn(nColumn, m_bSortAscending);
*pResult = 0;
}
void CLicenseDlg::SortByColumn(int /*nColumn*/, BOOL /*bAscending*/)
{
m_ListLicense.SetRedraw(FALSE);
RefreshList();
m_ListLicense.SetRedraw(TRUE);
m_ListLicense.Invalidate();
}
BOOL CLicenseDlg::OnInitDialog()
{
__super::OnInitDialog();
// 设置对话框标题(解决英语系统乱码问题)
SetWindowText(_TR("授权管理"));
m_hIcon = LoadIcon(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDI_ICON_PASSWORD));
SetIcon(m_hIcon, FALSE);
SetIcon(m_hIcon, TRUE);
InitListColumns();
LoadLicenses();
RefreshList();
return TRUE;
}
void CLicenseDlg::OnSize(UINT nType, int cx, int cy)
{
__super::OnSize(nType, cx, cy);
if (m_ListLicense.GetSafeHwnd()) {
m_ListLicense.MoveWindow(7, 7, cx - 14, cy - 14);
}
}
// 更新授权状态
bool SetLicenseStatus(const std::string& deviceID, const std::string& status)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 检查授权是否存在
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
if (existingPasscode.empty()) {
return false; // 授权不存在
}
cfg.SetStr(deviceID, "Status", status);
return true;
}
void CLicenseDlg::OnNMRClickLicenseList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMITEMACTIVATE pNMItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
*pResult = 0;
int nItem = pNMItemActivate->iItem;
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
// 创建弹出菜单
CMenu menu;
menu.CreatePopupMenu();
const auto& lic = m_Licenses[nIndex];
menu.AppendMenuL(MF_STRING, ID_LICENSE_RENEWAL, _T("预设续期(&N)..."));
menu.AppendMenuL(MF_STRING, ID_LICENSE_EDIT_REMARK, _T("编辑备注(&E)..."));
menu.AppendMenuL(MF_STRING, ID_LICENSE_AUTO_FRP, _T("自动FRP(&F)..."));
// 仅当该授权已分配 FRPC 端口时,才显示"撤销FRP"
{
std::string existingFrp = LoadLicenseFrpConfig(lic.SerialNumber);
if (ParseRemotePortFromFrpConfig(existingFrp) > 0) {
menu.AppendMenuL(MF_STRING, ID_LICENSE_REVOKE_FRP, _T("撤销FRP(&U)"));
}
}
// 只有当有 IP 记录时才显示查看 IP 历史选项
int ipCount = GetIPCountFromList(lic.IP);
if (ipCount > 0) {
CString strViewIPs;
strViewIPs.Format(_TR("查看 IP 历史 (%d)(&I)..."), ipCount);
menu.AppendMenuL(MF_STRING, ID_LICENSE_VIEW_IPS, strViewIPs);
}
menu.AppendMenuL(MF_SEPARATOR, 0, _T(""));
if (lic.Status == LICENSE_STATUS_ACTIVE) {
menu.AppendMenuL(MF_STRING, ID_LICENSE_REVOKE, _T("撤销授权(&R)"));
} else {
menu.AppendMenuL(MF_STRING, ID_LICENSE_ACTIVATE, _T("激活授权(&A)"));
}
menu.AppendMenuL(MF_SEPARATOR, 0, _T(""));
menu.AppendMenuL(MF_STRING, ID_LICENSE_DELETE, _T("删除授权(&D)"));
// 显示菜单
CPoint point;
GetCursorPos(&point);
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, point.x, point.y, this);
}
void CLicenseDlg::OnLicenseRevoke()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
if (SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_REVOKED)) {
m_Licenses[nIndex].Status = LICENSE_STATUS_REVOKED;
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_REVOKED);
}
}
void CLicenseDlg::OnLicenseActivate()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
if (SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE)) {
m_Licenses[nIndex].Status = LICENSE_STATUS_ACTIVE;
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_ACTIVE);
}
}
void CLicenseDlg::OnCancel()
{
// 隐藏窗口而不是关闭
ShowWindow(SW_HIDE);
}
void CLicenseDlg::OnOK()
{
// 不做任何操作,防止回车关闭对话框
}
void CLicenseDlg::ShowAndRefresh()
{
// 重新加载数据并显示
LoadLicenses();
RefreshList();
ShowWindow(SW_SHOW);
SetForegroundWindow();
}
// 从 passcode 解析过期日期第2段如 20260221
std::string ParseExpireDateFromPasscode(const std::string& passcode)
{
size_t pos1 = passcode.find('-');
if (pos1 == std::string::npos) return "";
size_t pos2 = passcode.find('-', pos1 + 1);
if (pos2 == std::string::npos) return "";
return passcode.substr(pos1 + 1, pos2 - pos1 - 1);
}
// 从 passcode 解析并发连接数第3段
int ParseHostNumFromPasscode(const std::string& passcode)
{
size_t pos1 = passcode.find('-');
if (pos1 == std::string::npos) return 0;
size_t pos2 = passcode.find('-', pos1 + 1);
if (pos2 == std::string::npos) return 0;
size_t pos3 = passcode.find('-', pos2 + 1);
if (pos3 == std::string::npos) return 0;
std::string numStr = passcode.substr(pos2 + 1, pos3 - pos2 - 1);
return atoi(numStr.c_str());
}
// 设置待续期信息
bool SetPendingRenewal(const std::string& deviceID, const std::string& expireDate, int hostNum, int quota)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 检查授权是否存在
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
if (existingPasscode.empty()) {
return false;
}
cfg.SetStr(deviceID, "PendingExpireDate", expireDate);
cfg.SetInt(deviceID, "PendingHostNum", hostNum);
cfg.SetInt(deviceID, "PendingQuota", quota > 0 ? quota : 1);
return true;
}
// 获取待续期信息
RenewalInfo GetPendingRenewal(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
RenewalInfo info;
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
info.ExpireDate = cfg.GetStr(deviceID, "PendingExpireDate", "");
info.HostNum = cfg.GetInt(deviceID, "PendingHostNum", 0);
info.Quota = cfg.GetInt(deviceID, "PendingQuota", 1); // 默认配额为1
return info;
}
// 清除待续期信息
bool ClearPendingRenewal(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
cfg.SetStr(deviceID, "PendingExpireDate", "");
cfg.SetInt(deviceID, "PendingHostNum", 0);
cfg.SetInt(deviceID, "PendingQuota", 0);
return true;
}
// 配额递减,返回是否还有剩余配额
// 关键read-modify-write 的 PendingQuota 必须在锁内完成,否则与 SetPendingRenewal
// 并发会丢失用户刚设置的预设续期(旧 bug用户报告"预设续期消失"的根因)。
bool DecrementPendingQuota(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
int quota = cfg.GetInt(deviceID, "PendingQuota", 1);
if (quota <= 0) {
return false; // 无配额
}
quota--;
cfg.SetInt(deviceID, "PendingQuota", quota);
if (quota <= 0) {
// 配额用完清除待续期信息嵌套加锁recursive_mutex 安全)
ClearPendingRenewal(deviceID);
return false;
}
return true; // 还有剩余配额
}
void CLicenseDlg::OnLicenseRenewal()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
// 从 passcode 解析当前过期日期和并发连接数
std::string currentExpireDate = ParseExpireDateFromPasscode(lic.Passcode);
int defaultHostNum = lic.PendingHostNum > 0 ? lic.PendingHostNum : ParseHostNumFromPasscode(lic.Passcode);
if (defaultHostNum <= 0) defaultHostNum = 100;
int defaultQuota = lic.PendingQuota > 0 ? lic.PendingQuota : 1;
// 格式化当前过期日期显示
CString strCurrentExpire;
if (currentExpireDate.length() == 8) {
strCurrentExpire.FormatL("当前到期: %s-%s-%s",
currentExpireDate.substr(0, 4).c_str(),
currentExpireDate.substr(4, 2).c_str(),
currentExpireDate.substr(6, 2).c_str());
} else {
strCurrentExpire = _TR("当前到期: 未知");
}
// 使用输入对话框获取续期信息(三个输入框)
CInputDialog dlg(this);
CString strTitle;
strTitle.FormatL("预设续期 (%s)", strCurrentExpire);
dlg.Init(strTitle, _L("续期天数:"));
dlg.Init2(_L("并发连接数:"), std::to_string(defaultHostNum).c_str());
dlg.Init3(_L("配额(机器数):"), std::to_string(defaultQuota).c_str());
if (dlg.DoModal() != IDOK)
return;
int days = atoi(dlg.m_str);
int hostNum = atoi(dlg.m_sSecondInput);
int quota = atoi(dlg.m_sThirdInput);
if (quota <= 0) quota = 1;
if (days <= 0 || hostNum <= 0) {
MessageBoxL("请输入有效的天数和并发连接数!", "错误", MB_ICONWARNING);
return;
}
// 计算新的过期日期 (从原到期日期开始加天数)
CTime baseTime;
if (currentExpireDate.length() == 8) {
int year = atoi(currentExpireDate.substr(0, 4).c_str());
int month = atoi(currentExpireDate.substr(4, 2).c_str());
int day = atoi(currentExpireDate.substr(6, 2).c_str());
baseTime = CTime(year, month, day, 0, 0, 0);
} else {
baseTime = CTime::GetCurrentTime(); // 无法解析时用当前日期
}
CTimeSpan span(days, 0, 0, 0);
CTime newExpireTime = baseTime + span;
CString strNewExpireDate;
strNewExpireDate.Format(_T("%04d%02d%02d"),
newExpireTime.GetYear(), newExpireTime.GetMonth(), newExpireTime.GetDay());
std::string newExpireDate = CT2A(strNewExpireDate);
if (SetPendingRenewal(lic.SerialNumber, newExpireDate, hostNum, quota)) {
m_Licenses[nIndex].PendingExpireDate = newExpireDate;
m_Licenses[nIndex].PendingHostNum = hostNum;
m_Licenses[nIndex].PendingQuota = quota;
// 续期后自动激活(如果之前是 Expired
if (lic.Status == LICENSE_STATUS_EXPIRED) {
SetLicenseStatus(lic.SerialNumber, LICENSE_STATUS_ACTIVE);
m_Licenses[nIndex].Status = LICENSE_STATUS_ACTIVE;
m_ListLicense.SetItemText(nItem, LIC_COL_STATUS, LICENSE_STATUS_ACTIVE);
}
// 显示格式化的新过期日期
CString strPending;
strPending.Format(_T("%s-%s-%s"),
newExpireDate.substr(0, 4).c_str(),
newExpireDate.substr(4, 2).c_str(),
newExpireDate.substr(6, 2).c_str());
m_ListLicense.SetItemText(nItem, LIC_COL_EXPIRE, strPending);
CString msg;
msg.FormatL("已预设续期至: %s\n并发连接数: %d\n配额: %d (支持%d台机器续期)\n客户端上线时将自动下发新授权",
strPending, hostNum, quota, quota);
MessageBox(msg, _TR("预设成功"), MB_ICONINFORMATION);
}
}
// 设置授权备注
bool SetLicenseRemark(const std::string& deviceID, const std::string& remark)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 检查授权是否存在
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
if (existingPasscode.empty()) {
return false;
}
cfg.SetStr(deviceID, "Remark", remark);
return true;
}
void CLicenseDlg::OnLicenseEditRemark()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
// 使用输入对话框编辑备注
CInputDialog dlg(this);
dlg.Init(_L("编辑备注"), _L("备注:"));
dlg.m_str = lic.Remark.c_str();
if (dlg.DoModal() != IDOK)
return;
std::string newRemark = CT2A(dlg.m_str);
if (SetLicenseRemark(lic.SerialNumber, newRemark)) {
m_Licenses[nIndex].Remark = newRemark;
m_ListLicense.SetItemText(nItem, LIC_COL_REMARK, newRemark.c_str());
}
}
// 删除授权
bool DeleteLicense(const std::string& deviceID)
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
// 检查授权是否存在
std::string existingPasscode = cfg.GetStr(deviceID, "Passcode", "");
if (existingPasscode.empty()) {
return false; // 授权不存在
}
// 若该授权占用了 FRP 端口,先释放 frp_ports.ini 中的占用记录,
// 否则端口会一直被这个已不存在的 SN "挂账",导致端口池逐步耗尽。
std::string frpConfig = cfg.GetStr(deviceID, "FrpConfig", "");
int allocatedPort = ParseRemotePortFromFrpConfig(frpConfig);
if (allocatedPort > 0) {
FreeFrpPortAllocation(allocatedPort, deviceID);
}
// 删除该 section (通过写入 NULL 删除整个 section)
BOOL ret = ::WritePrivateProfileStringA(deviceID.c_str(), NULL, NULL, iniPath.c_str());
::WritePrivateProfileStringA(NULL, NULL, NULL, iniPath.c_str()); // 刷新缓存
// 关键:清掉 UpdateLicenseActivity 的内存缓存。否则若同 SN 客户端再次连上来,
// cache 命中会跳过落盘 → disk 永远不会重建被删的 section。
InvalidateLicenseActivityCache(deviceID);
return ret != FALSE;
}
void CLicenseDlg::OnLicenseDelete()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
// 确认删除
CString msg;
msg.FormatL("确定要删除授权 \"%s\" 吗?\n\n此操作不可撤销!", lic.SerialNumber.c_str());
if (MessageBox(msg, _TR("确认删除"), MB_ICONWARNING | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
return;
}
if (DeleteLicense(lic.SerialNumber)) {
// 从列表和内存中删除
m_ListLicense.DeleteItem(nItem);
m_Licenses.erase(m_Licenses.begin() + nIndex);
// 刷新列表以更新 ID 编号
RefreshList();
} else {
MessageBoxL("删除授权失败!", "错误", MB_ICONERROR);
}
}
// 计算时间戳距今天数
// 支持6位格式 yyMMdd 和旧4位格式 MMdd
static int GetDaysFromTimestamp(const std::string& timestamp)
{
if (timestamp.empty()) return -1; // 无时间戳,返回-1表示无法判断
SYSTEMTIME now;
GetLocalTime(&now);
int year, month, day;
if (timestamp.length() == 6) {
// 新格式: yyMMdd (如 "260303" 表示 2026-03-03)
year = 2000 + atoi(timestamp.substr(0, 2).c_str());
month = atoi(timestamp.substr(2, 2).c_str());
day = atoi(timestamp.substr(4, 2).c_str());
} else if (timestamp.length() == 4) {
// 旧格式: MMdd需要推断年份
month = atoi(timestamp.substr(0, 2).c_str());
day = atoi(timestamp.substr(2, 2).c_str());
// 假设是今年或去年(如果日期比今天晚则是去年)
year = now.wYear;
if (month > now.wMonth || (month == now.wMonth && day > now.wDay)) {
year--; // 日期比今天晚,应该是去年
}
} else {
return -1; // 无法解析
}
// 计算天数差
SYSTEMTIME st = { 0 };
st.wYear = (WORD)year;
st.wMonth = (WORD)month;
st.wDay = (WORD)day;
FILETIME ft1, ft2;
SystemTimeToFileTime(&st, &ft1);
SystemTimeToFileTime(&now, &ft2);
ULARGE_INTEGER u1, u2;
u1.LowPart = ft1.dwLowDateTime;
u1.HighPart = ft1.dwHighDateTime;
u2.LowPart = ft2.dwLowDateTime;
u2.HighPart = ft2.dwHighDateTime;
// 转换为天数 (100纳秒单位)
__int64 diff = (__int64)(u2.QuadPart - u1.QuadPart);
return (int)(diff / (10000000LL * 60 * 60 * 24));
}
void CLicenseDlg::OnLicenseViewIPs()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
auto& lic = m_Licenses[nIndex]; // 非 const因为可能需要更新
if (lic.IP.empty()) {
MessageBoxL("该授权暂无 IP 记录", "IP 历史", MB_ICONINFORMATION);
return;
}
const int EXPIRY_DAYS = 180; // 过期天数
// 解析 IP 列表
// 格式: "1.2.3.4(PC01)|260303, 1.2.3.4(PC02)|260215"
std::vector<IPRecord> records;
std::string newIPList; // 用于保存过滤后的 IP 列表
int removedCount = 0;
size_t start = 0;
while (start < lic.IP.length()) {
size_t end = lic.IP.find(',', start);
if (end == std::string::npos) end = lic.IP.length();
std::string entry = lic.IP.substr(start, end - start);
// 去除前后空格
size_t first = entry.find_first_not_of(' ');
size_t last = entry.find_last_not_of(' ');
if (first != std::string::npos && last != std::string::npos) {
entry = entry.substr(first, last - first + 1);
}
// 解析 IP|时间戳
size_t pipePos = entry.find('|');
std::string ip, timestamp;
if (pipePos != std::string::npos) {
ip = entry.substr(0, pipePos);
timestamp = entry.substr(pipePos + 1);
} else {
ip = entry;
}
if (!ip.empty()) {
// 检查是否过期
int daysAgo = GetDaysFromTimestamp(timestamp);
if (daysAgo >= 0 && daysAgo > EXPIRY_DAYS) {
// 记录已过期,跳过
removedCount++;
start = end + 1;
continue;
}
// 保留有效记录
if (!newIPList.empty()) newIPList += ", ";
newIPList += entry;
// 解析 IP 和机器名: "1.2.3.4(PC01)" -> ip="1.2.3.4", machine="PC01"
IPRecord record;
record.ip = ip;
record.timestamp = timestamp;
record.daysAgo = daysAgo;
size_t parenPos = ip.find('(');
if (parenPos != std::string::npos) {
record.ip = ip.substr(0, parenPos);
size_t endParen = ip.find(')', parenPos);
if (endParen != std::string::npos) {
record.machineName = ip.substr(parenPos + 1, endParen - parenPos - 1);
}
}
// 格式化日期
if (!timestamp.empty() && (timestamp.length() == 6 || timestamp.length() == 4)) {
std::string monthStr, dayStr;
if (timestamp.length() == 6) {
monthStr = timestamp.substr(2, 2);
dayStr = timestamp.substr(4, 2);
} else {
monthStr = timestamp.substr(0, 2);
dayStr = timestamp.substr(2, 2);
}
record.formattedDate = monthStr + "-" + dayStr;
}
records.push_back(record);
}
start = end + 1;
}
// 如果有记录被删除,保存更新后的 IP 列表
if (removedCount > 0) {
// 锁内只做 I/O —— UI 控件更新SetItemText放锁外避免锁内触发
// 任何可能的消息循环回调,保持锁占用时间最短
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
cfg.SetStr(lic.SerialNumber, "IP", newIPList);
}
lic.IP = newIPList; // 更新内存中的数据(与 m_Licenses 同步,不需要锁)
// 更新列表显示UI 线程操作,必须在锁外)
CString strIPDisplay = FormatIPDisplay(newIPList).c_str();
m_ListLicense.SetItemText(nItem, LIC_COL_IP, strIPDisplay);
}
// 如果没有有效记录
if (records.empty()) {
CString msg;
msg.Format(_TR("所有 IP 记录均已过期(超过 %d 天),已自动清理"), EXPIRY_DAYS);
MessageBoxA(msg, _TR("IP 历史"), MB_ICONINFORMATION);
return;
}
// 使用对话框显示 IP 历史
CString strTitle;
strTitle.Format(_TR("IP 历史 - %s"), lic.SerialNumber.c_str());
CIPHistoryDlg dlg(this);
dlg.SetTitle(strTitle);
dlg.SetRecords(records);
dlg.SetRemovedCount(removedCount);
dlg.DoModal();
}
// 从 IP 列表字符串获取 IP 数量
int GetIPCountFromList(const std::string& ipListStr)
{
if (ipListStr.empty()) return 0;
int count = 1;
for (char c : ipListStr) {
if (c == ',') count++;
}
return count;
}
// 从 IP 列表字符串获取第一个 IP不含时间戳
std::string GetFirstIPFromList(const std::string& ipListStr)
{
if (ipListStr.empty()) return "";
// 找到第一个逗号或字符串末尾
size_t commaPos = ipListStr.find(',');
std::string firstEntry = (commaPos != std::string::npos)
? ipListStr.substr(0, commaPos)
: ipListStr;
// 去除时间戳部分
size_t pipePos = firstEntry.find('|');
if (pipePos != std::string::npos) {
firstEntry = firstEntry.substr(0, pipePos);
}
// 去除前后空格
size_t first = firstEntry.find_first_not_of(' ');
size_t last = firstEntry.find_last_not_of(' ');
if (first != std::string::npos && last != std::string::npos) {
return firstEntry.substr(first, last - first + 1);
}
return firstEntry;
}
// 格式化 IP 显示: 多 IP 时显示 "[数量] IP1, IP2, IP3, ..."
std::string FormatIPDisplay(const std::string& ipListStr)
{
if (ipListStr.empty()) return "";
int count = GetIPCountFromList(ipListStr);
if (count <= 1) {
return GetFirstIPFromList(ipListStr);
}
// 解析前 3 个 IP不含时间戳
std::string ips[3];
int extracted = 0;
size_t start = 0;
while (start < ipListStr.length() && extracted < 3) {
size_t end = ipListStr.find(',', start);
if (end == std::string::npos) end = ipListStr.length();
std::string entry = ipListStr.substr(start, end - start);
// 去除前后空格
size_t first = entry.find_first_not_of(' ');
size_t last = entry.find_last_not_of(' ');
if (first != std::string::npos && last != std::string::npos) {
entry = entry.substr(first, last - first + 1);
}
// 去除时间戳
size_t pipePos = entry.find('|');
if (pipePos != std::string::npos) {
entry = entry.substr(0, pipePos);
}
if (!entry.empty()) {
ips[extracted++] = entry;
}
start = end + 1;
}
// 格式化: "[数量] IP1, IP2, IP3, ..."
char buf[256];
if (count <= 3) {
// 3 个或以下,全部显示
if (extracted == 2) {
sprintf_s(buf, "[%d] %s, %s", count, ips[0].c_str(), ips[1].c_str());
} else {
sprintf_s(buf, "[%d] %s, %s, %s", count, ips[0].c_str(), ips[1].c_str(), ips[2].c_str());
}
} else {
// 超过 3 个,显示前 3 个加 ...
sprintf_s(buf, "[%d] %s, %s, %s, ...", count, ips[0].c_str(), ips[1].c_str(), ips[2].c_str());
}
return buf;
}
// 检查 IP+机器名 是否在授权数据库中存在
// IP 字段格式: "1.2.3.4(PC01)|260218, 1.2.3.5(PC02)|260215" (yyMMdd)
bool FindLicenseByIPAndMachine(const std::string& ip, const std::string& machineName, std::string* outSN)
{
if (ip.empty()) return false;
// 加锁保护整个 list 遍历,避免与并发的 SetStr(IP, ...) 交错读到中间态。
// GetAllLicenses 内部也加锁recursive_mutex 允许嵌套。
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
auto licenses = GetAllLicenses();
for (const auto& lic : licenses) {
if (lic.IP.empty()) continue;
// 解析 IP 列表中的每个条目
size_t start = 0;
while (start < lic.IP.length()) {
size_t end = lic.IP.find(',', start);
if (end == std::string::npos) end = lic.IP.length();
std::string entry = lic.IP.substr(start, end - start);
// 去除前后空格
size_t first = entry.find_first_not_of(' ');
size_t last = entry.find_last_not_of(' ');
if (first != std::string::npos && last != std::string::npos) {
entry = entry.substr(first, last - first + 1);
}
// 去除时间戳部分 "|yyMMdd"
size_t pipePos = entry.find('|');
if (pipePos != std::string::npos) {
entry = entry.substr(0, pipePos);
}
// 解析 "IP(机器名)" 格式
std::string entryIP = entry, entryMachine;
size_t parenPos = entry.find('(');
if (parenPos != std::string::npos) {
entryIP = entry.substr(0, parenPos);
size_t endParen = entry.find(')', parenPos);
if (endParen != std::string::npos) {
entryMachine = entry.substr(parenPos + 1, endParen - parenPos - 1);
}
}
// 匹配 IP 和机器名(机器名可选匹配)
if (entryIP == ip) {
// IP 匹配,检查机器名
if (machineName.empty() || entryMachine.empty() || entryMachine == machineName) {
if (outSN) *outSN = lic.SerialNumber;
return true;
}
}
start = end + 1;
}
}
return false;
}
// 从 FrpConfig 字符串解析 remotePort
// 格式: "serverAddr:serverPort-remotePort-expireDate-authValue"
// 注意serverAddr 可能是含 '-' 的域名(如 my-server.com必须从右往左定位
// 与 CMy2015RemoteDlg::ParseFrpAutoConfig 的拆分策略一致。
static int ParseRemotePortFromFrpConfig(const std::string& frpConfig)
{
if (frpConfig.empty()) return 0;
// 倒数第 1 个 '-':分隔 expireDate 与 authValue
size_t dashAuth = frpConfig.rfind('-');
if (dashAuth == std::string::npos || dashAuth == 0) return 0;
std::string s2 = frpConfig.substr(0, dashAuth);
// 倒数第 2 个 '-':分隔 remotePort 与 expireDate
size_t dashExpire = s2.rfind('-');
if (dashExpire == std::string::npos || dashExpire == 0) return 0;
std::string s3 = s2.substr(0, dashExpire);
// 倒数第 3 个 '-':分隔 serverAddr:serverPort 与 remotePort
size_t dashPort = s3.rfind('-');
if (dashPort == std::string::npos || dashPort == 0) return 0;
return atoi(s3.substr(dashPort + 1).c_str());
}
// 释放 frp_ports.ini 中指定端口的占用记录。
// 必须传入 expectedOwner —— 仅当端口当前的归属确实是它时才清除,
// 避免在 race 或外部改动后误抹掉别的 SN 的占用记录。
static bool FreeFrpPortAllocation(int port, const std::string& expectedOwner)
{
if (port <= 0 || expectedOwner.empty()) return false;
config portsCfg(CFrpsForSubDlg::GetFrpPortsPath());
char portStr[16];
sprintf_s(portStr, "%d", port);
std::string currentOwner = portsCfg.GetStr("ports", portStr, "");
if (currentOwner != expectedOwner) {
// 已被改写或释放,不动它
return false;
}
portsCfg.SetStr("ports", portStr, ""); // 写入空 ownerFindNextAvailablePort 将其视为可用
return true;
}
void CLicenseDlg::OnLicenseAutoFrp()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
// 1. 前提条件FRPS 服务器必须已在「下级 FRP 代理设置」中配置并启用
if (!CFrpsForSubDlg::IsFrpsConfigured()) {
MessageBoxL("请先在 扩展 → 下级 FRP 代理设置 中启用并配置 FRPS 服务器地址、端口与 Token然后再使用此功能。",
"未配置 FRPS", MB_OK | MB_ICONINFORMATION);
return;
}
// 2. 读取该授权当前的 FRP 配置(若已分配端口)
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
// 已分配端口时,先询问是否覆盖
if (existingPort > 0) {
CString msg;
msg.FormatL("该授权已分配 FRPC 远程端口 %d是否覆盖并重新设置",
existingPort);
if (MessageBox(msg, _TR("覆盖 FRPC 配置"),
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
return;
}
}
// 3. 决定默认端口:已有端口则沿用,否则查找下一个可用端口
int defaultPort = existingPort;
if (defaultPort <= 0) {
defaultPort = CFrpsForSubDlg::FindNextAvailablePort();
if (defaultPort <= 0) {
MessageBoxL("FRPS 端口范围已满,无法自动分配,请扩大「下级 FRP 代理设置」中的端口范围。",
"端口已满", MB_OK | MB_ICONWARNING);
return;
}
}
// 4. 弹出输入对话框,允许用户确认或修改端口
CInputDialog dlg(this);
CString strTitle;
strTitle.FormatL("自动 FRP - %s", lic.SerialNumber.c_str());
dlg.Init(strTitle, _L("FRPC 远程端口 (1024-65535):"));
CString strDefault;
strDefault.Format(_T("%d"), defaultPort);
dlg.m_str = strDefault;
if (dlg.DoModal() != IDOK)
return;
int remotePort = _ttoi(dlg.m_str);
if (remotePort < 1024 || remotePort > 65535) {
MessageBoxL("FRP 远程端口无效1024-65535", "提示", MB_OK | MB_ICONWARNING);
return;
}
// 5. 端口冲突检测:若端口已被其它序列号占用则拒绝
std::string portOwner = CFrpsForSubDlg::GetPortOwner(remotePort);
if (!portOwner.empty() && portOwner != lic.SerialNumber) {
CString msg;
msg.FormatL("端口 %d 已被序列号 %s 占用,请选择其它端口。",
remotePort, portOwner.c_str());
MessageBox(msg, _TR("端口冲突"), MB_OK | MB_ICONWARNING);
return;
}
// 6. 先生成 FRP 配置串(失败则直接返回,不动 frp_ports.ini
FrpsConfig frpsConfig = CFrpsForSubDlg::GetFrpsConfig();
// expireDate 固定 20371231由 License 自身的过期控制实际有效性)
std::string frpConfig = GenerateFrpConfig(
frpsConfig.server, frpsConfig.port, remotePort,
frpsConfig.token, "20371231", frpsConfig.authMode);
if (frpConfig.empty()) {
MessageBoxL("生成 FRP 配置失败", "错误", MB_OK | MB_ICONERROR);
return;
}
// 7. 提交端口变更(顺序:新端口 RecordPortAllocation → 释放旧端口 → 写 licenses.ini
// 顺序保证:任意一步失败都不会出现"旧端口已释放 + 新端口未占用"的真空态。
CFrpsForSubDlg::RecordPortAllocation(remotePort, lic.SerialNumber);
if (existingPort > 0 && existingPort != remotePort) {
FreeFrpPortAllocation(existingPort, lic.SerialNumber); // 仅当旧端口确实归属本 SN 时才释放
}
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
cfg.SetStr(lic.SerialNumber, "FrpConfig", frpConfig);
}
CString msg;
msg.FormatL("已为授权 %s 配置 FRPC 远程端口 %d。\n\n下级上线时将自动接收 FRP 配置并启用反向代理。",
lic.SerialNumber.c_str(), remotePort);
MessageBox(msg, _TR("配置成功"), MB_OK | MB_ICONINFORMATION);
}
void CLicenseDlg::OnLicenseRevokeFrp()
{
int nItem = m_ListLicense.GetNextItem(-1, LVNI_SELECTED);
if (nItem < 0)
return;
size_t nIndex = (size_t)m_ListLicense.GetItemData(nItem);
if (nIndex >= m_Licenses.size())
return;
const auto& lic = m_Licenses[nIndex];
// 读取当前 FRP 配置;若本就没有,菜单理应不显示,这里再防御一次
std::string existingFrpConfig = LoadLicenseFrpConfig(lic.SerialNumber);
int existingPort = ParseRemotePortFromFrpConfig(existingFrpConfig);
if (existingFrpConfig.empty() && existingPort <= 0) {
return;
}
// 二次确认(撤销后下级下次上线即丢失反向代理通道)
CString confirm;
confirm.FormatL("确定撤销授权 %s 的 FRP 配置吗?\n\n远程端口 %d 将被释放,下级下次上线后反向代理失效。",
lic.SerialNumber.c_str(), existingPort);
if (MessageBox(confirm, _TR("撤销 FRP 配置"),
MB_ICONQUESTION | MB_YESNO | MB_DEFBUTTON2) != IDYES) {
return;
}
// 先释放 frp_ports.ini 中的端口占用(仅当归属仍为本 SN 时才释放)。
// 顺序:先释放端口,再清 FrpConfig —— 即便后一步失败,端口也已可被复用,
// 不会留下"FrpConfig 还在但端口已挂账"的悬空状态。
if (existingPort > 0) {
FreeFrpPortAllocation(existingPort, lic.SerialNumber);
}
// 清除 licenses.ini 中该授权的 FrpConfig 字段
{
std::lock_guard<std::recursive_mutex> _lock(LicensesIniMutex());
std::string iniPath = GetLicensesPath();
config cfg(iniPath);
cfg.SetStr(lic.SerialNumber, "FrpConfig", "");
}
CString msg;
msg.FormatL("已撤销授权 %s 的 FRP 配置,远程端口 %d 已释放。",
lic.SerialNumber.c_str(), existingPort);
MessageBox(msg, _TR("撤销成功"), MB_OK | MB_ICONINFORMATION);
}