From 83c76cb26aae87cb3b3a6faf56a768e112d297bb Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Wed, 16 Sep 2026 08:24:29 -0700 Subject: [PATCH] slab: a game mode that clears the way for GeForce NOW MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GeForce NOW refuses to stream when it decides you are on a VPN, and it never inspects routing: a visible utun carrying a 100.64/10 address, plus a DNS resolver that is not the gateway, is the whole tell. Tailscale trips both with no exit node set, where not one streamed packet enters the tunnel. Separately, AirDrop's awdl0 radio hops the Wi-Fi card off-channel mid-match — measured on blueberry, the difference between avg 18 ms / max 80 ms and avg 3.5 / max 18 on the gateway hop. Two unrelated faults, one moment you hit them. So Fleet & System gains a single switch that pauses the tailnet, pins awdl0 down and makes AirDrop undiscoverable, and puts all three back on the way out — discoverability to the setting it found, not a guess. The submenu reports what the machine actually is rather than what the switch was told, including the exact tell GFN reads, and it tells a radio that is merely idle apart from one Slab is holding: macOS parks awdl0 on its own and raises it again unasked. Auto-detect engages on GeForce NOW's launch and stands down when it quits; a hold set by hand outlives the app that happened to be open. Holding the radio needs root, so a small LaunchDaemon watches a flag file this app owns — one admin prompt ever, and until it is installed the mode does the tailnet half alone, which is what keeps an unattended engage from raising a dialog over a running game. Co-Authored-By: Claude Opus 5 (1M context) --- slab/README.md | 6 + .../Sources/SlabMenubar/AppDelegate.swift | 47 +++ .../Sources/SlabMenubar/GameMode.swift | 313 ++++++++++++++++++ .../Sources/SlabMenubar/MenuBuilder.swift | 68 ++++ .../Sources/SlabMenubar/Paths.swift | 10 + .../Sources/SlabMenubar/StateSnapshot.swift | 17 + toolchain/macos/gfn.sh | 12 +- 7 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 slab/menubar-swift/Sources/SlabMenubar/GameMode.swift diff --git a/slab/README.md b/slab/README.md index 521416a956..b7c5afe78f 100644 --- a/slab/README.md +++ b/slab/README.md @@ -21,6 +21,12 @@ What the menu bar does today: rocks, ⌃⌥⌘A flips dark mode fleet-wide. - **Status sounds.** Three synthesized cues (complete / awaiting / disconnected) that share one voice and differ only in contour. +- **Game Mode.** GeForce NOW calls Tailscale a VPN on sight of the tunnel + interface, even with no exit node routing a single packet. One switch + pauses the tailnet and pins AirDrop's `awdl0` radio down (it channel-hops + the Wi-Fi card mid-stream), and puts both back on the way out — optionally + by itself, the moment GeForce NOW opens. + (`menubar-swift/Sources/SlabMenubar/GameMode.swift`) - **Fleet.** Every Mac runs the same menu bar; they share a ledger of live sessions over the tailnet (`host:name`), `frame` photographs any desk and `puppet` acts on it. Rocks are never drawn for remote machines. diff --git a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift index d551ee5943..77dc630cca 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift @@ -399,6 +399,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { // requested, so no Screen Recording prompt at launch (lazy grant). FrameCapture.shared.start() + // Game Mode watches for GeForce NOW launching/quitting. Opt-in: with + // "Auto" off this observes and does nothing, so the wiring is the + // same either way and costs a couple of workspace notifications. + GameMode.shared.startWatchingGFN() + // `reel` — the moving-picture sibling of frame. Same lazy-grant file // watcher; hardware-encodes an SCStream straight to mp4 when asked. if #available(macOS 15.0, *) { ScreenRecord.shared.start() } @@ -1686,6 +1691,48 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate { } } + // ── Game Mode ──────────────────────────────────────────────────────── + // A clean GeForce NOW client is two unrelated fixes applied together: + // pause Tailscale (GFN calls any visible tunnel a VPN) and hold AirDrop's + // awdl0 radio down (it hops the Wi-Fi card off-channel mid-stream). See + // GameMode.swift for the measurements and the root-helper handshake. + + @objc func toggleGameMode() { + GameMode.shared.toggle() + // Tailscale's own up/down takes a beat to settle; refresh once now so + // the menu is honest immediately, and again once the tunnel is gone. + refresh() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in self?.refresh() } + } + + @objc func toggleGameModeAuto() { + GameMode.shared.setAutoDetect(!state.gameMode.autoDetect) + refresh() + DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in self?.refresh() } + } + + @objc func installGameModeHelper() { + if let error = GameMode.shared.installHelper() { + presentGameModeError("Could not install the radio helper", error) + } + refresh() + } + + @objc func removeGameModeHelper() { + if let error = GameMode.shared.removeHelper() { + presentGameModeError("Could not remove the radio helper", error) + } + refresh() + } + + private func presentGameModeError(_ title: String, _ detail: String) { + let alert = NSAlert() + alert.messageText = title + alert.informativeText = detail + alert.alertStyle = .warning + alert.runModal() + } + @objc func sleepNow() { ShellRunner.runAsync(Paths.claudeSleep, args: ["now"]) } diff --git a/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift b/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift new file mode 100644 index 0000000000..158fc45ed8 --- /dev/null +++ b/slab/menubar-swift/Sources/SlabMenubar/GameMode.swift @@ -0,0 +1,313 @@ +import AppKit + +/// Game Mode — one switch that turns this Mac into a clean GeForce NOW client. +/// +/// GFN refuses to stream when it decides you're on a VPN, and it does not +/// inspect routing: a live `utun` carrying a 100.64/10 address, plus a DNS +/// resolver that isn't the gateway, is enough. Tailscale trips both tells +/// even with no exit node set, where not one streamed packet enters the +/// tunnel. Separately, AirDrop's `awdl0` radio hops the Wi-Fi card +/// off-channel every few seconds; measured on blueberry 2026-09-02 that is +/// the difference between avg 18 ms / max 80 ms and avg 3.5 / max 18 on the +/// gateway hop. +/// +/// Both faults are network-adjacent and neither is the other's cause, so the +/// mode does both at once and undoes both on the way out: +/// +/// on → `tailscale down` + awdl0 pinned down + AirDrop undiscoverable +/// off → `tailscale up` + awdl0 released + AirDrop restored +/// +/// Nothing else about the machine changes, and the tailnet comes back with +/// its peers intact — `down`/`up` is Tailscale's own pause, not a logout. +/// +/// Privilege: `ifconfig awdl0 down` is root-only, and macOS re-raises awdl0 +/// whenever Continuity is touched, so the holding is done by a small root +/// LaunchDaemon that watches a flag file this app owns. That costs exactly +/// one admin prompt, ever; every toggle afterwards is a file write. The +/// tailnet half needs no privilege at all, which is what lets auto-detect +/// engage unattended without a dialog ever appearing behind a running game. +final class GameMode { + static let shared = GameMode() + + static let helperLabel = "computer.aesthetic.slab.gamemode" + static var helperPlist: String { "/Library/LaunchDaemons/\(helperLabel).plist" } + /// `toolchain/macos/gfn.sh on` installs its own unconditional awdl-down + /// daemon. Two holders would fight over the same interface, so installing + /// ours evicts that one. + static let legacyLabel = "computer.aesthetic.gfn-awdl" + + static let gfnBundleID = "com.nvidia.gfnpc.mall" + + /// True when this mode engaged itself because GeForce NOW launched. Only + /// an automatic engage may auto-disengage: a hold you switched on by hand + /// outlives the app that happened to be open. + private var engagedAutomatically = false + private var watching = false + + // ── state ──────────────────────────────────────────────────────────── + + /// Read entirely from flag files and `getifaddrs` — no forks, so + /// `StateSnapshot.gather()` can call it every tick for free. + static func state() -> GameModeState { + var s = GameModeState() + let fm = FileManager.default + s.on = fm.fileExists(atPath: Paths.gameModeFlag) + s.autoDetect = fm.fileExists(atPath: Paths.gameModeAutoFlag) + s.helperInstalled = fm.fileExists(atPath: helperPlist) + let (awdlUp, tunnelVisible) = interfaces() + s.awdlUp = awdlUp + s.tailnetVisible = tunnelVisible + s.gfnRunning = !NSRunningApplication + .runningApplications(withBundleIdentifier: gfnBundleID).isEmpty + return s + } + + /// (awdl0 is up, a utun carries a 100.64/10 address). The second value is + /// precisely what GFN's VPN heuristic keys on, so the menu reports the + /// same fact the streamer is reading rather than a proxy for it. + private static func interfaces() -> (awdlUp: Bool, tunnelVisible: Bool) { + var head: UnsafeMutablePointer? + guard getifaddrs(&head) == 0, let first = head else { return (false, false) } + defer { freeifaddrs(head) } + + var awdlUp = false + var tunnel = false + for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) { + let name = String(cString: ptr.pointee.ifa_name) + if name == "awdl0", ptr.pointee.ifa_flags & UInt32(IFF_UP) != 0 { awdlUp = true } + guard name.hasPrefix("utun"), + let sa = ptr.pointee.ifa_addr, + sa.pointee.sa_family == UInt8(AF_INET) else { continue } + var addr = sockaddr_in() + memcpy(&addr, sa, MemoryLayout.size) + let ip = UInt32(bigEndian: addr.sin_addr.s_addr) + if ip & 0xFFC0_0000 == 0x6440_0000 { tunnel = true } // 100.64.0.0/10 + } + return (awdlUp, tunnel) + } + + // ── the switch ─────────────────────────────────────────────────────── + + func toggle() { + if FileManager.default.fileExists(atPath: Paths.gameModeFlag) { + disengage() + } else { + engage(automatic: false) + } + } + + /// Engage. `automatic` engages only what can be done silently, so a game + /// launching never raises an auth dialog over the stream; the awdl half + /// simply stays off until the helper is installed from the menu. + func engage(automatic: Bool) { + engagedAutomatically = automatic + try? FileManager.default.createDirectory( + atPath: (Paths.gameModeFlag as NSString).deletingLastPathComponent, + withIntermediateDirectories: true) + FileManager.default.createFile(atPath: Paths.gameModeFlag, contents: nil) + + stashAirDropMode() + setAirDrop("Off") + tailscale("down") + // The helper polls, but a direct nudge drops the radio now rather + // than up to one poll interval into the first match. + if FileManager.default.fileExists(atPath: GameMode.helperPlist) { pokeHelper() } + } + + func disengage() { + engagedAutomatically = false + try? FileManager.default.removeItem(atPath: Paths.gameModeFlag) + tailscale("up") + setAirDrop(stashedAirDropMode() ?? "Contacts Only") + try? FileManager.default.removeItem(atPath: Paths.airDropStash) + if FileManager.default.fileExists(atPath: GameMode.helperPlist) { pokeHelper() } + } + + private func tailscale(_ verb: String) { + guard let ts = Tools.resolve("tailscale") else { return } + DispatchQueue.global(qos: .userInitiated).async { + ShellRunner.run(ts, args: [verb], timeout: 20) + } + } + + /// The helper wakes on its own poll; this only shortens the wait by + /// touching the flag's directory so a watching `stat` sees it sooner. + private func pokeHelper() { + try? FileManager.default.setAttributes( + [.modificationDate: Date()], + ofItemAtPath: (Paths.gameModeFlag as NSString).deletingLastPathComponent) + } + + // AirDrop discoverability is saved and put back rather than forced to + // "Everyone", because most Macs sit on "Contacts Only" and silently + // widening that is not this feature's business. + private func stashAirDropMode() { + guard !FileManager.default.fileExists(atPath: Paths.airDropStash) else { return } + let current = ShellRunner.output( + "/usr/bin/defaults", + args: ["read", "com.apple.sharingd", "DiscoverableMode"], timeout: 5 + )?.trimmingCharacters(in: .whitespacesAndNewlines) + guard let current, !current.isEmpty, current != "Off" else { return } + try? current.write(toFile: Paths.airDropStash, atomically: true, encoding: .utf8) + } + + private func stashedAirDropMode() -> String? { + guard let raw = try? String(contentsOfFile: Paths.airDropStash, encoding: .utf8) else { return nil } + let value = raw.trimmingCharacters(in: .whitespacesAndNewlines) + return value.isEmpty ? nil : value + } + + private func setAirDrop(_ mode: String) { + DispatchQueue.global(qos: .utility).async { + ShellRunner.run("/usr/bin/defaults", + args: ["write", "com.apple.sharingd", "DiscoverableMode", "-string", mode], + timeout: 5) + ShellRunner.run("/usr/bin/killall", args: ["sharingd"], timeout: 5) + } + } + + // ── auto-detect ────────────────────────────────────────────────────── + + /// Watch for GeForce NOW coming and going. Opt-in: with the preference + /// off this observes and does nothing, so the notifications stay cheap + /// and the wiring is identical either way. + func startWatchingGFN() { + guard !watching else { return } + watching = true + let center = NSWorkspace.shared.notificationCenter + center.addObserver(self, selector: #selector(appLaunched(_:)), + name: NSWorkspace.didLaunchApplicationNotification, object: nil) + center.addObserver(self, selector: #selector(appTerminated(_:)), + name: NSWorkspace.didTerminateApplicationNotification, object: nil) + + // Slab restarts far more often than a game session lasts; adopt a GFN + // that is already up rather than waiting for the next launch. + let s = GameMode.state() + if s.autoDetect, s.gfnRunning, !s.on { engage(automatic: true) } + } + + @objc private func appLaunched(_ note: Notification) { + guard bundleID(note) == GameMode.gfnBundleID else { return } + let s = GameMode.state() + guard s.autoDetect, !s.on else { return } + engage(automatic: true) + } + + @objc private func appTerminated(_ note: Notification) { + guard bundleID(note) == GameMode.gfnBundleID else { return } + guard engagedAutomatically else { return } + disengage() + } + + private func bundleID(_ note: Notification) -> String? { + (note.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication)? + .bundleIdentifier + } + + func setAutoDetect(_ enabled: Bool) { + if enabled { + try? FileManager.default.createDirectory( + atPath: (Paths.gameModeAutoFlag as NSString).deletingLastPathComponent, + withIntermediateDirectories: true) + FileManager.default.createFile(atPath: Paths.gameModeAutoFlag, contents: nil) + // Turning it on while the game is already open should act now, + // not at the next launch. + let s = GameMode.state() + if s.gfnRunning, !s.on { engage(automatic: true) } + } else { + try? FileManager.default.removeItem(atPath: Paths.gameModeAutoFlag) + } + } + + // ── the root helper ────────────────────────────────────────────────── + + /// The daemon body. Holding awdl0 down has to be a loop rather than a + /// one-shot because macOS re-raises the interface whenever anything + /// touches Continuity; releasing is edge-triggered so the interface is + /// brought back exactly once, not re-upped every poll against a user who + /// turned AirDrop off for their own reasons. + private static func helperScript(flag: String) -> String { + """ + held=0 + while true; do + if [ -f '\(flag)' ]; then + /sbin/ifconfig awdl0 down 2>/dev/null + held=1 + elif [ "$held" = 1 ]; then + /sbin/ifconfig awdl0 up 2>/dev/null + held=0 + fi + sleep 5 + done + """ + } + + private static func helperPlistXML(flag: String) -> String { + """ + + + + Label\(helperLabel) + ProgramArguments + /bin/bash-c + \(helperScript(flag: flag).replacingOccurrences(of: "&", with: "&") + .replacingOccurrences(of: "<", with: "<") + .replacingOccurrences(of: ">", with: ">")) + + RunAtLoad + KeepAlive + + """ + } + + /// One admin prompt, once. Returns an error string on failure, nil on + /// success. Must run on the main thread — NSAppleScript puts up the + /// authorization dialog itself. + @discardableResult + func installHelper() -> String? { + let staged = NSTemporaryDirectory() + "\(GameMode.helperLabel).plist" + do { + try GameMode.helperPlistXML(flag: Paths.gameModeFlag) + .write(toFile: staged, atomically: true, encoding: .utf8) + } catch { + return "could not stage the helper: \(error.localizedDescription)" + } + // Deliberately quote-free so it survives the AppleScript string layer + // intact; every path here is a fixed location with no spaces. + let cmd = [ + "/bin/cp \(staged) \(GameMode.helperPlist)", + "/usr/sbin/chown root:wheel \(GameMode.helperPlist)", + "/bin/chmod 644 \(GameMode.helperPlist)", + "/bin/launchctl bootout system/\(GameMode.legacyLabel) 2>/dev/null", + "/bin/rm -f /Library/LaunchDaemons/\(GameMode.legacyLabel).plist", + "/bin/launchctl bootout system/\(GameMode.helperLabel) 2>/dev/null", + "/bin/launchctl bootstrap system \(GameMode.helperPlist)", + ].joined(separator: "; ") + return GameMode.runAsAdmin( + cmd, prompt: "Slab Game Mode needs to install a small helper so it can hold the AirDrop radio down while you stream.") + } + + @discardableResult + func removeHelper() -> String? { + let cmd = [ + "/bin/launchctl bootout system/\(GameMode.helperLabel) 2>/dev/null", + "/bin/rm -f \(GameMode.helperPlist)", + "/sbin/ifconfig awdl0 up 2>/dev/null", + ].joined(separator: "; ") + return GameMode.runAsAdmin(cmd, prompt: "Slab Game Mode is removing its radio helper.") + } + + private static func runAsAdmin(_ command: String, prompt: String) -> String? { + let escaped = command + .replacingOccurrences(of: "\\", with: "\\\\") + .replacingOccurrences(of: "\"", with: "\\\"") + let source = "do shell script \"\(escaped)\" with administrator privileges with prompt \"\(prompt)\"" + var err: NSDictionary? + NSAppleScript(source: source)?.executeAndReturnError(&err) + guard let err else { return nil } + // -128 is the user clicking Cancel on the auth dialog: a decision, + // not a failure worth reporting back as one. + if (err[NSAppleScript.errorNumber] as? Int) == -128 { return nil } + return (err[NSAppleScript.errorMessage] as? String) ?? "authorization failed" + } +} diff --git a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift index 808bbb5090..d89ec92603 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift @@ -89,6 +89,7 @@ enum MenuBuilder { let fleet = NSMenu() fleet.addItem(buildSystem(state: state, target: target)) fleet.addItem(buildTailnet(state: state, target: target)) + fleet.addItem(buildGameMode(state: state, target: target)) if state.deskflow.configured { fleet.addItem(buildDeskflow(state: state, target: target)) } @@ -512,6 +513,73 @@ enum MenuBuilder { return parent } + /// "Game Mode" — the GeForce NOW switch. GFN calls Tailscale a VPN on + /// sight of the tunnel interface even with no exit node, and AirDrop's + /// awdl0 radio hops the Wi-Fi card off-channel mid-stream; this drops + /// both together and puts both back. The rows below the switch report + /// the machine's real state, including the exact tell GFN reads. + private static func buildGameMode(state: StateSnapshot, target: AppDelegate) -> NSMenuItem { + let g = state.gameMode + let title: String + if g.on { + var held = ["tailnet down"] + if g.helperInstalled && !g.awdlUp { held.append("awdl held") } + title = "Game Mode: on · " + held.joined(separator: " · ") + } else { + title = g.gfnRunning ? "Game Mode: off · GFN open" : "Game Mode: off" + } + let parent = NSMenuItem(title: title, action: nil, keyEquivalent: "") + let sub = NSMenu() + sub.autoenablesItems = false + + let toggle = item(g.on ? "Leave game mode" : "Enter game mode", + selector: #selector(AppDelegate.toggleGameMode), target: target) + toggle.state = g.on ? .on : .off + toggle.toolTip = "Pause Tailscale and pin the AirDrop radio down for a clean stream, then restore both on the way out." + sub.addItem(toggle) + + let auto = item("Auto when GeForce NOW opens", + selector: #selector(AppDelegate.toggleGameModeAuto), target: target) + auto.state = g.autoDetect ? .on : .off + auto.toolTip = "Engage on GeForce NOW's launch and stand down when it quits. A hold you switched on by hand is left alone." + sub.addItem(auto) + + sub.addItem(.separator()) + sub.addItem(info(g.tailnetVisible + ? "Tailnet: up — GFN reads this as a VPN" + : "Tailnet: paused — nothing for GFN to see")) + // Down and held are different states: macOS parks awdl0 whenever + // nothing is using Continuity, and it raises it again unprompted. + // Only the helper plus an engaged mode keeps it down through a match. + let radio: String + if g.awdlUp { + radio = "up — channel-hopping" + } else if g.on && g.helperInstalled { + radio = "held down by Slab" + } else { + radio = "down (idle — macOS may raise it any time)" + } + sub.addItem(info("AirDrop radio: " + radio)) + sub.addItem(info("GeForce NOW: " + (g.gfnRunning ? "running" : "not running"))) + + sub.addItem(.separator()) + if g.helperInstalled { + let remove = item("Remove radio helper…", + selector: #selector(AppDelegate.removeGameModeHelper), target: target) + remove.toolTip = "Uninstall the root LaunchDaemon and release awdl0. Game mode keeps working; it just stops touching the radio." + sub.addItem(remove) + } else { + let install = item("Install radio helper… (one admin prompt)", + selector: #selector(AppDelegate.installGameModeHelper), target: target) + install.toolTip = "Holding awdl0 down needs root, and macOS re-raises it constantly. Installs a small daemon once; every toggle after this is free." + sub.addItem(install) + sub.addItem(info("Without it, game mode pauses the tailnet only")) + } + + parent.submenu = sub + return parent + } + /// List Claude/Codex sessions inside the Agents section. Each row is /// colored with the same palette as its icon /// edge — cyan for working, red for awaiting, gray for stale — so the diff --git a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift index fe0c39698e..2a5dcf8442 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/Paths.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/Paths.swift @@ -208,6 +208,16 @@ enum Paths { /// independent of the ambient mute, which silences the hooks' TTS/pad /// layer; this marker exists only once turned off from the menu. static var statusSoundsDisabledFlag: String { "\(slabHome)/state/status-sounds-off" } + + /// Game Mode — the GeForce NOW switch (GameMode.swift). Presence of the + /// flag means engaged; the root awdl helper reads this exact path, so it + /// is the handshake between the menubar and the daemon as well as the + /// menu's own state. The auto flag is opt-in, absent by default. + static var gameModeFlag: String { "\(slabHome)/state/game-mode" } + static var gameModeAutoFlag: String { "\(slabHome)/state/game-mode-auto" } + /// AirDrop discoverability as it was before game mode turned it off, + /// so leaving the mode restores the user's setting instead of a guess. + static var airDropStash: String { "\(slabHome)/state/game-mode-airdrop" } } enum Tools { diff --git a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift index d51f19f23e..f9705afc03 100644 --- a/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift +++ b/slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift @@ -124,6 +124,20 @@ struct DeployStatusState { var url: String = "" } +/// Game Mode's live picture, all of it cheap to read (flag files + +/// getifaddrs), so the menu can show what GFN itself is seeing. +struct GameModeState { + var on = false // the flag file — mode engaged + var autoDetect = false // engage by itself when GeForce NOW opens + var helperInstalled = false // root awdl daemon present + /// awdl0 is up. macOS idles this interface down on its own, so "down" + /// alone never means Slab is holding it — only the helper plus an + /// engaged mode does, which is why the menu reports the two apart. + var awdlUp = false + var tailnetVisible = true // a utun carries 100.64/10 — GFN's VPN tell + var gfnRunning = false +} + struct StateSnapshot { var lidClosed: Bool = false var sleepDisabled: Bool = false @@ -166,6 +180,8 @@ struct StateSnapshot { /// Path to the active recording WAV when callRecording is true. /// Empty otherwise. var callRecordingPath: String = "" + /// GeForce NOW game mode — tailnet paused + AirDrop radio pinned down. + var gameMode: GameModeState = GameModeState() var totalActive: Int { activePrompts + activeSubagents } var hasWork: Bool { totalActive > 0 } @@ -222,6 +238,7 @@ struct StateSnapshot { let (rec, recPath) = readCallRecordingState() s.callRecording = rec s.callRecordingPath = recPath + s.gameMode = GameMode.state() return s } diff --git a/toolchain/macos/gfn.sh b/toolchain/macos/gfn.sh index 411d919253..5eddcc37e6 100755 --- a/toolchain/macos/gfn.sh +++ b/toolchain/macos/gfn.sh @@ -10,8 +10,16 @@ # macOS re-raises awdl0 whenever AirDrop/Continuity is touched, so `on` # installs a root LaunchDaemon that re-lowers it every 15 s; `off` removes it. # Privilege: sudo when there's a TTY, else a macOS admin dialog (works from -# Claude's `!` runner). Tailscale is left alone: no exit node, so GFN traffic -# never enters it. +# Claude's `!` runner). Tailscale is left alone here — but note that GFN's own +# VPN check does NOT care about routing: a visible utun holding a 100.64/10 +# address is enough for it to refuse to stream, exit node or not. +# +# Slab's menu bar now does both halves together (Fleet & System → Game Mode): +# tailnet paused + this same awdl hold, engaged by hand or automatically when +# GeForce NOW launches. Installing Slab's helper evicts this script's daemon +# so the two never fight over the interface — see +# slab/menubar-swift/Sources/SlabMenubar/GameMode.swift. This script remains +# the no-Slab path and the measurement tool (`gfn.sh check`). set -u LABEL=computer.aesthetic.gfn-awdl -- 2.51.2