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>
This commit is contained in:
yuanyuanxiang
2026-05-03 23:18:30 +02:00
parent b732f841d0
commit 92f3df8464
7 changed files with 483 additions and 43 deletions

View File

@@ -45,6 +45,7 @@ find_library(CARBON_FRAMEWORK Carbon REQUIRED)
find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED) find_library(VIDEOTOOLBOX_FRAMEWORK VideoToolbox REQUIRED)
find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED) find_library(COREMEDIA_FRAMEWORK CoreMedia REQUIRED)
find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED) find_library(COREVIDEO_FRAMEWORK CoreVideo REQUIRED)
find_library(ACCELERATE_FRAMEWORK Accelerate REQUIRED)
find_library(ICONV_LIBRARY iconv REQUIRED) find_library(ICONV_LIBRARY iconv REQUIRED)
target_link_libraries(ghost PRIVATE target_link_libraries(ghost PRIVATE
@@ -58,6 +59,7 @@ target_link_libraries(ghost PRIVATE
${VIDEOTOOLBOX_FRAMEWORK} ${VIDEOTOOLBOX_FRAMEWORK}
${COREMEDIA_FRAMEWORK} ${COREMEDIA_FRAMEWORK}
${COREVIDEO_FRAMEWORK} ${COREVIDEO_FRAMEWORK}
${ACCELERATE_FRAMEWORK}
${ICONV_LIBRARY} ${ICONV_LIBRARY}
"${CMAKE_SOURCE_DIR}/lib/libzstd.a" "${CMAKE_SOURCE_DIR}/lib/libzstd.a"
) )

View File

@@ -6,8 +6,27 @@
bool Permissions::checkScreenCapture() { bool Permissions::checkScreenCapture() {
// macOS 10.15+ requires screen recording permission // macOS 10.15+ requires screen recording permission
if (@available(macOS 10.15, *)) { if (@available(macOS 10.15, *)) {
// Use CGPreflightScreenCaptureAccess for reliable permission check // CGPreflightScreenCaptureAccess() is unreliable - it can return false
// This API is available since macOS 10.15 // even when permission is granted (especially after code re-signing).
// Instead, actually try to capture the screen to verify permission.
CGDirectDisplayID displayID = CGMainDisplayID();
CGImageRef image = CGDisplayCreateImage(displayID);
if (image != NULL) {
// Got an image - permission is granted
// Additional check: verify image has actual content (not blank)
size_t width = CGImageGetWidth(image);
size_t height = CGImageGetHeight(image);
CGImageRelease(image);
if (width > 0 && height > 0) {
return true;
}
}
// Failed to capture - permission not granted or display issue
// Fall back to preflight check for triggering dialog
return CGPreflightScreenCaptureAccess(); return CGPreflightScreenCaptureAccess();
} }

View File

@@ -3,6 +3,7 @@
#import <CoreGraphics/CoreGraphics.h> #import <CoreGraphics/CoreGraphics.h>
#import <dispatch/dispatch.h> #import <dispatch/dispatch.h>
#import <IOKit/pwr_mgt/IOPMLib.h> #import <IOKit/pwr_mgt/IOPMLib.h>
#import <IOSurface/IOSurface.h>
#import "../client/IOCPClient.h" #import "../client/IOCPClient.h"
#import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_* #import "../common/commands.h" // QualityLevel, QualityProfile, ALGORITHM_*
#include <vector> #include <vector>
@@ -11,6 +12,7 @@
#include <thread> #include <thread>
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <condition_variable>
// Forward declarations // Forward declarations
class IOCPClient; class IOCPClient;
@@ -118,6 +120,7 @@ private:
std::vector<uint8_t> m_prevFrame; std::vector<uint8_t> m_prevFrame;
std::vector<uint8_t> m_currFrame; std::vector<uint8_t> m_currFrame;
std::vector<uint8_t> m_diffBuffer; std::vector<uint8_t> m_diffBuffer;
std::vector<uint8_t> m_tempBuffer; // 临时缓冲区,避免每帧分配
// Quality settings // Quality settings
std::atomic<uint8_t> m_algorithm; std::atomic<uint8_t> m_algorithm;
@@ -133,4 +136,25 @@ private:
// Power management: prevent display sleep during remote desktop // Power management: prevent display sleep during remote desktop
IOPMAssertionID m_displayAssertionID; IOPMAssertionID m_displayAssertionID;
// Cached color space (avoid per-frame creation)
CGColorSpaceRef m_colorSpace;
// CGDisplayStream (efficient continuous capture)
CGDisplayStreamRef m_displayStream;
dispatch_queue_t m_streamQueue;
IOSurfaceRef m_latestSurface;
std::mutex m_surfaceMutex;
std::condition_variable m_surfaceCond;
std::atomic<bool> m_hasNewFrame;
// Initialize/cleanup display stream
bool initDisplayStream();
void cleanupDisplayStream();
// Process frame from IOSurface (called by stream callback)
void processIOSurface(IOSurfaceRef surface);
// Capture from IOSurface to buffer (with vertical flip)
bool captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer);
}; };

