mirror of
https://github.com/samsonjs/Peekaboo.git
synced 2026-03-25 09:25:47 +00:00
Fix Screen Recording permission detection and improve error reporting
- Replace broken CGDisplayBounds check with ScreenCaptureKit API - Add proper error handling to detect permission-related failures - Add server_status subcommand to expose permission status via JSON - Ensure users get clear error messages when permissions are missing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a491adbdf1
commit
de5a0cb97e
3 changed files with 143 additions and 63 deletions
|
|
@ -322,38 +322,55 @@ struct ImageCommand: ParsableCommand {
|
|||
if let error = captureError {
|
||||
throw error
|
||||
}
|
||||
} catch let error as CaptureError {
|
||||
// Re-throw CaptureError as-is
|
||||
throw error
|
||||
} catch {
|
||||
// Check if this is a permission error from ScreenCaptureKit
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
if errorString.contains("screen recording") || errorString.contains("permission") {
|
||||
throw CaptureError.screenRecordingPermissionDenied
|
||||
}
|
||||
throw CaptureError.captureCreationFailed
|
||||
}
|
||||
}
|
||||
|
||||
private func captureDisplayWithScreenCaptureKit(_ displayID: CGDirectDisplayID, to path: String) async throws {
|
||||
// Get available content
|
||||
let availableContent = try await SCShareableContent.current
|
||||
do {
|
||||
// Get available content
|
||||
let availableContent = try await SCShareableContent.current
|
||||
|
||||
// Find the display by ID
|
||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||
throw CaptureError.captureCreationFailed
|
||||
// Find the display by ID
|
||||
guard let scDisplay = availableContent.displays.first(where: { $0.displayID == displayID }) else {
|
||||
throw CaptureError.captureCreationFailed
|
||||
}
|
||||
|
||||
// Create content filter for the entire display
|
||||
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
||||
|
||||
// Configure capture settings
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.width = scDisplay.width
|
||||
configuration.height = scDisplay.height
|
||||
configuration.backgroundColor = .black
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = true
|
||||
|
||||
// Capture the image
|
||||
let image = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
try saveImage(image, to: path)
|
||||
} catch {
|
||||
// Check if this is a permission error
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
if errorString.contains("screen recording") || errorString.contains("permission") {
|
||||
throw CaptureError.screenRecordingPermissionDenied
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Create content filter for the entire display
|
||||
let filter = SCContentFilter(display: scDisplay, excludingWindows: [])
|
||||
|
||||
// Configure capture settings
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.width = scDisplay.width
|
||||
configuration.height = scDisplay.height
|
||||
configuration.backgroundColor = .black
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = true
|
||||
|
||||
// Capture the image
|
||||
let image = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
try saveImage(image, to: path)
|
||||
}
|
||||
|
||||
private func captureWindow(_ window: WindowData, to path: String) throws(CaptureError) {
|
||||
|
|
@ -375,38 +392,55 @@ struct ImageCommand: ParsableCommand {
|
|||
if let error = captureError {
|
||||
throw error
|
||||
}
|
||||
} catch let error as CaptureError {
|
||||
// Re-throw CaptureError as-is
|
||||
throw error
|
||||
} catch {
|
||||
// Check if this is a permission error from ScreenCaptureKit
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
if errorString.contains("screen recording") || errorString.contains("permission") {
|
||||
throw CaptureError.screenRecordingPermissionDenied
|
||||
}
|
||||
throw CaptureError.windowCaptureFailed
|
||||
}
|
||||
}
|
||||
|
||||
private func captureWindowWithScreenCaptureKit(_ window: WindowData, to path: String) async throws {
|
||||
// Get available content
|
||||
let availableContent = try await SCShareableContent.current
|
||||
do {
|
||||
// Get available content
|
||||
let availableContent = try await SCShareableContent.current
|
||||
|
||||
// Find the window by ID
|
||||
guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else {
|
||||
throw CaptureError.windowNotFound
|
||||
// Find the window by ID
|
||||
guard let scWindow = availableContent.windows.first(where: { $0.windowID == window.windowId }) else {
|
||||
throw CaptureError.windowNotFound
|
||||
}
|
||||
|
||||
// Create content filter for the specific window
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
|
||||
// Configure capture settings
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.width = Int(window.bounds.width)
|
||||
configuration.height = Int(window.bounds.height)
|
||||
configuration.backgroundColor = .clear
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = false
|
||||
|
||||
// Capture the image
|
||||
let image = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
try saveImage(image, to: path)
|
||||
} catch {
|
||||
// Check if this is a permission error
|
||||
let errorString = error.localizedDescription.lowercased()
|
||||
if errorString.contains("screen recording") || errorString.contains("permission") {
|
||||
throw CaptureError.screenRecordingPermissionDenied
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
// Create content filter for the specific window
|
||||
let filter = SCContentFilter(desktopIndependentWindow: scWindow)
|
||||
|
||||
// Configure capture settings
|
||||
let configuration = SCStreamConfiguration()
|
||||
configuration.width = Int(window.bounds.width)
|
||||
configuration.height = Int(window.bounds.height)
|
||||
configuration.backgroundColor = .clear
|
||||
configuration.shouldBeOpaque = true
|
||||
configuration.showsCursor = false
|
||||
|
||||
// Capture the image
|
||||
let image = try await SCScreenshotManager.captureImage(
|
||||
contentFilter: filter,
|
||||
configuration: configuration
|
||||
)
|
||||
|
||||
try saveImage(image, to: path)
|
||||
}
|
||||
|
||||
private func saveImage(_ image: CGImage, to path: String) throws(CaptureError) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ struct ListCommand: ParsableCommand {
|
|||
static let configuration = CommandConfiguration(
|
||||
commandName: "list",
|
||||
abstract: "List running applications or windows",
|
||||
subcommands: [AppsSubcommand.self, WindowsSubcommand.self],
|
||||
subcommands: [AppsSubcommand.self, WindowsSubcommand.self, ServerStatusSubcommand.self],
|
||||
defaultSubcommand: AppsSubcommand.self
|
||||
)
|
||||
}
|
||||
|
|
@ -177,3 +177,44 @@ struct WindowsSubcommand: ParsableCommand {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerStatusSubcommand: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "server_status",
|
||||
abstract: "Check server permissions status"
|
||||
)
|
||||
|
||||
@Flag(name: .long, help: "Output results in JSON format")
|
||||
var jsonOutput = false
|
||||
|
||||
func run() throws {
|
||||
Logger.shared.setJsonOutputMode(jsonOutput)
|
||||
|
||||
let screenRecording = PermissionsChecker.checkScreenRecordingPermission()
|
||||
let accessibility = PermissionsChecker.checkAccessibilityPermission()
|
||||
|
||||
let permissions = ServerPermissions(
|
||||
screen_recording: screenRecording,
|
||||
accessibility: accessibility
|
||||
)
|
||||
|
||||
let data = ServerStatusData(permissions: permissions)
|
||||
|
||||
if jsonOutput {
|
||||
outputSuccess(data: data)
|
||||
} else {
|
||||
print("Server Permissions Status:")
|
||||
print(" Screen Recording: \(screenRecording ? "✅ Granted" : "❌ Not granted")")
|
||||
print(" Accessibility: \(accessibility ? "✅ Granted" : "❌ Not granted")")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ServerPermissions: Codable {
|
||||
let screen_recording: Bool
|
||||
let accessibility: Bool
|
||||
}
|
||||
|
||||
struct ServerStatusData: Codable {
|
||||
let permissions: ServerPermissions
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
import AVFoundation
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import ScreenCaptureKit
|
||||
|
||||
class PermissionsChecker {
|
||||
static func checkScreenRecordingPermission() -> Bool {
|
||||
// Check if we can capture screen content by trying to get display bounds
|
||||
var displayCount: UInt32 = 0
|
||||
let result = CGGetActiveDisplayList(0, nil, &displayCount)
|
||||
if result != .success || displayCount == 0 {
|
||||
return false
|
||||
// ScreenCaptureKit requires screen recording permission
|
||||
// We check by attempting to get shareable content
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var hasPermission = false
|
||||
|
||||
Task {
|
||||
do {
|
||||
// This will fail if we don't have screen recording permission
|
||||
_ = try await SCShareableContent.current
|
||||
hasPermission = true
|
||||
} catch {
|
||||
// If we get an error, we don't have permission
|
||||
hasPermission = false
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
// Try to capture a small image to test permissions
|
||||
guard let mainDisplayID = CGMainDisplayID() as CGDirectDisplayID? else {
|
||||
return false
|
||||
}
|
||||
|
||||
// Test by trying to get display bounds - this requires screen recording permission
|
||||
let bounds = CGDisplayBounds(mainDisplayID)
|
||||
return bounds != CGRect.zero
|
||||
|
||||
semaphore.wait()
|
||||
return hasPermission
|
||||
}
|
||||
|
||||
static func checkAccessibilityPermission() -> Bool {
|
||||
|
|
|
|||
Loading…
Reference in a new issue