#import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "../client/IOCPClient.h" #define XXH_INLINE_ALL #include "../common/xxhash.h" #include "../common/rtt_estimator.h" #include "../common/client_auth_state.h" #include "../common/posix_net_helpers.h" #include "../common/sub_conn_thread.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 g_running(true); static std::atomic g_needResendLogin(false); // 分组变更后需要重发登录信息 // Client ID (calculated from system info, used by ScreenHandler) uint64_t g_myClientID = 0; // 服务端身份校验全局状态已抽到 common/client_auth_state.h(namespace ClientAuth) // 远程地址:当前为写死状态,如需调试,请按实际情况修改 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 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"; } // 读取 IOKit 维护的 IOPlatformUUID(与 Windows MachineGuid 等价) // 这是主板/系统级 UUID,由 IOPlatformExpertDevice 服务提供,重装系统通常不变。 // 对应:Windows HKLM\Software\Microsoft\Cryptography\MachineGuid // Linux /etc/machine-id static std::string getMachineId() { std::string result; io_service_t platformExpert = IOServiceGetMatchingService( kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice")); if (platformExpert != IO_OBJECT_NULL) { CFTypeRef uuidProperty = IORegistryEntryCreateCFProperty( platformExpert, CFSTR(kIOPlatformUUIDKey), kCFAllocatorDefault, 0); if (uuidProperty != nullptr) { if (CFGetTypeID(uuidProperty) == CFStringGetTypeID()) { CFStringRef uuidStr = (CFStringRef)uuidProperty; char buf[64] = {}; if (CFStringGetCString(uuidStr, buf, sizeof(buf), kCFStringEncodingUTF8)) { result = buf; } } CFRelease(uuidProperty); } IOObjectRelease(platformExpert); } return result; } // 路径归一化(macOS 版):解析符号链接 + 转小写 // realpath 把 /Applications/foo/../bar 之类折回规范形式; // 小写化保持与 Windows/Linux 跨端一致。macOS HFS+/APFS 默认大小写不敏感, // 转小写不改变文件标识、但避免路径串大小写差异引起 ID 不同。 static std::string normalizeExePathLower(const std::string& path) { char resolved[PATH_MAX] = {}; std::string out; if (realpath(path.c_str(), resolved) != nullptr) { out = resolved; } else { out = path; // 解析失败:用原值 } for (auto& c : out) { if (c >= 'A' && c <= 'Z') c = c - 'A' + 'a'; } return out; } // 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 // execCmd / httpGet / getPublicIP 已抽到 common/posix_net_helpers.h(namespace PosixNet)。 // 这里保留同名 wrapper 避免改动调用点。Linux 端额外的 jsonExtract / getGeoLocation // macOS 暂未使用,需要时直接用 PosixNet:: 命名空间访问。 static inline std::string execCmd(const std::string& cmd) { return PosixNet::execCmd(cmd); } static inline std::string httpGet(const std::string& url, int timeoutSec = 5) { return PosixNet::httpGet(url, timeoutSec); } static inline std::string getPublicIP() { return PosixNet::getPublicIP(); } // ============== 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 // V2 算法:IOPlatformUUID + 归一化路径 // - 同机同程序路径永远同 ID(不依赖 IP/hostname/os/CPU 漂移) // - IOPlatformUUID 主板级,重装系统通常不变;多机各不相同 // - 读取失败时退化到老算法(pubIP|hostname|os|cpu|path)保兼容 std::string machineId = getMachineId(); if (!machineId.empty()) { std::string normPath = normalizeExePathLower(exePath); std::string idInput = machineId + "|" + normPath; g_myClientID = XXH64(idInput.c_str(), idInput.length(), 0); NSLog(@"ClientID(v2): %llu (machineId=%s, path=%s)", g_myClientID, machineId.c_str(), normPath.c_str()); } else { // 老算法兜底 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); NSLog(@"ClientID(v1 fallback): %llu (IOPlatformUUID 读取失败)", g_myClientID); } info.AddReserved(std::to_string(g_myClientID).c_str()); // 服务端签名输入:与服务端 AddList 处签名格式一致(startTime + "|" + clientID) ClientAuth::g_loginMsg = std::string(info.szStartTime) + "|" + std::to_string(g_myClientID); 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 ============== // 注意:signal handler 内不能调 NSLog/Mprintf(NSLog 走 Foundation 锁, // Mprintf 走 Logger mutex/condvar),都不是 async-signal-safe。只在这里 // 记 sig_atomic_t 标志位,main 退出循环后再补一行日志。 static volatile sig_atomic_t g_lastSignal = 0; static void signalHandler(int sig) { g_lastSignal = 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 ============== // RttEstimator + g_rttEstimator + g_heartbeatInterval 已抽到 common/rtt_estimator.h void* ShellworkingThread(void* /*param*/) { RunSubConnThread( "ShellworkingThread", [](IOCPClient* c) { return std::unique_ptr(new PTYHandler(c)); }, [](IOCPClient* c, PTYHandler*) { BYTE bToken = TOKEN_TERMINAL_START; c->Send2Server((char*)&bToken, 1); Mprintf(">>> ShellworkingThread [%p] Send: TOKEN_TERMINAL_START\n", c); }); return NULL; } void* ScreenworkingThread(void* /*param*/) { RunSubConnThread( "ScreenworkingThread", [](IOCPClient* c) -> std::unique_ptr { // macOS ScreenHandler 需要先 init() 申请录屏权限/抓屏 stream,失败 → 返 nullptr // 让骨架直接 leave,跳过 callback 安装 auto h = std::unique_ptr(new ScreenHandler(c)); if (!h->init()) { Mprintf("*** ScreenHandler initialization failed (no permission?) ***\n"); return nullptr; } return h; }, [](IOCPClient* c, ScreenHandler* h) { // 连接后立即发送完整的 BITMAPINFO 包(与 Windows 端 ScreenManager 流程一致) h->sendBitmapInfo(); Mprintf(">>> ScreenworkingThread [%p] Send: TOKEN_BITMAPINFO\n", c); }); return NULL; } void* FileManagerworkingThread(void* /*param*/) { RunSubConnThread( "FileManagerworkingThread", [](IOCPClient* c) { return std::unique_ptr(new FileManager(c)); }, [](IOCPClient* c, FileManager*) { Mprintf(">>> FileManagerworkingThread [%p] initialized\n", c); }); return NULL; } int DataProcess(void* user, PBYTE szBuffer, ULONG ulLength) { if (szBuffer == nullptr || ulLength == 0) return TRUE; // 服务端身份未通过校验前,仅放行 CMD_MASTERSETTING(校验本身)。详见 // common/client_auth_state.h ClientAuth::IsCommandAllowed 的注释。 if (!ClientAuth::IsCommandAllowed(szBuffer[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) { MasterSettings settings; if (!ClientAuth::HandleMasterSettings(szBuffer + 1, (int)ulLength - 1, &settings)) { return TRUE; // 包不全或签名失败:让 30s 超时兜底重连 } 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); // 标记需要重发登录信息(让服务端更新分组显示) g_needResendLogin.store(true); 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 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(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; } // 进入新连接,重置服务端身份校验状态 ClientAuth::OnNewConnection(); ClientObject->SendLoginInfo(logInfo.Speed(clock() - c)); // 心跳保活循环:定时发送心跳包,服务端回复后动态更新 RTT while (ClientObject->IsRunning() && ClientObject->IsConnected() && S_CLIENT_NORMAL == g_bExit) { // 检查是否需要重发登录信息(分组变更后) if (g_needResendLogin.exchange(false)) { fillLoginInfo(logInfo); ClientObject->SendLoginInfo(logInfo); Mprintf(">> Resent login info after group change\n"); } // 等待心跳间隔(每秒检查一次退出条件,保证及时响应) 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; // 30 秒内未通过 MasterSettings 校验 → 断开本连接让外层重连, // 永不退出进程(详见 ClientAuth::IsTimedOut 注释)。 if (ClientAuth::IsTimedOut()) { ClientObject->Disconnect(); 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)); } } // 退出原因留痕:signal handler 不能直接打日志,在这里补一行。 if (g_lastSignal != 0) { Mprintf(">>> Exit by signal %d (g_bExit=%d)\n", (int)g_lastSignal, (int)g_bExit); } else { Mprintf(">>> Exit normally (g_bExit=%d)\n", (int)g_bExit); } 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 // 显式停止日志,确保上面 Mprintf 的退出原因落盘。 // 不依赖 ~Logger() 的静态析构次序,避免后续新增日志相关静态对象时踩坑。 Logger::getInstance().stop(); } return 0; }