View File

@@ -12,6 +12,7 @@
#import <CoreGraphics/CoreGraphics.h> #import <CoreGraphics/CoreGraphics.h>
#import <ApplicationServices/ApplicationServices.h> #import <ApplicationServices/ApplicationServices.h>
#import <mach/mach_time.h> #import <mach/mach_time.h>
#import <Accelerate/Accelerate.h>
// Global client ID (calculated in main.mm) // Global client ID (calculated in main.mm)
extern uint64_t g_myClientID; extern uint64_t g_myClientID;
@@ -31,9 +32,17 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
, 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) , m_displayAssertionID(0)
, m_colorSpace(nullptr)
, m_displayStream(nullptr)
, m_streamQueue(nullptr)
, m_latestSurface(nullptr)
, m_hasNewFrame(false)
{ {
memset(&m_bmpHeader, 0, sizeof(m_bmpHeader)); memset(&m_bmpHeader, 0, sizeof(m_bmpHeader));
// Cache color space (avoid per-frame creation)
m_colorSpace = CGColorSpaceCreateDeviceRGB();
// Initialize input handler for mouse/keyboard control // Initialize input handler for mouse/keyboard control
m_inputHandler = std::make_unique<InputHandler>(); m_inputHandler = std::make_unique<InputHandler>();
if (m_inputHandler->init()) { if (m_inputHandler->init()) {
@@ -46,6 +55,13 @@ ScreenHandler::ScreenHandler(IOCPClient* client)
ScreenHandler::~ScreenHandler() ScreenHandler::~ScreenHandler()
{ {
stop(); stop();
cleanupDisplayStream();
// Release cached color space
if (m_colorSpace) {
CGColorSpaceRelease(m_colorSpace);
m_colorSpace = nullptr;
}
} }
bool ScreenHandler::init() bool ScreenHandler::init()
@@ -153,10 +169,191 @@ bool ScreenHandler::init()
NSLog(@"Display wake complete"); NSLog(@"Display wake complete");
} }
// Initialize CGDisplayStream for efficient capture
if (!initDisplayStream()) {
NSLog(@"Warning: CGDisplayStream init failed, falling back to legacy capture");
}
NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height); NSLog(@"ScreenHandler initialized: %dx%d", m_width, m_height);
return true; return true;
} }
bool ScreenHandler::initDisplayStream()
{
// Create dispatch queue for stream callbacks
m_streamQueue = dispatch_queue_create("com.ghost.screenstream", DISPATCH_QUEUE_SERIAL);
if (!m_streamQueue) {
NSLog(@"Failed to create dispatch queue for display stream");
return false;
}
// Stream properties
CFMutableDictionaryRef properties = CFDictionaryCreateMutable(
kCFAllocatorDefault, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks
);
// Request minimum frame interval based on FPS (e.g., 15 FPS = 1/15 sec)
int fps = m_maxFPS.load();
if (fps <= 0) fps = 15;
double interval = 1.0 / (double)fps;
CFNumberRef intervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberDoubleType, &interval);
CFDictionarySetValue(properties, kCGDisplayStreamMinimumFrameTime, intervalRef);
CFRelease(intervalRef);
// Show cursor in stream
CFDictionarySetValue(properties, kCGDisplayStreamShowCursor, kCFBooleanFalse);
// Preserve aspect ratio
CFDictionarySetValue(properties, kCGDisplayStreamPreserveAspectRatio, kCFBooleanTrue);
// Create the display stream with BGRA format
__block ScreenHandler* handler = this;
m_displayStream = CGDisplayStreamCreateWithDispatchQueue(
m_displayID,
m_width,
m_height,
'BGRA', // Pixel format
properties,
m_streamQueue,
^(CGDisplayStreamFrameStatus status,
uint64_t displayTime,
IOSurfaceRef frameSurface,
CGDisplayStreamUpdateRef updateRef) {
(void)displayTime;
(void)updateRef;
if (status == kCGDisplayStreamFrameStatusFrameComplete && frameSurface) {
handler->processIOSurface(frameSurface);
} else if (status == kCGDisplayStreamFrameStatusFrameIdle) {
// Screen not changed, still notify for FPS timing
handler->m_hasNewFrame.store(true);
handler->m_surfaceCond.notify_one();
} else if (status == kCGDisplayStreamFrameStatusStopped) {
NSLog(@"CGDisplayStream stopped");
}
}
);
CFRelease(properties);
if (!m_displayStream) {
NSLog(@"Failed to create CGDisplayStream");
m_streamQueue = nullptr; // ARC manages dispatch objects
return false;
}
// Start the stream
CGError err = CGDisplayStreamStart(m_displayStream);
if (err != kCGErrorSuccess) {
NSLog(@"Failed to start CGDisplayStream: %d", err);
CFRelease(m_displayStream);
m_displayStream = nullptr;
m_streamQueue = nullptr; // ARC manages dispatch objects
return false;
}
NSLog(@"CGDisplayStream started: %dx%d @ %d FPS", m_width, m_height, fps);
return true;
}
void ScreenHandler::cleanupDisplayStream()
{
if (m_displayStream) {
CGDisplayStreamStop(m_displayStream);
CFRelease(m_displayStream);
m_displayStream = nullptr;
}
// ARC manages dispatch objects, just nil the pointer
m_streamQueue = nullptr;
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
CFRelease(m_latestSurface);
m_latestSurface = nullptr;
}
}
void ScreenHandler::processIOSurface(IOSurfaceRef surface)
{
// Retain the surface and store it
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
CFRelease(m_latestSurface);
}
m_latestSurface = (IOSurfaceRef)CFRetain(surface);
m_hasNewFrame.store(true);
m_surfaceCond.notify_one();
}
bool ScreenHandler::captureFromIOSurface(IOSurfaceRef surface, std::vector<uint8_t>& buffer)
{
if (!surface) return false;
// Lock the surface for CPU read
IOSurfaceLock(surface, kIOSurfaceLockReadOnly, nullptr);
size_t width = IOSurfaceGetWidth(surface);
size_t height = IOSurfaceGetHeight(surface);
size_t bytesPerRow = IOSurfaceGetBytesPerRow(surface);
void* baseAddr = IOSurfaceGetBaseAddress(surface);
if (!baseAddr || width != (size_t)m_width || height != (size_t)m_height) {
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
return false;
}
// Ensure temp buffer is allocated
size_t requiredSize = m_width * 4 * m_height;
if (m_tempBuffer.size() != requiredSize) {
m_tempBuffer.resize(requiredSize);
}
// Copy from IOSurface to temp buffer (handle different bytesPerRow)
size_t dstBytesPerRow = m_width * 4;
if (bytesPerRow == dstBytesPerRow) {
memcpy(m_tempBuffer.data(), baseAddr, requiredSize);
} else {
// Row by row copy for different strides
uint8_t* src = (uint8_t*)baseAddr;
uint8_t* dst = m_tempBuffer.data();
for (size_t y = 0; y < height; y++) {
memcpy(dst + y * dstBytesPerRow, src + y * bytesPerRow, dstBytesPerRow);
}
}
IOSurfaceUnlock(surface, kIOSurfaceLockReadOnly, nullptr);
// Flip vertically using Accelerate framework (SIMD optimized)
vImage_Buffer src = {
.data = m_tempBuffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = dstBytesPerRow
};
vImage_Buffer dst = {
.data = buffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = dstBytesPerRow
};
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
if (err != kvImageNoError) {
// Fallback to manual flip
for (size_t y = 0; y < height; y++) {
memcpy(buffer.data() + (height - 1 - y) * dstBytesPerRow,
m_tempBuffer.data() + y * dstBytesPerRow,
dstBytesPerRow);
}
}
return true;
}
void ScreenHandler::start(IOCPClient* client, uint64_t clientID) void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
{ {
// If already running, just send TOKEN_BITMAPINFO again // If already running, just send TOKEN_BITMAPINFO again
@@ -190,6 +387,10 @@ void ScreenHandler::start(IOCPClient* client, uint64_t clientID)
void ScreenHandler::stop() void ScreenHandler::stop()
{ {
m_running = false; m_running = false;
// Wake up capture thread if waiting
m_surfaceCond.notify_all();
if (m_captureThread.joinable()) { if (m_captureThread.joinable()) {
m_captureThread.join(); m_captureThread.join();
} }
@@ -451,7 +652,27 @@ void ScreenHandler::applyQualityLevel(int8_t level, bool persist)
bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer) bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
{ {
// Create image from display // Try to use IOSurface from display stream (more efficient)
if (m_displayStream) {
IOSurfaceRef surface = nullptr;
{
std::lock_guard<std::mutex> lock(m_surfaceMutex);
if (m_latestSurface) {
surface = (IOSurfaceRef)CFRetain(m_latestSurface);
}
}
if (surface) {
bool result = captureFromIOSurface(surface, buffer);
CFRelease(surface);
if (result) {
return true;
}
}
// Fall through to legacy method if IOSurface failed
}
// Legacy method: CGDisplayCreateImage (fallback)
CGImageRef image = CGDisplayCreateImage(m_displayID); CGImageRef image = CGDisplayCreateImage(m_displayID);
if (!image) { if (!image) {
NSLog(@"Failed to capture screen image"); NSLog(@"Failed to capture screen image");
@@ -462,50 +683,59 @@ bool ScreenHandler::captureScreen(std::vector<uint8_t>& buffer)
size_t height = CGImageGetHeight(image); size_t height = CGImageGetHeight(image);
if (width != (size_t)m_width || height != (size_t)m_height) { if (width != (size_t)m_width || height != (size_t)m_height) {
// Screen resolution changed, need to reinitialize
CGImageRelease(image); CGImageRelease(image);
NSLog(@"Screen resolution changed: %zux%zu", width, height); NSLog(@"Screen resolution changed: %zux%zu", width, height);
return false; return false;
} }
// Create bitmap context to get raw pixel data
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
size_t bytesPerRow = width * 4; size_t bytesPerRow = width * 4;
size_t requiredSize = bytesPerRow * height;
// Temporary buffer for top-down BGRA if (m_tempBuffer.size() != requiredSize) {
std::vector<uint8_t> tempBuffer(bytesPerRow * height); m_tempBuffer.resize(requiredSize);
}
CGContextRef context = CGBitmapContextCreate( CGContextRef context = CGBitmapContextCreate(
tempBuffer.data(), m_tempBuffer.data(),
width, width,
height, height,
8, 8,
bytesPerRow, bytesPerRow,
colorSpace, m_colorSpace,
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little // BGRA kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little
); );
CGColorSpaceRelease(colorSpace);
if (!context) { if (!context) {
CGImageRelease(image); CGImageRelease(image);
NSLog(@"Failed to create bitmap context"); NSLog(@"Failed to create bitmap context");
return false; return false;
} }
// Draw image into context
CGContextDrawImage(context, CGRectMake(0, 0, width, height), image); CGContextDrawImage(context, CGRectMake(0, 0, width, height), image);
CGContextRelease(context); CGContextRelease(context);
CGImageRelease(image); CGImageRelease(image);
// Flip vertically (BMP is bottom-up, CGImage is top-down) // Flip vertically using Accelerate framework
vImage_Buffer src = {
.data = m_tempBuffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = bytesPerRow
};
vImage_Buffer dst = {
.data = buffer.data(),
.height = (vImagePixelCount)height,
.width = (vImagePixelCount)width,
.rowBytes = bytesPerRow
};
vImage_Error err = vImageVerticalReflect_ARGB8888(&src, &dst, kvImageNoFlags);
if (err != kvImageNoError) {
for (size_t y = 0; y < height; y++) { for (size_t y = 0; y < height; y++) {
size_t srcRow = y; memcpy(buffer.data() + (height - 1 - y) * bytesPerRow,
size_t dstRow = height - 1 - y; m_tempBuffer.data() + y * bytesPerRow,
memcpy(buffer.data() + dstRow * bytesPerRow,
tempBuffer.data() + srcRow * bytesPerRow,
bytesPerRow); bytesPerRow);
} }
}
return true; return true;
} }
@@ -766,10 +996,11 @@ uint8_t ScreenHandler::getCursorTypeIndex()
// Reuse cursor position from getCursorPosition (called before this) // Reuse cursor position from getCursorPosition (called before this)
CGPoint pos = s_cachedLogicalPos; CGPoint pos = s_cachedLogicalPos;
// Throttle: only check if cursor moved significantly or 100ms elapsed // Throttle: only check if cursor moved significantly or 250ms elapsed
// (Accessibility API is expensive, cursor type is just a visual hint)
uint64_t now = getTickMs(); uint64_t now = getTickMs();
bool posChanged = (fabs(pos.x - lastPos.x) > 5 || fabs(pos.y - lastPos.y) > 5); bool posChanged = (fabs(pos.x - lastPos.x) > 10 || fabs(pos.y - lastPos.y) > 10);
if (!posChanged && (now - lastCheckTime) < 100) { if (!posChanged && (now - lastCheckTime) < 250) {
return cachedIndex; return cachedIndex;
} }
lastCheckTime = now; lastCheckTime = now;
@@ -842,13 +1073,12 @@ uint8_t ScreenHandler::getCursorTypeIndex()
void ScreenHandler::captureLoop() void ScreenHandler::captureLoop()
{ {
NSLog(@"ScreenHandler CaptureLoop started (%dx%d)", m_width, m_height); NSLog(@"ScreenHandler CaptureLoop started (%dx%d)%s", m_width, m_height,
m_displayStream ? " [CGDisplayStream]" : " [Legacy]");
uint8_t currentAlgo = m_algorithm.load(); uint8_t currentAlgo = m_algorithm.load();
// Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display // Always send raw first frame (TOKEN_FIRSTSCREEN) to initialize server display
// This matches Windows client behavior: first frame is always raw bitmap,
// even in H264 mode. Server needs TOKEN_FIRSTSCREEN to set m_bIsFirst = FALSE.
sendFirstScreen(); sendFirstScreen();
// Small delay to ensure first frame is processed before H264 stream starts // Small delay to ensure first frame is processed before H264 stream starts
@@ -857,6 +1087,23 @@ void ScreenHandler::captureLoop()
while (m_running) { while (m_running) {
uint64_t start = getTickMs(); uint64_t start = getTickMs();
// Wait for new frame from display stream (push model)
// This is key optimization: CPU sleeps when screen is static
if (m_displayStream) {
std::unique_lock<std::mutex> lock(m_surfaceMutex);
int fps = m_maxFPS.load();
if (fps <= 0) fps = 15;
int waitMs = 1000 / fps;
// Wait for new frame or timeout (maintains FPS even if no change)
m_surfaceCond.wait_for(lock, std::chrono::milliseconds(waitMs), [this] {
return m_hasNewFrame.load() || !m_running;
});
m_hasNewFrame.store(false);
if (!m_running) break;
}
uint8_t algo = m_algorithm.load(); uint8_t algo = m_algorithm.load();
// Check if algorithm changed // Check if algorithm changed
@@ -864,18 +1111,14 @@ void ScreenHandler::captureLoop()
NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo); NSLog(@"Algorithm changed: %d -> %d", currentAlgo, algo);
currentAlgo = algo; currentAlgo = algo;
// If switching to/from H264, reset encoder
if (algo == ALGORITHM_H264) { if (algo == ALGORITHM_H264) {
// Starting H264 - will be initialized in sendH264Frame
sendH264Frame(true); // First H264 frame is keyframe sendH264Frame(true); // First H264 frame is keyframe
} else if (m_h264Encoder) { } else if (m_h264Encoder) {
// Switching away from H264 - close encoder
m_h264Encoder->close(); m_h264Encoder->close();
m_h264Encoder.reset(); m_h264Encoder.reset();
sendFirstScreen(); // Send full frame for DIFF modes sendFirstScreen();
} }
} else { } else {
// Normal frame
if (algo == ALGORITHM_H264) { if (algo == ALGORITHM_H264) {
sendH264Frame(false); sendH264Frame(false);
} else { } else {
@@ -883,6 +1126,8 @@ void ScreenHandler::captureLoop()
} }
} }
// Only use sleep-based FPS control for legacy mode
if (!m_displayStream) {
int fps = m_maxFPS.load(); int fps = m_maxFPS.load();
if (fps <= 0) fps = 10; if (fps <= 0) fps = 10;
int sleepMs = 1000 / fps; int sleepMs = 1000 / fps;
@@ -893,6 +1138,7 @@ void ScreenHandler::captureLoop()
usleep(wait * 1000); usleep(wait * 1000);
} }
} }
}
NSLog(@"ScreenHandler CaptureLoop stopped"); NSLog(@"ScreenHandler CaptureLoop stopped");
} }

