curtain/Sources/Curtain/System.swift
Aric Camarata 8c19e960d2 Detection root-cause fix + audit batch: netstat path, UDP activator, settings coherence, refactor, docs
Detection: netstat lives at /usr/sbin/netstat, not /usr/bin — the hardcoded wrong
path silently killed the ESTABLISHED-TCP activator (root cause of the failed live
test). Fixed and live-verified. Added peered-UDP activator (5900-5902) for
High-Performance sessions, per-signal transition logging, unconditional error
logging for dead probe helpers, and probe v2 with full CGSession dictionary
diffing. 7 new parser tests (32 total).

Fixes from a full audit + adversarial review: idle source setting honored
(default now Remote session activity), cover scope reduced to a coherent
two-mode model with legacy migration (per-display toggle was inverted in
onlyMarked and dead in all), curtain test no longer schedules a teardown over a
live session, specific-display password box placement gets a real picker,
refuse-to-arm enforced, activation notification posts a real banner, menu
password gate bypassed when the event tap is dead, shared single-decoder aerial
player with stale-task guard and async playability check, password buffer zeroed
on successful unlock and Esc, XPC interruption/invalidation handlers, modern
Accessibility settings URL, launchPath modernized, codesign failures now abort
release.sh, monotonic CFBundleVersion, install.sh temp cleanup, dead
armDisarmHotkey setting removed.

Refactor: Curtain.swift and PreferencesWindow.swift split into focused files
(largest now 479 lines). Wiki, README, and contributing docs updated to match.
Build clean at 0 warnings, 32/32 tests pass.
2026-06-09 20:36:30 -04:00

133 lines
5.5 KiB
Swift

import Cocoa
import IOKit.pwr_mgt
/// Purpose: Thin wrappers over the macOS system actions Curtain needs.
/// Constraints: every call here was validated against macOS 26 (Sequoia-era).
/// SPORT: MASTER-SYSTEM
enum System {
// MARK: - Reliable screen lock
//
// CGSession -suspend was removed in recent macOS. osascript Ctrl+Cmd+Q needs
// Accessibility and is unreliable from a launchd agent. SACLockScreenImmediate
// (private login.framework symbol) locks immediately with no extra permission.
private static let loginPaths = [
"/System/Library/PrivateFrameworks/login.framework/Versions/Current/login",
"/System/Library/PrivateFrameworks/login.framework/login"
]
/// Resolve the private lock symbol without calling it. Returns the function
/// pointer if found, nil otherwise. Both callers use this so probing and
/// locking stay in sync.
private static func resolveLockFn() -> (@convention(c) () -> Int32)? {
typealias LockFn = @convention(c) () -> Int32
for p in loginPaths {
if let h = dlopen(p, RTLD_LAZY), let sym = dlsym(h, "SACLockScreenImmediate") {
return unsafeBitCast(sym, to: LockFn.self)
}
}
return nil
}
/// Call once at launch to surface a clear warning if the fast lock path is
/// missing on this OS build, before the user ever relies on it.
static func startupLockProbe() {
if resolveLockFn() == nil {
NSLog("Curtain: SACLockScreenImmediate unavailable — lock will fall back to osascript")
}
}
static func lockScreen() {
if let lock = resolveLockFn() {
_ = lock()
return
}
NSLog("Curtain: SACLockScreenImmediate could not be resolved on either login.framework path — falling back to osascript")
// Fallback (needs Accessibility): the lock-screen shortcut.
let t = Process()
t.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
t.arguments = ["-e", "tell application \"System Events\" to keystroke \"q\" using {command down, control down}"]
try? t.run()
}
/// Put all displays to sleep (after a lock = a dark, locked Mac).
/// Runs off the main thread so a slow exec never stalls the UI.
static func sleepDisplays() {
DispatchQueue.global(qos: .userInitiated).async {
let t = Process()
t.executableURL = URL(fileURLWithPath: "/usr/bin/pmset")
t.arguments = ["displaysleepnow"]
try? t.run()
}
}
// MARK: - Prevent display sleep during a session
nonisolated(unsafe) private static var assertionID: IOPMAssertionID = 0
nonisolated(unsafe) private static var assertionActive = false
static func preventDisplaySleep() {
guard !assertionActive else { return }
let ok = IOPMAssertionCreateWithName(
kIOPMAssertionTypeNoDisplaySleep as CFString,
IOPMAssertionLevel(kIOPMAssertionLevelOn),
"Curtain active" as CFString,
&assertionID)
assertionActive = (ok == kIOReturnSuccess)
}
static func allowDisplaySleep() {
if assertionActive { IOPMAssertionRelease(assertionID); assertionActive = false }
}
// MARK: - End the active Screen Sharing session
//
// Killing the connection processes needs root. The disconnect feature is an
// optional privileged daemon installed separately (SMAppService.daemon). The
// daemon client sets disconnectHandler at launch; if nothing sets it, the
// disconnect is simply a no-op with a logged note. No sudo, no blocking.
/// Set by the daemon client when the privileged disconnect helper is enabled.
/// Invoked on a background queue so it never touches the main thread.
nonisolated(unsafe) static var disconnectHandler: (() -> Void)?
static func endScreenShareSession() {
if let handler = disconnectHandler {
DispatchQueue.global(qos: .userInitiated).async { handler() }
return
}
NSLog("Curtain: disconnect requested but the remote-disconnect helper is not enabled")
}
// MARK: - Displays
static func serial(of screen: NSScreen) -> UInt32 {
let id = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as! CGDirectDisplayID
return CGDisplaySerialNumber(id)
}
/// Stable per-display UUID. Survives reboots and port changes better than the
/// serial, and unlike the serial it is unique even when EDID passthrough makes
/// vendor IDs identical. Returns nil if the display can't be resolved.
static func uuid(of screen: NSScreen) -> String? {
guard let num = screen.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID else {
return nil
}
guard let cfUUID = CGDisplayCreateUUIDFromDisplayID(num)?.takeRetainedValue() else {
return nil
}
return CFUUIDCreateString(nil, cfUUID) as String
}
/// A native display can be hidden invisibly (sharingType .none). A DisplayLink
/// display only exists via screen capture, so .none hides it from the capture
/// too it must use .readOnly (visible in the remote view). We match by UUID
/// now, falling back to the legacy serial list so older configs keep working.
static func isDisplayLink(_ screen: NSScreen) -> Bool {
if let id = uuid(of: screen) {
return Settings.displayLinkUUIDs.contains(id)
}
return Settings.legacyDisplayLinkSerials.contains(serial(of: screen))
}
}