curtain/Sources/Curtain/SessionMonitor.swift
acamarata 30d5a77ffa v1.1 — settings window, modular lifecycle actions, login item, logo
Rework Curtain into a proper menu-bar app (Caffeine-style):
- SwiftUI settings window: app, on-start, on-idle, on-end, security, displays
- Modular ActionSet/ActionRunner — per-event toggles (disconnect/lock/screen-off/deactivate)
- Configurable idle timeout; open-at-login via SMAppService; optional menu bar
- Settings persisted in UserDefaults (shared with @AppStorage); salted-SHA256 password
- Drawn curtains logo (menu-bar template + offscreen-rendered app icon)
- Split into single-responsibility modules with comment blocks
2026-06-01 15:51:20 -04:00

78 lines
2.9 KiB
Swift

import Foundation
/// Purpose: Detect Screen Sharing connect / disconnect / idle and fire callbacks.
/// How: polls `netstat` for an ESTABLISHED connection on the VNC port (5900).
/// lsof does NOT work here the screensharing sockets are owned by _rmd/root
/// and are invisible to a user-context lsof (verified). Disconnect is debounced
/// (N consecutive misses) so a transient blip never kills a live session.
/// SPORT: MASTER-SESSIONMONITOR
final class SessionMonitor {
var onConnect: (() -> Void)?
var onDisconnect: (() -> Void)?
var onIdleTimeout: (() -> Void)?
private var timer: Timer?
private var connected = false
private var missCount = 0
private var idleFired = false
private let missLimit = 3 // ~6s at 2s poll
private let pollInterval: TimeInterval = 2
func start() {
connected = isVNCEstablished()
if connected { onConnect?() }
timer = Timer.scheduledTimer(withTimeInterval: pollInterval, repeats: true) { [weak self] _ in
self?.tick()
}
}
func stop() { timer?.invalidate(); timer = nil }
private func tick() {
if isVNCEstablished() {
missCount = 0
if !connected { connected = true; idleFired = false; onConnect?() }
if Settings.idleEnabled {
if !idleFired, idleSeconds() >= Settings.idleMinutes * 60 {
idleFired = true
onIdleTimeout?()
} else if idleSeconds() < Settings.idleMinutes * 60 {
idleFired = false
}
}
} else if connected {
missCount += 1
if missCount >= missLimit { connected = false; missCount = 0; onDisconnect?() }
}
}
// MARK: - Probes
private func isVNCEstablished() -> Bool {
let out = shell("/usr/bin/netstat", ["-an"])
for line in out.split(separator: "\n") {
if line.contains(".5900 ") && line.contains("ESTABLISHED") { return true }
}
return false
}
/// Seconds since the last HID (human) input event.
private func idleSeconds() -> Int {
let out = shell("/usr/sbin/ioreg", ["-c", "IOHIDSystem"])
for line in out.split(separator: "\n") where line.contains("HIDIdleTime") {
if let ns = line.split(separator: "=").last.flatMap({ Int($0.trimmingCharacters(in: .whitespaces)) }) {
return ns / 1_000_000_000
}
}
return 0
}
private func shell(_ path: String, _ args: [String]) -> String {
let p = Process(); p.launchPath = path; p.arguments = args
let pipe = Pipe(); p.standardOutput = pipe; p.standardError = Pipe()
do { try p.run() } catch { return "" }
let data = pipe.fileHandleForReading.readDataToEndOfFile()
p.waitUntilExit()
return String(data: data, encoding: .utf8) ?? ""
}
}