103
macos/install.sh Normal file
View File

@@ -0,0 +1,103 @@
#!/bin/bash
# macOS Ghost Client 安装脚本
# 用法: ./install.sh [ghost路径]
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
GHOST_SRC="${1:-$SCRIPT_DIR/build/bin/ghost}"
GHOST_DST="/usr/local/bin/ghost"
PLIST_DST="/Library/LaunchDaemons/com.ghost.client.plist"
echo "=== Ghost Client 安装程序 ==="
echo "源文件: $GHOST_SRC"
# 检查源文件
if [ ! -f "$GHOST_SRC" ]; then
echo ""
echo "错误: 找不到 $GHOST_SRC"
echo ""
echo "请先编译: ./build.sh"
echo ""
echo "或指定路径: $0 <ghost可执行文件路径>"
exit 1
fi
set -e
# 1. 停止旧服务(只停止安装目录的,不影响调试目录)
echo "[1/6] 停止旧服务..."
sudo launchctl unload "$PLIST_DST" 2>/dev/null || true
sudo pkill -9 -f "$GHOST_DST" 2>/dev/null || true
# 2. 复制程序
echo "[2/6] 安装程序到 $GHOST_DST..."
sudo cp "$GHOST_SRC" "$GHOST_DST"
sudo chmod +x "$GHOST_DST"
# 3. 清除隔离属性
echo "[3/6] 清除隔离属性..."
sudo xattr -cr "$GHOST_DST"
# 4. 签名
echo "[4/6] 签名程序..."
sudo codesign --force --deep --sign - "$GHOST_DST"
# 5. 创建 launchd plist
echo "[5/6] 创建 launchd 服务..."
sudo tee "$PLIST_DST" > /dev/null << 'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ghost.client</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/ghost</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/var/log/ghost.log</string>
<key>StandardErrorPath</key>
<string>/var/log/ghost.log</string>
</dict>
</plist>
EOF
sudo chown root:wheel "$PLIST_DST"
sudo chmod 644 "$PLIST_DST"
# 6. 完成
echo "[6/6] 安装完成!"
echo ""
echo "========================================"
echo "重要: 首次运行需要授权系统权限"
echo "========================================"
echo ""
echo "请执行以下步骤:"
echo ""
echo "1. 手动运行以触发权限请求:"
echo " $GHOST_DST"
echo ""
echo "2. 授权后按 Ctrl+C 停止程序(权限需重启生效)"
echo ""
echo "3. 启动服务:"
echo " sudo launchctl load $PLIST_DST"
echo ""
echo "如未弹出授权对话框,手动添加:"
echo " 系统设置 > 隐私与安全性 > 屏幕录制 > 添加 ghost"
echo " 系统设置 > 隐私与安全性 > 辅助功能 > 添加 ghost"
echo ""
echo "常用命令:"
echo " 启动: sudo launchctl start com.ghost.client"
echo " 停止: sudo launchctl stop com.ghost.client"
echo " 卸载: sudo launchctl unload $PLIST_DST"
echo " 日志: tail -f /var/log/ghost.log"
echo ""

