3 Commits

Author SHA1 Message Date
yuanyuanxiang
56419f8ecb Fix: MFC remote desktop touchpad two-finger scroll not working 2026-05-01 23:25:32 +02:00
yuanyuanxiang
bb6fd7b1b9 Feature: Add user idle and screen lock detection for macOS 2026-05-01 23:20:46 +02:00
yuanyuanxiang
3607f1d768 Feature: Add power management to keep macOS client always responsive 2026-05-01 21:43:55 +02:00
4 changed files with 181 additions and 3 deletions

View File

@@ -2,6 +2,7 @@
#import <CoreGraphics/CoreGraphics.h> #import <CoreGraphics/CoreGraphics.h>
#import <dispatch/dispatch.h> #import <dispatch/dispatch.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import "../client/IOCPClient.h" #import "../client/IOCPClient.h"
#include <vector> #include <vector>
#include <atomic> #include <atomic>
@@ -132,4 +133,7 @@ private:
// Input handler for mouse/keyboard control // Input handler for mouse/keyboard control
std::unique_ptr<InputHandler> m_inputHandler; std::unique_ptr<InputHandler> m_inputHandler;
// Power management: prevent display sleep during remote desktop
IOPMAssertionID m_displayAssertionID;
}; };

View File

@@ -26,6 +26,7 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
, m_maxFPS(15) , m_maxFPS(15)
, m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility , m_qualityLevel(QUALITY_GOOD) // Use fixed QUALITY_GOOD (H264) for web compatibility
, m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD) , m_h264Bitrate(3000000) // 3 Mbps (matches Windows QUALITY_GOOD)
, m_displayAssertionID(0)
{ {
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader)); memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
@@ -115,6 +116,21 @@ void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
m_clientID = clientID; m_clientID = clientID;
m_running = true; m_running = true;
// Prevent display sleep during remote desktop session
if (m_displayAssertionID == 0) {
IOReturn result = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep,
kIOPMAssertionLevelOn,
CFSTR("SimpleRemoter - remote desktop session active"),
&m_displayAssertionID
);
if (result == kIOReturnSuccess) {
NSLog(@"Display sleep disabled for remote desktop (ID: %u)", m_displayAssertionID);
} else {
NSLog(@"Warning: Failed to prevent display sleep (error: 0x%x)", result);
}
}
m_captureThread = std::thread(&ScreenHandler::captureLoop, this); m_captureThread = std::thread(&ScreenHandler::captureLoop, this);
} }
@@ -130,6 +146,13 @@ void ScreenHandler::stop()
m_h264Encoder->close(); m_h264Encoder->close();
m_h264Encoder.reset(); m_h264Encoder.reset();
} }
// Release display sleep assertion - allow screen to turn off
if (m_displayAssertionID != 0) {
IOPMAssertionRelease(m_displayAssertionID);
NSLog(@"Display sleep re-enabled (released ID: %u)", m_displayAssertionID);
m_displayAssertionID = 0;
}
} }
void ScreenHandler::sendBitmapInfo() void ScreenHandler::sendBitmapInfo()

View File

@@ -6,6 +6,7 @@
#import <signal.h> #import <signal.h>
#import <unistd.h> #import <unistd.h>
#import <IOKit/IOKitLib.h> #import <IOKit/IOKitLib.h>
#import <IOKit/pwr_mgt/IOPMLib.h>
#import <fstream> #import <fstream>
#import <thread> #import <thread>
#import <atomic> #import <atomic>
@@ -140,9 +141,113 @@ static std::string getTimeString()
return std::string([dateString UTF8String]); return std::string([dateString UTF8String]);
} }
// Get active application name // 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() 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]; NSRunningApplication* app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) { if (app) {
NSString* name = [app localizedName]; NSString* name = [app localizedName];
@@ -478,6 +583,30 @@ int main(int argc, const char* argv[])
@autoreleasepool { @autoreleasepool {
NSLog(@"=== macOS Ghost Client ==="); NSLog(@"=== macOS Ghost Client ===");
// ============== 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 // Setup signal handlers
setupSignals(); setupSignals();
@@ -549,6 +678,15 @@ int main(int argc, const char* argv[])
} }
NSLog(@"Shutting down..."); 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; return 0;

View File

@@ -2305,8 +2305,8 @@ BOOL CScreenSpyDlg::PreTranslateMessage(MSG* pMsg)
MSG wheelMsg = *pMsg; MSG wheelMsg = *pMsg;
wheelMsg.lParam = MAKELPARAM(pt.x, pt.y); wheelMsg.lParam = MAKELPARAM(pt.x, pt.y);
SendScaledMouseMessage(&wheelMsg, true); SendScaledMouseMessage(&wheelMsg, true);
return TRUE; // 已处理,阻止继续分发到 OnMouseWheel
} }
break;
case WM_KEYDOWN: case WM_KEYDOWN:
case WM_KEYUP: case WM_KEYUP:
case WM_SYSKEYDOWN: case WM_SYSKEYDOWN:
@@ -2682,7 +2682,20 @@ void CScreenSpyDlg::OnLButtonUp(UINT nFlags, CPoint point)
BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) BOOL CScreenSpyDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt)
{ {
return __super::OnMouseWheel(nFlags, zDelta, pt); // Convert screen coordinates to client coordinates
ScreenToClient(&pt);
// Build MSG structure for SendScaledMouseMessage
MSG msg = {};
msg.hwnd = m_hWnd;
msg.message = WM_MOUSEWHEEL;
msg.wParam = MAKEWPARAM(nFlags, zDelta);
msg.lParam = MAKELPARAM(pt.x, pt.y);
msg.time = GetTickCount();
msg.pt = { pt.x, pt.y };
SendScaledMouseMessage(&msg, true);
return TRUE; // Message handled, don't pass to parent
} }