Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
included:
- MenuBarCaptureService
- MenuBarItemService
- Shared
- Thaw
Expand Down
214 changes: 214 additions & 0 deletions MenuBarCaptureService/Listener.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
//
// Listener.swift
// Project: Thaw
//
// Copyright (Ice) © 2023–2025 Jordan Baird
// Copyright (Thaw) © 2026 Toni Förster
// Licensed under the GNU GPLv3

import CoreGraphics
import Foundation
import XPC

/// XPC listener for offscreen SkyLight capture.
///
/// Explicitly nonisolated: XPC callbacks arrive on arbitrary threads, and the
/// target's default actor isolation is MainActor.
final nonisolated class Listener: @unchecked Sendable {
private let diagLog = DiagLog(category: "CaptureListener")
static let shared = Listener()

private let name = MenuBarCaptureService.name
private var xpcListener: XPCListener?
private let captureLock = NSLock()
private let instanceID = UInt64.random(in: .min ... .max)
private var captureCount = 0

private init() {}

deinit {
cancel()
}

private func handleMessage(_ message: XPCReceivedMessage) -> MenuBarCaptureService.Response? {
do {
let request = try message.decode(as: MenuBarCaptureService.Request.self)
switch request {
case .start:
return .start
case let .configureLogging(filePath):
let requested = URL(fileURLWithPath: filePath)
.standardizedFileURL.resolvingSymlinksInPath()
let approvedDir = DiagnosticLogger.shared.logDirectory
.standardizedFileURL.resolvingSymlinksInPath()
guard requested.path.hasPrefix(approvedDir.path + "/") else {
diagLog.error(
"Capture listener rejected configureLogging path outside approved log directory: \(filePath)"
)
return nil
}
DiagnosticLogger.shared.attachToFile(at: requested)
return .configureLogging
case let .captureBatch(batch):
return capture(batch)
case .recycle:
scheduleExit()
return .recycle
}
} catch {
diagLog.error("Capture listener failed to handle message with error \(error)")
return nil
}
}

private func capture(_ request: MenuBarCaptureService.CaptureBatchRequest) -> MenuBarCaptureService.Response {
captureLock.lock()
defer { captureLock.unlock() }

let frames = autoreleasepool { () -> [MenuBarCaptureService.Frame] in
captureFrames(request)
}
if MenuBarCaptureService.shouldRecycle(captureCount: captureCount) {
scheduleExit()
}
return .captureBatch(
MenuBarCaptureService.CaptureBatchResponse(
requestID: request.requestID,
instanceID: instanceID,
frames: frames
)
)
}

private func captureFrames(
_ request: MenuBarCaptureService.CaptureBatchRequest
) -> [MenuBarCaptureService.Frame] {
let scale = CGFloat(request.expectedScale)
guard scale > 0, scale.isFinite else { return [] }

let allowed = Set(Bridging.getMenuBarWindowList(option: .itemsOnly))
let windowIDs = MenuBarCaptureService.validatedWindowIDs(request.windowIDs, allowed: allowed)
guard !windowIDs.isEmpty else { return [] }

var storage = [CGWindowID: CGRect]()
var orderedIDs = [CGWindowID]()
var boundsUnion = CGRect.null
for windowID in windowIDs {
guard let bounds = Bridging.getWindowBounds(for: windowID) else { continue }
guard Bridging.isValidCaptureBounds(bounds, scale: scale) else { continue }
storage[windowID] = bounds
orderedIDs.append(windowID)
boundsUnion = boundsUnion.union(bounds)
}
guard !orderedIDs.isEmpty,
Bridging.isValidCaptureBounds(boundsUnion, scale: scale)
else {
return []
}

let options = CGWindowImageOption(rawValue: request.optionRawValue)
let composite = Bridging.captureWindowsImage(
windowIDs: orderedIDs,
options: options
)
guard let composite else {
diagLog.debug("captureFrames: SkyLight returned nil for \(orderedIDs.count) windows")
return []
}
captureCount += 1

let expectedWidth = boundsUnion.width * scale
guard abs(CGFloat(composite.width) - expectedWidth) < 1 else {
diagLog.debug(
"captureFrames: width mismatch (expected \(expectedWidth), got \(composite.width))"
)
return []
}

if let encoded = MenuBarCaptureService.encodeBGRA(composite),
MenuBarCaptureService.isFullyTransparentBGRA(
pixels: encoded.pixels,
width: composite.width,
height: composite.height,
bytesPerRow: encoded.bytesPerRow
)
{
return []
}

var frames = [MenuBarCaptureService.Frame]()
var batchBytes = 0
for windowID in orderedIDs {
guard let bounds = storage[windowID] else { continue }
let cropRect = CGRect(
x: (bounds.origin.x - boundsUnion.origin.x) * scale,
y: (bounds.origin.y - boundsUnion.origin.y) * scale,
width: bounds.width * scale,
height: bounds.height * scale
)
guard let cropped = composite.cropping(to: cropRect),
let encoded = MenuBarCaptureService.encodeBGRA(cropped)
else {
continue
}
batchBytes += encoded.pixels.count
guard batchBytes <= 16 * 1_024 * 1_024 else { break }
frames.append(
MenuBarCaptureService.Frame(
windowID: windowID,
width: cropped.width,
height: cropped.height,
bytesPerRow: encoded.bytesPerRow,
scale: request.expectedScale,
pixels: encoded.pixels
)
)
}
return frames
}

private func scheduleExit() {
DispatchQueue.main.async {
exit(0)
}
}

private func uncheckedActivateWithSameTeamRequirement() throws {
xpcListener = try XPCListener(service: name, requirement: .isFromSameTeam()) { request in
request.accept { [self] message in
self.handleMessage(message)
}
}
}

private func uncheckedActivateWithoutPeerRequirement() throws {
xpcListener = try XPCListener(service: name) { request in
request.accept { [self] message in
self.handleMessage(message)
}
}
diagLog.warning(
"Capture listener is active WITHOUT peer validation (ad-hoc/teamless build): any local process may connect"
)
}

func activate() {
guard xpcListener == nil else {
diagLog.notice("Capture listener is already active")
return
}
do {
if CodeSigningInfo.processTeamIdentifier == nil {
try uncheckedActivateWithoutPeerRequirement()
} else {
try uncheckedActivateWithSameTeamRequirement()
}
} catch {
diagLog.error("Failed to activate capture listener with error \(error)")
}
}

func cancel() {
xpcListener.take()?.cancel()
}
}
17 changes: 17 additions & 0 deletions MenuBarCaptureService/Resources/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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>ThawSkyLightFrameworkPath</key>
<string>/System/Library/PrivateFrameworks/SkyLight.framework/SkyLight</string>
<key>XPCService</key>
<dict>
<key>JoinExistingSession</key>
<true/>
<key>RunLoopType</key>
<string>NSRunLoop</string>
<key>ServiceType</key>
<string>Application</string>
</dict>
</dict>
</plist>
17 changes: 17 additions & 0 deletions MenuBarCaptureService/main.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// main.swift
// Project: Thaw
//
// Copyright (Ice) © 2023–2025 Jordan Baird
// Copyright (Thaw) © 2026 Toni Förster
// Licensed under the GNU GPLv3

import Foundation

Listener.shared.activate()

while true {
autoreleasepool {
_ = RunLoop.current.run(mode: .default, before: Date(timeIntervalSinceNow: 60))
}
}
Loading