View File

@@ -573,6 +573,7 @@ static void signalHandler(int sig)
{ {
NSLog(@"Received signal %d, shutting down...", sig); NSLog(@"Received signal %d, shutting down...", sig);
g_running = false; g_running = false;
g_bExit = S_CLIENT_EXIT; // 通知所有工作线程退出
} }
static void setupSignals() static void setupSignals()
@@ -860,13 +861,25 @@ int main(int argc, const char* argv[])
// Check permissions // Check permissions
NSLog(@"Checking permissions..."); NSLog(@"Checking permissions...");
if (!Permissions::checkScreenCapture()) { bool hasScreenCapture = Permissions::checkScreenCapture();
if (hasScreenCapture) {
NSLog(@"Screen capture permission: OK");
} else {
NSLog(@"Screen capture permission not granted."); NSLog(@"Screen capture permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Screen Recording"); 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(); Permissions::openScreenCaptureSettings();
} }
}
if (!Permissions::checkAccessibility()) { bool hasAccessibility = Permissions::checkAccessibility();
if (hasAccessibility) {
NSLog(@"Accessibility permission: OK");
} else {
NSLog(@"Accessibility permission not granted."); NSLog(@"Accessibility permission not granted.");
NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility"); NSLog(@"Please grant permission in System Preferences > Privacy & Security > Accessibility");
Permissions::requestAccessibility(); Permissions::requestAccessibility();
@@ -877,6 +890,8 @@ int main(int argc, const char* argv[])
NSLog(@"Full Disk Access: not detected (may be false negative)."); 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"); 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 // Don't auto-open settings since detection is unreliable
} else {
NSLog(@"Full Disk Access: OK");
} }
// Create client // Create client

