import AppKit import Darwin import MediaPlayer @_cdecl("terebi_app_main") @MainActor public func checkAppStartup() { if let configuration = Bundle.main.url(forResource: "cli-check", withExtension: "json") { CLIInstanceCheck.run(configuration) return } AppStartupCheck.run() } @MainActor private enum AppStartupCheck { static var model: PlayerModel? static var timer: Timer? static let deadline = ContinuousClock.now.advanced(by: .seconds(15)) static let historyDomain = Bundle.main.bundleIdentifier! + ".history" static func run() { _ = NSApplication.shared NSApp.setActivationPolicy(.regular) let defaults = UserDefaults(suiteName: historyDomain)! model = PlayerModel(defaults: defaults) model!.setVolume(0) model!.open(URL(fileURLWithPath: CommandLine.arguments[2]).appendingPathComponent("av.mp4")) timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in MainActor.assumeIsolated { finishWhenReady() } } NSApp.run() } static func finishWhenReady() { guard let model else { return } let ready = model.canTogglePlayback && model.state.status == PLAYER_PLAYING guard ready || model.state.status == PLAYER_ERROR || ContinuousClock.now >= deadline else { return } timer?.invalidate() let pid = ProcessInfo.processInfo.processIdentifier let application = NSRunningApplication.current let recognized = application.processIdentifier == pid && NSWorkspace.shared.runningApplications.contains { $0.processIdentifier == pid && $0.bundleURL == Bundle.main.bundleURL } let result: [String: Any] = [ "passed": ready && recognized && MPNowPlayingInfoCenter.default().playbackState == .playing, "pid": pid, "registeredPID": application.processIdentifier, "bundleIdentifier": application.bundleIdentifier ?? "nil", "playing": ready, ] do { let pipe = try FileHandle(forWritingTo: URL(fileURLWithPath: CommandLine.arguments[1])) try pipe.write(contentsOf: JSONSerialization.data(withJSONObject: result)) try pipe.write(contentsOf: Data([10])) try pipe.close() } catch { FileHandle.standardError.write(Data("Startup check could not report: \(error)\n".utf8)) } model.shutdown() UserDefaults(suiteName: historyDomain)!.removePersistentDomain(forName: historyDomain) self.model = nil NSApp.terminate(nil) } } @MainActor private enum CLIInstanceCheck { static var timer: Timer? static var requestID = "" static var editorText = "" static func selectMenuItem(_ title: String, in menu: NSMenu) -> Bool { for (index, item) in menu.items.enumerated() { if item.title == title, item.action != nil { precondition(item.isEnabled, "Native menu action \(title) must be available") menu.performActionForItem(at: index) return true } if let submenu = item.submenu, selectMenuItem(title, in: submenu) { return true } } return false } static func prepareMenu(_ menu: NSMenu) { // Direct key-equivalent dispatch does not open/track the native menu first. // Ask AppKit to prepare and validate its real items just as normal menu use does. menu.delegate?.menuNeedsUpdate?(menu) menu.update() for item in menu.items { if let submenu = item.submenu { prepareMenu(submenu) } } } static func run(_ configuration: URL) { let settings = try! JSONSerialization.jsonObject(with: Data(contentsOf: configuration)) as! [String: String] let directory = URL(fileURLWithPath: settings["directory"]!, isDirectory: true) let stderrURL = directory.appendingPathComponent("stderr-\(ProcessInfo.processInfo.processIdentifier).log") precondition(FileManager.default.createFile(atPath: stderrURL.path, contents: nil)) let errorLog = try! FileHandle(forWritingTo: stderrURL) precondition(dup2(errorLog.fileDescriptor, STDERR_FILENO) >= 0) try! errorLog.close() setenv("PATH", settings["path"]!, 1) _ = NSApplication.shared _ = NotificationCenter.default.addObserver(forName: NSApplication.willFinishLaunchingNotification, object: nil, queue: .main) { _ in MainActor.assumeIsolated { PlayerModel.shared.setVolume(0) } } // Exercise the real SwiftUI scene and delegate without displaying test windows. _ = NotificationCenter.default.addObserver(forName: NSWindow.didUpdateNotification, object: nil, queue: .main) { _ in MainActor.assumeIsolated { for window in NSApp.windows { window.alphaValue = 0 window.ignoresMouseEvents = true } } } timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in MainActor.assumeIsolated { let model = PlayerModel.shared let pid = ProcessInfo.processInfo.processIdentifier // Text input can create non-main auxiliary windows; count actual player windows. let windows = NSApp.windows.filter { $0.canBecomeMain && !($0 is NSPanel) && $0.contentView != nil } for window in windows { window.alphaValue = 0 } let request = directory.appendingPathComponent("keys-\(pid).json") var pendingRequestID = "" if let data = try? Data(contentsOf: request), let command = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let id = command["id"] as? String, id != requestID, let window = windows.first { pendingRequestID = id if !NSApp.isActive || !window.isKeyWindow || !window.isMainWindow { // Synthetic native key requests need foreground focus; cooperative activation // can be refused by the unrelated app currently active on the workstation. NSApp.activate(ignoringOtherApps: true) window.makeKeyAndOrderFront(nil) window.makeMain() } else { requestID = id if let media = command["media"] as? String { model.open(URL(fileURLWithPath: media)) } if let action = command["action"] as? String { switch action { case "youtube": model.openYouTube(command["url"] as! String) case "seek": let position = command["position"] as! Double precondition(model.seekRelative(position - model.state.position), "Native seek must be accepted") case "pause": precondition(model.perform(.pause), "Native pause must be accepted") case "play": precondition(model.perform(.play), "Native play must be accepted") case "quality": precondition(model.canChangeYouTubeQuality, "Native quality change must be available") model.setYouTubeQuality((command["height"] as! Int)) case "close": model.closeFile() case "quit": timer?.invalidate() precondition(model.perform(.quit), "Native quit must be accepted") return default: fatalError("Unknown native check action \(action)") } } if let events = command["events"] as? [[String: Any]] { window.makeKey() for input in events { if let title = input["select"] as? String { guard let menu = NSApp.mainMenu else { fatalError("Native application menu is missing") } prepareMenu(menu) precondition(selectMenuItem(title, in: menu), "Native menu action \(title) must exist") continue } let editor = NSTextView(frame: .zero) if input["editing"] as? Bool == true { window.contentView?.addSubview(editor) window.makeFirstResponder(editor) } else { window.makeFirstResponder(nil) } let text = input["key"] as! String let event = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: NSEvent.ModifierFlags(rawValue: UInt(input["flags"] as? Int ?? 0)), timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: text, charactersIgnoringModifiers: text, isARepeat: input["repeat"] as? Bool ?? false, keyCode: UInt16(input["code"] as? Int ?? 7))! if input["menu"] as? Bool == true { if let menu = NSApp.mainMenu { prepareMenu(menu) } precondition(NSApp.mainMenu?.performKeyEquivalent(with: event) == true, "Configured shortcut must be accepted by the native menu") } else if input["queued"] as? Bool == true { NSApp.postEvent(event, atStart: false) } else { NSApp.sendEvent(event) } if input["editing"] as? Bool == true && input["queued"] as? Bool != true { editorText = editor.string window.makeFirstResponder(nil) editor.removeFromSuperview() } } } } } let applications = NSWorkspace.shared.runningApplications.filter { !$0.isTerminated && $0.bundleURL == Bundle.main.bundleURL } let snapshot: [String: Any] = [ "pid": pid, "pids": applications.map(\.processIdentifier).sorted(), "media": model.fileName ?? "", "status": model.state.status.rawValue, "position": model.state.position, "duration": model.state.duration, "seekPending": model.state.seek_pending, "canSeek": model.canSeek, "busy": model.isBusy, "youtubeHeight": model.youtubeHeight ?? 0, "canChangeQuality": model.canChangeYouTubeQuality, "qualityHeights": model.youtubeHeights, "windows": windows.count, "hasAudio": model.state.has_audio, "hasVideo": model.state.has_video, "error": model.errorText ?? "", "subtitleLabels": model.subtitleTracks.map(\.label), "selectedSubtitle": model.selectedSubtitle ?? -1, "subtitleText": model.subtitleText, "subtitleError": model.subtitleError ?? "", "showBufferInfo": model.showsBufferInfo, "automaticQuality": model.automaticYouTubeQuality, "snapToEdges": model.compactWindow.snappingEnabled, "requestID": requestID, "pendingRequestID": pendingRequestID, "active": NSApp.isActive, "keyWindow": windows.first?.isKeyWindow ?? false, "mainWindow": windows.first?.isMainWindow ?? false, "editorText": editorText, "compact": model.isCompact, "onTop": model.isOnTop, ] do { let data = try JSONSerialization.data(withJSONObject: snapshot) try data.write(to: directory.appendingPathComponent("state-\(pid).json"), options: .atomic) } catch { fatalError("Could not publish CLI instance check: \(error)") } } } terebiAppMain() } }