curtain/Sources/Curtain/main.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

38 lines
1.6 KiB
Swift

import Cocoa
// Curtain a privacy curtain for macOS Screen Sharing.
// Normally launches as a background menu-bar agent (no Dock icon).
//
// Hidden build helper: `Curtain --render-icon <dir>` writes an .iconset of PNGs so
// install.sh can build AppIcon.icns without shipping any image assets.
if CommandLine.arguments.contains("--render-icon"),
let dirIndex = CommandLine.arguments.firstIndex(of: "--render-icon"),
CommandLine.arguments.indices.contains(dirIndex + 1) {
CurtainIcon.exportIconset(to: CommandLine.arguments[dirIndex + 1]) // offscreen bitmap render
exit(0)
}
// The AppKit bootstrap is main-actor work; top-level code is nonisolated under Swift 6,
// so run it inside an assumeIsolated block (process start is already on the main thread).
// app.run() blocks here and never returns, so `delegate` and the signal source stay
// retained on this stack. NSApplication.delegate is weak, so `delegate` must be held.
MainActor.assumeIsolated {
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.accessory) // background agent; settings window still shows
// SIGTERM (launchd stop / `kill`): a C signal handler can't safely touch AppKit, so
// ignore the default action and route the signal through a DispatchSource on the main
// queue, where it can run cleanup before exiting.
signal(SIGTERM, SIG_IGN)
let sigtermSource = DispatchSource.makeSignalSource(signal: SIGTERM, queue: .main)
sigtermSource.setEventHandler {
MainActor.assumeIsolated { delegate.cleanup() }
exit(0)
}
sigtermSource.resume()
app.run()
}