31
macos/uninstall.sh Normal file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# macOS Ghost Client 卸载脚本
echo "=== Ghost Client 卸载程序 ==="
# 1. 停止并卸载 launchd 服务
echo "[1/4] 停止服务..."
sudo launchctl unload /Library/LaunchDaemons/com.ghost.client.plist 2>/dev/null
launchctl unload ~/Library/LaunchAgents/com.ghost.client.plist 2>/dev/null
# 2. 杀死残留进程
echo "[2/4] 终止进程..."
sudo pkill -9 -f "/usr/local/bin/ghost" 2>/dev/null
# 3. 删除文件
echo "[3/4] 删除文件..."
sudo rm -f /Library/LaunchDaemons/com.ghost.client.plist
rm -f ~/Library/LaunchAgents/com.ghost.client.plist
sudo rm -f /usr/local/bin/ghost
rm -rf ~/.config/ghost
sudo rm -f /var/log/ghost.log
# 4. 完成
echo "[4/4] 卸载完成!"
echo ""
echo "注意: 系统权限(屏幕录制/辅助功能)未重置。"
echo ""
echo "如需重置系统权限(会影响所有应用),请手动执行:"
echo " tccutil reset ScreenCapture"
echo " tccutil reset Accessibility"
echo " tccutil reset SystemPolicyAllFiles"