114 lines
5.1 KiB
Swift
114 lines
5.1 KiB
Swift
import AppKit
|
|
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 func run(_ configuration: URL) {
|
|
let settings = try! JSONSerialization.jsonObject(with: Data(contentsOf: configuration)) as! [String: String]
|
|
let directory = URL(fileURLWithPath: settings["directory"]!, isDirectory: true)
|
|
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 } }
|
|
}
|
|
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
|
|
MainActor.assumeIsolated {
|
|
let model = PlayerModel.shared
|
|
let pid = ProcessInfo.processInfo.processIdentifier
|
|
let windows = NSApp.windows.filter { !($0 is NSPanel) && $0.contentView != nil }
|
|
for window in windows { window.alphaValue = 0 }
|
|
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,
|
|
"windows": windows.count,
|
|
"hasAudio": model.state.has_audio,
|
|
"hasVideo": model.state.has_video,
|
|
"error": model.errorText ?? "",
|
|
"showBufferInfo": model.showsBufferInfo,
|
|
"automaticQuality": model.automaticYouTubeQuality,
|
|
"snapToEdges": model.compactWindow.snappingEnabled,
|
|
]
|
|
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()
|
|
}
|
|
}
|