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.
182 lines
6.7 KiB
C++
182 lines
6.7 KiB
C++
#pragma once
|
||
|
||
#include <afx.h>
|
||
#include <afxwin.h>
|
||
#include <mutex>
|
||
#include "Resource.h"
|
||
#include "common/commands.h"
|
||
#include "LangManager.h"
|
||
|
||
// 全局 licenses.ini 互斥锁(Meyers singleton,跨翻译单元共享)。
|
||
// 所有读写 licenses.ini 的函数入口必须加锁,否则在心跳并发下会出现
|
||
// read-modify-write 丢更新(典型受害者:PendingQuota / IP 列表)。
|
||
// 使用 recursive_mutex 是因为部分函数会嵌套调用(如 DecrementPendingQuota → ClearPendingRenewal)。
|
||
std::recursive_mutex& LicensesIniMutex();
|
||
|
||
// 让 UpdateLicenseActivity 内部缓存里某个 SN 的 entry 失效。
|
||
// 必须在外部修改了授权(删除 / 重新创建 section)后调用,否则 cache 命中策略
|
||
// 会跳过本应触发的"既往授权自动加入"路径,导致 disk 上的 section 不会重建。
|
||
// 实现在 CPasswordDlg.cpp,需持 LicensesIniMutex(内部会自行加锁,可在已加锁线程嵌套调用)。
|
||
void InvalidateLicenseActivityCache(const std::string& deviceID);
|
||
|
||
// CPasswordDlg 对话框
|
||
namespace TcpClient {
|
||
std::string ObfuscateAuthorization(const std::string& auth);
|
||
std::string DeobfuscateAuthorization(const std::string& obfuscated);
|
||
bool IsAuthorizationValid(const std::string& authObfuscated);
|
||
}
|
||
|
||
void WriteHash(const char* pwdHash, const char* upperHash);
|
||
std::string getUpperHash();
|
||
std::string GetUpperHash();
|
||
std::string GetFinderString(const char* buf);
|
||
|
||
// 获取密码哈希值
|
||
std::string GetPwdHash();
|
||
|
||
const Validation* GetValidation(int offset=100);
|
||
|
||
std::string GetMasterId();
|
||
|
||
std::string GetHMAC(int offset=100);
|
||
|
||
void SetHMAC(const std::string str, int offset=100);
|
||
|
||
bool IsPwdHashValid(const char* pwdHash = nullptr);
|
||
|
||
bool WritePwdHash(char* target, const std::string& pwdHash, const Validation &verify);
|
||
|
||
class CPasswordDlg : public CDialogLangEx
|
||
{
|
||
DECLARE_DYNAMIC(CPasswordDlg)
|
||
|
||
public:
|
||
CPasswordDlg(CWnd* pParent = nullptr); // 标准构造函数
|
||
virtual ~CPasswordDlg();
|
||
|
||
enum { IDD = IDD_DIALOG_PASSWORD };
|
||
|
||
protected:
|
||
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
|
||
|
||
DECLARE_MESSAGE_MAP()
|
||
public:
|
||
HICON m_hIcon;
|
||
CEdit m_EditDeviceID;
|
||
CEdit m_EditPassword;
|
||
CString m_sDeviceID;
|
||
CString m_sPassword;
|
||
virtual BOOL OnInitDialog();
|
||
CComboBox m_ComboBinding;
|
||
afx_msg void OnCbnSelchangeComboBind();
|
||
CEdit m_EditPasscodeHmac;
|
||
CString m_sPasscodeHmac;
|
||
CEdit m_EditRootCert;
|
||
CString m_sRootCert;
|
||
int m_nBindType;
|
||
virtual void OnOK();
|
||
};
|
||
|
||
|
||
// 授权信息保存辅助函数
|
||
std::string GetLicensesPath();
|
||
bool SaveLicenseInfo(const std::string& deviceID, const std::string& passcode,
|
||
const std::string& hmac, const std::string& remark = "",
|
||
const std::string& authorization = "",
|
||
const std::string& frpConfig = "");
|
||
bool LoadLicenseInfo(const std::string& deviceID, std::string& passcode,
|
||
std::string& hmac, std::string& remark);
|
||
// 加载授权的 FRP 配置
|
||
std::string LoadLicenseFrpConfig(const std::string& deviceID);
|
||
// 加载授权的 Authorization(用于 V2 授权返回给第一层)
|
||
std::string LoadLicenseAuthorization(const std::string& deviceID);
|
||
// 更新授权的 Authorization(V2 续期时更新)
|
||
// authorization: 混淆后的 Authorization 字符串
|
||
bool UpdateLicenseAuthorization(const std::string& deviceID, const std::string& authorization);
|
||
// 更新授权活跃信息(IP、位置、最后活跃时间)
|
||
// 如果授权不存在则自动创建记录
|
||
// machineName: 机器名,用于区分同一公网IP下的不同机器
|
||
bool UpdateLicenseActivity(const std::string& deviceID, const std::string& passcode,
|
||
const std::string& hmac, const std::string& ip = "",
|
||
const std::string& location = "", const std::string& machineName = "");
|
||
// 检查授权是否已被撤销
|
||
bool IsLicenseRevoked(const std::string& deviceID);
|
||
|
||
// 构建 V1 Authorization(第一层/下级返回给下级)
|
||
// sn: 用于日志输出的设备标识
|
||
// 返回混淆后的 Authorization,失败返回空字符串
|
||
std::string BuildV1Authorization(const std::string& sn = "", bool heartbeat = false);
|
||
|
||
class CPwdGenDlg : public CDialogLangEx
|
||
{
|
||
DECLARE_DYNAMIC(CPwdGenDlg)
|
||
|
||
public:
|
||
CPwdGenDlg(CWnd* pParent = nullptr); // 标准构造函数
|
||
virtual ~CPwdGenDlg();
|
||
|
||
enum {
|
||
IDD = IDD_DIALOG_KEYGEN
|
||
};
|
||
|
||
protected:
|
||
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
|
||
|
||
DECLARE_MESSAGE_MAP()
|
||
public:
|
||
HICON m_hIcon;
|
||
CEdit m_EditDeviceID;
|
||
CEdit m_EditPassword;
|
||
CEdit m_EditUserPwd;
|
||
CString m_sDeviceID;
|
||
CString m_sPassword;
|
||
CString m_sUserPwd;
|
||
afx_msg void OnBnClickedButtonGenkey();
|
||
afx_msg void OnBnClickedButtonSaveLicense();
|
||
CDateTimeCtrl m_PwdExpireDate;
|
||
COleDateTime m_ExpireTm;
|
||
CDateTimeCtrl m_StartDate;
|
||
COleDateTime m_StartTm;
|
||
virtual BOOL OnInitDialog();
|
||
CEdit m_EditHostNum;
|
||
int m_nHostNum;
|
||
CEdit m_EditHMAC;
|
||
CButton m_BtnSaveLicense;
|
||
BOOL m_bIsLocalDevice; // 是否为本机授权
|
||
CString m_sHMAC; // HMAC 值
|
||
CEdit m_EditAuthorization; // 多层授权显示框
|
||
CString m_sAuthorization; // 多层授权: Authorization 字段
|
||
|
||
// V2 Authorization 下级并发数限制
|
||
CEdit m_EditAuthHostNum; // 下级并发数输入框
|
||
int m_nAuthHostNum; // 下级并发数值
|
||
BOOL m_bAuthHostNumManual; // 是否手动修改过
|
||
|
||
afx_msg void OnEnChangeEditHostNum(); // 连接数变化时同步
|
||
afx_msg void OnEnChangeEditAuthHostNum(); // 下级并发数手动修改
|
||
|
||
// V2 授权相关
|
||
CComboBox m_ComboVersion; // 版本选择下拉框
|
||
CEdit m_EditPrivateKey; // 私钥文件路径
|
||
CButton m_BtnBrowseKey; // 浏览私钥文件按钮
|
||
CButton m_BtnGenKeyPair; // 生成密钥对按钮
|
||
int m_nVersion; // 0=V1(HMAC), 1=V2(ECDSA)
|
||
CString m_sPrivateKeyPath; // 私钥文件路径
|
||
|
||
afx_msg void OnCbnSelchangeComboVersion(); // 版本切换事件
|
||
afx_msg void OnBnClickedButtonBrowseKey(); // 浏览私钥文件事件
|
||
afx_msg void OnBnClickedButtonGenKeypair(); // 生成密钥对事件
|
||
|
||
// FRP 代理相关
|
||
CButton m_CheckFrpProxy; // FRP 代理复选框
|
||
CEdit m_EditFrpRemotePort; // FRP 远程端口
|
||
CButton m_BtnFrpAutoPort; // 自动分配端口按钮
|
||
CStatic m_StaticFrpInfo; // FRPS 信息显示
|
||
int m_nFrpRemotePort; // FRP 远程端口值
|
||
std::string m_sFrpConfig; // 生成的 FRP 配置字符串
|
||
|
||
afx_msg void OnBnClickedCheckFrpProxy(); // FRP 复选框点击
|
||
afx_msg void OnBnClickedBtnFrpAutoPort(); // 自动分配端口按钮
|
||
void UpdateFrpControlStates(); // 更新 FRP 控件状态
|
||
};
|