Files
SimpleRemoter/server/2015Remote/CPasswordDlg.h
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

182 lines
6.7 KiB
C++
Raw 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.
#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);
// 更新授权的 AuthorizationV2 续期时更新)
// 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 控件状态
};