mirror of
https://github.com/acamarata/curtain.git
synced 2026-07-01 03:04:25 +00:00
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.
109 lines
6.1 KiB
Markdown
109 lines
6.1 KiB
Markdown
# Troubleshooting
|
|
|
|
## Emergency unlock (always works)
|
|
|
|
If you are stuck behind the curtain for any reason, press **Control + Option + Command + U** at the desk. This force-deactivates the curtain immediately. It is a Carbon hotkey, so it works even when Accessibility has not been granted. This is your guaranteed escape.
|
|
|
|
## The curtain will not appear / Curtain refuses to cover
|
|
|
|
Accessibility permission is not granted. Curtain will not put up a cover it cannot unlock, so when the permission is missing it refuses to show the cover and posts a notification instead.
|
|
|
|
1. Open System Settings → Privacy & Security → Accessibility.
|
|
2. Find **Curtain** in the list and make sure it is enabled.
|
|
3. If it is not in the list, launch Curtain from `/Applications` once to force a registration attempt.
|
|
4. After granting, relaunch Curtain so the permission takes effect.
|
|
|
|
## macOS blocks the app on first launch (Gatekeeper)
|
|
|
|
Curtain is ad-hoc signed and not yet notarized, so a clean download is quarantined. Clear the flag once, then open normally:
|
|
|
|
```bash
|
|
xattr -dr com.apple.quarantine /Applications/Curtain.app
|
|
```
|
|
|
|
This is a current limitation that goes away once a notarized build ships.
|
|
|
|
## The app will not launch
|
|
|
|
Confirm whether the process is running:
|
|
|
|
```bash
|
|
pgrep -fl Curtain
|
|
```
|
|
|
|
If nothing prints, launch Curtain from `/Applications`. If it launches and immediately exits, open Console.app and filter for `Curtain` to see the crash or exit reason. The Gatekeeper quarantine flag above is the most common cause of a silent first-launch failure.
|
|
|
|
## The curtain does not arm when Screen Sharing connects
|
|
|
|
First confirm Accessibility is granted (see above) and that Curtain is armed. The menu-bar icon is the curtains glyph; it tints red while the curtain is active. Open the menu and confirm **Armed** has a checkmark. If it does not, choose **Armed** to re-enable.
|
|
|
|
If the menu-bar icon is missing entirely, Curtain is not running. Launch it from `/Applications` and check `pgrep -fl Curtain`.
|
|
|
|
### Use the probe script to verify detection
|
|
|
|
Run the included probe while a Screen Sharing session is active:
|
|
|
|
```bash
|
|
swift Scripts/probe-detection.swift
|
|
```
|
|
|
|
It prints the live values of all three detection signals:
|
|
|
|
| Line | Meaning |
|
|
|---|---|
|
|
| `captured=true` | `CGSSessionScreenIsCaptured` is true. Curtain should activate. |
|
|
| `tcp_estab=true` | An ESTABLISHED inbound TCP connection exists on port 5900. |
|
|
| `udp_peered=true` | A peered UDP socket exists on ports 5900-5902. |
|
|
| `tcp_listen=true` | A LISTEN socket on 5900 exists. This does NOT activate the curtain. |
|
|
| `udp=true` | A UDP socket on 5900-5902 exists but is not peered. This does NOT activate. |
|
|
| `DICT …` | A key in the CGSession dictionary that appeared or changed since last poll. |
|
|
|
|
If `captured=false` and both `tcp_estab` and `udp_peered` are also false while a session is clearly running, there is a detection gap. Note the DICT output and file an issue with the output attached.
|
|
|
|
### Re-grant Accessibility after every rebuild of an ad-hoc build
|
|
|
|
Rebuilding the app from source produces a new binary with a new code signature. macOS TCC ties the Accessibility grant to the code signature, so the old grant no longer applies. After rebuilding:
|
|
|
|
1. Open System Settings → Privacy & Security → Accessibility.
|
|
2. Remove the old Curtain entry if present, then re-add it.
|
|
3. Relaunch Curtain.
|
|
|
|
## The curtain activates when no one is connected
|
|
|
|
The three activation signals are: `CGSSessionScreenIsCaptured`, an ESTABLISHED TCP connection on port 5900, and a peered UDP socket on ports 5900-5902. A lingering Screen Sharing process, an idle `:5900` LISTEN socket, or a wildcard UDP socket does **not** activate the curtain. If it arms with no session, run the probe (`swift Scripts/probe-detection.swift`) to see which signal is true, and check whether something on the machine is capturing the console screen.
|
|
|
|
## DisplayLink monitor is not covered
|
|
|
|
This is expected if the monitor has not been marked.
|
|
|
|
Open **Settings → Displays** and mark the monitor as DisplayLink. Each display is identified by a stable UUID, so the marking persists. On a DisplayLink monitor the curtain also shows in the remote view. That is by design. See [How It Works — DisplayLink](How-It-Works#displaylink) for the technical reason.
|
|
|
|
## Multiple displays: the remote view only shows one screen
|
|
|
|
The Apple Screen Sharing app shows one host display at a time. Switch between them from its **View** menu. This is normal Screen Sharing behavior, not a Curtain bug.
|
|
|
|
## The session keeps dropping
|
|
|
|
Curtain debounces disconnect detection: it waits for three consecutive missed polls (about 6 seconds) before declaring the session ended. On a stable network this never trips by accident. If sessions drop on a reliable connection, check whether another process is interfering with port 5900 or restarting Screen Sharing.
|
|
|
|
## The remote operator's mouse and keyboard stop working
|
|
|
|
This should not happen when the event tap is working correctly. The tap only blocks events with source state ID `1` (physical hardware). Remote events have a different source ID and pass through untouched.
|
|
|
|
## The Mac does not lock when the session ends
|
|
|
|
The lock uses `SACLockScreenImmediate` from login.framework. Confirm the lock screen is enabled: System Settings → Lock Screen → Require password. If the OS lock screen is disabled, there is nothing for the lock call to fall back to.
|
|
|
|
## I forgot my password
|
|
|
|
Settings live in UserDefaults. Reset everything to defaults:
|
|
|
|
```bash
|
|
defaults delete io.acamarata.curtain
|
|
```
|
|
|
|
Relaunch Curtain. It starts fresh and the default password `curtain` applies again. Set a new one from the settings window.
|
|
|
|
## The disconnect helper is not working
|
|
|
|
The optional disconnect feature installs a privileged helper and needs one admin approval. On a notarized or Developer-ID build it registers a daemon through SMAppService.daemon, approved once in System Settings. On a local ad-hoc or dev build it falls back to a small current-user-scoped privileged helper installed with one admin prompt. If disconnect actions do nothing, confirm you approved the helper, and re-run **Settings → Disconnect → Enable disconnect-remote-on-end** to reinstall it.
|