Skip to content

Instantly share code, notes, and snippets.

@nyteshade
Last active April 23, 2025 15:02
Show Gist options
  • Save nyteshade/9450d62a1d147191b74ba10ae4578e51 to your computer and use it in GitHub Desktop.
Save nyteshade/9450d62a1d147191b74ba10ae4578e51 to your computer and use it in GitHub Desktop.
Launcher
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>QEMU</key>
<string>qemu-system-ppc</string>
<key>Arguments</key>
<array>
<string>-M</string>
<string>mac99,via=pmu</string>
<string>-m</string>
<string>2048</string>
<string>-drive</string>
<string>file=$RESOURCES/disk.img,format=raw</string>
<string>-boot</string>
<string>c</string>
</array>
</dict>
</plist>
import Foundation
import SwiftUI
import AppKit
import UniformTypeIdentifiers
/// QEMULauncher: Reads QEMU options from a plist file and launches QEMU with those options
///
/// This application is designed to be placed in App.app/Contents/MacOS/Launcher and will
/// read options from App.app/Contents/Resources/options.plist to launch QEMU.
class QEMULauncher: ObservableObject {
/// Path to the plist file containing QEMU options
let plistPath: String
/// Path to the Resources directory
let resourcesPath: String
/// Which qemu command to invoke with the supplied arguments. Defaults to
/// `qemu-system-ppc` but other variants can be specified instead.
var qemuSystemCmd: String = "qemu-system-ppc"
/// The running QEMU process
private var qemuProcess: Process?
/// The running Proxy process
private var proxyProcess: Process?
/// Process is running
@Published var isRunning: Bool = false
/// Proxy is running
@Published var isProxyRunning: Bool = false
/// Captured output from QEMU
@Published var capturedOutput: String = ""
/// Captured output from Proxy
@Published var proxyOutput: String = ""
/// Exit code from QEMU
@Published var exitCode: Int32 = 0
/// Status message
@Published var statusMessage: String = "Initializing..."
/// Error message
@Published var errorMessage: String = ""
/// Initializes the launcher with paths to required resources
///
/// - Parameters:
/// - plistPath: Path to the plist file containing QEMU options
/// - resourcesPath: Path to the Resources directory
init(plistPath: String, resourcesPath: String) {
self.plistPath = plistPath
self.resourcesPath = resourcesPath
}
/// Reads QEMU options from the plist file
///
/// - Returns: Array of command line arguments
/// - Throws: Error if the plist cannot be read or parsed
func readOptionsFromPlist() throws -> [String] {
guard let plistData = FileManager.default.contents(atPath: plistPath) else {
throw NSError(domain: "QEMULauncher", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to read plist file at \(plistPath)"])
}
var format = PropertyListSerialization.PropertyListFormat.xml
guard let plistDict = try PropertyListSerialization.propertyList(from: plistData, options: .mutableContainers, format: &format) as? [String: Any] else {
throw NSError(domain: "QEMULauncher", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to parse plist file as dictionary"])
}
qemuSystemCmd = plistDict["QEMU"] as? String ?? "qemu-system-ppc"
// Get the command line arguments array
guard let arguments = plistDict["Arguments"] as? [String] else {
throw NSError(domain: "QEMULauncher", code: 3, userInfo: [NSLocalizedDescriptionKey: "No 'Arguments' array found in plist"])
}
// Process arguments to replace any placeholders with full resource paths
return arguments.map { arg in
if arg.contains("$RESOURCES") {
return arg.replacingOccurrences(of: "$RESOURCES", with: resourcesPath)
}
return arg
}
}
/// Finds the full path to an executable by searching in PATH
///
/// - Parameter command: The command to find
/// - Returns: The full path to the command, or nil if not found
private func findExecutablePath(forCommand command: String) -> String? {
// Check if the command is already a full path
if FileManager.default.fileExists(atPath: command) {
return command
}
// Common locations to check for QEMU
let commonLocations = [
"/usr/local/bin/",
"/opt/homebrew/bin/",
"/usr/bin/",
"/bin/"
]
// Check common locations
for location in commonLocations {
let path = location + command
if FileManager.default.fileExists(atPath: path) {
return path
}
}
// Use which command
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/which")
process.arguments = [command]
let pipe = Pipe()
process.standardOutput = pipe
do {
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty {
return path
}
}
} catch {
print("Error finding path for \(command): \(error.localizedDescription)")
}
return nil
}
/// Launches the proxy server for old SSL compatibility
///
/// - Throws: Error if the proxy server fails to launch
func launchProxyServer() throws {
print("DEBUG: Attempting to launch proxy server")
// Path to the proxy server binary in the Resources directory
let proxyPath = resourcesPath + "/oldssl-proxy"
// Check if the proxy server binary exists
if !FileManager.default.fileExists(atPath: proxyPath) {
print("DEBUG: oldssl-proxy binary not found, using script-based approach")
// Create a script to run squid if the binary doesn't exist
let scriptPath = resourcesPath + "/start-proxy.sh"
// Simple script to check and start squid if available
let scriptContent = """
#!/bin/bash
export SQUID_CONFIG_DIR="\(resourcesPath)/squid"
export CERT_DIR="\(resourcesPath)/squid/ssl_cert"
echo "Starting proxy server script..."
echo "SQUID_CONFIG_DIR=$SQUID_CONFIG_DIR"
echo "CERT_DIR=$CERT_DIR"
# Check if squid is installed
if ! command -v squid &> /dev/null; then
echo "ERROR: Squid is not installed. Please install squid via homebrew: brew install squid"
exit 1
fi
echo "Found squid in PATH"
# Create directories if they don't exist
echo "Creating config directories..."
mkdir -p "$SQUID_CONFIG_DIR"
mkdir -p "$CERT_DIR"
mkdir -p "$CERT_DIR/public"
# Generate SSL certificate if it doesn't exist
if [ ! -f "$CERT_DIR/myCA.pem" ]; then
echo "Generating SSL certificate..."
openssl req -new -newkey rsa:1024 -sha1 -days 1825 -nodes -x509 -extensions v3_ca \
-subj '/C=AU/ST=Some-State/O=OldSSL Proxy' -keyout "$CERT_DIR/myCA.pem" \
-out "$CERT_DIR/myCA.pem" -batch
openssl x509 -in "$CERT_DIR/myCA.pem" -outform DER -out "$CERT_DIR/public/OldSSL.der"
openssl x509 -in "$CERT_DIR/myCA.pem" -outform PEM -out "$CERT_DIR/public/OldSSL.crt"
else
echo "SSL certificate already exists"
fi
# Create squid config file
echo "Creating squid configuration..."
cat > "$SQUID_CONFIG_DIR/squid.conf" << 'EOL'
http_port 3128 ssl-bump \
cert=$CERT_DIR/myCA.pem \
cipher=HIGH:MEDIUM:!LOW:!aNULL:!eNULL:!MD5:!EXP:!PSK:!SRP:!DSS \
options=NO_TICKET,ALL \
generate-host-certificates=on dynamic_cert_mem_cache_size=4MB
visible_hostname squid-oldssl-proxy
ssl_bump bump all
tcp_outgoing_address 0.0.0.0
sslproxy_cert_sign_hash sha1
http_access allow all
EOL
# Replace $CERT_DIR with actual path
echo "Updating paths in config..."
sed -i.bak "s|\\$CERT_DIR|$CERT_DIR|g" "$SQUID_CONFIG_DIR/squid.conf"
# Start squid
echo "Starting squid proxy server..."
squid -f "$SQUID_CONFIG_DIR/squid.conf" -N
"""
// Write the script to the file
do {
try scriptContent.write(toFile: scriptPath, atomically: true, encoding: String.Encoding.utf8)
try FileManager.default.setAttributes([FileAttributeKey.posixPermissions: 0o755], ofItemAtPath: scriptPath)
print("DEBUG: Created proxy script at \(scriptPath)")
} catch {
print("DEBUG: Failed to create proxy script: \(error.localizedDescription)")
throw NSError(domain: "QEMULauncher", code: 6, userInfo: [NSLocalizedDescriptionKey: "Failed to create proxy script: \(error.localizedDescription)"])
}
// Create the process
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/bash")
process.arguments = [scriptPath]
// Set up pipes for standard output and error
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
// Set up asynchronous reading of output and error
let outputHandle = outputPipe.fileHandleForReading
let errorHandle = errorPipe.fileHandleForReading
// Setup output handling
outputHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.proxyOutput += output
print("PROXY OUTPUT: \(output)")
}
}
}
// Setup error handling
errorHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.proxyOutput += "ERROR: " + output
print("PROXY ERROR: \(output)")
}
}
}
// Set up process termination handler
process.terminationHandler = { [weak self] process in
guard let self = self else { return }
// Clean up file handles
outputHandle.readabilityHandler = nil
errorHandle.readabilityHandler = nil
DispatchQueue.main.async {
self.isProxyRunning = false
print("DEBUG: Proxy server exited with code: \(process.terminationStatus)")
}
}
// Start the process
do {
print("DEBUG: Launching proxy script")
try process.run()
self.proxyProcess = process
self.isProxyRunning = true
print("DEBUG: Proxy server started on port 3128")
} catch {
print("DEBUG: Failed to launch proxy server: \(error.localizedDescription)")
throw NSError(domain: "QEMULauncher", code: 7, userInfo: [NSLocalizedDescriptionKey: "Failed to launch proxy server: \(error.localizedDescription)"])
}
} else {
print("DEBUG: Using pre-compiled oldssl-proxy binary")
// Use pre-compiled binary
let process = Process()
process.executableURL = URL(fileURLWithPath: proxyPath)
// Set up pipes for standard output and error
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
// Set up output handler
let outputHandle = outputPipe.fileHandleForReading
outputHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.proxyOutput += output
print("PROXY OUTPUT: \(output)")
}
}
}
// Set up error handler
let errorHandle = errorPipe.fileHandleForReading
errorHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.proxyOutput += "ERROR: " + output
print("PROXY ERROR: \(output)")
}
}
}
// Set up termination handler
process.terminationHandler = { [weak self] process in
guard let self = self else { return }
// Clean up file handles
outputHandle.readabilityHandler = nil
errorHandle.readabilityHandler = nil
DispatchQueue.main.async {
self.isProxyRunning = false
print("DEBUG: Binary proxy server exited with code: \(process.terminationStatus)")
}
}
// Start the process
do {
print("DEBUG: Launching binary proxy")
try process.run()
self.proxyProcess = process
self.isProxyRunning = true
print("DEBUG: Binary proxy server started from binary on port 3128")
} catch {
print("DEBUG: Failed to launch proxy server binary: \(error.localizedDescription)")
throw NSError(domain: "QEMULauncher", code: 7, userInfo: [NSLocalizedDescriptionKey: "Failed to launch proxy server binary: \(error.localizedDescription)"])
}
}
}
/// Terminates the proxy server if it's running
func terminateProxyServer() {
guard let process = proxyProcess, isProxyRunning else { return }
process.terminate()
isProxyRunning = false
}
/// Launches QEMU with the specified arguments
///
/// - Parameters:
/// - arguments: Array of command line arguments
/// - outputCallback: Optional callback to receive output from QEMU
/// - Throws: Error if QEMU fails to launch
func launchQEMU(with arguments: [String]) throws {
guard !isRunning else {
throw NSError(domain: "QEMULauncher", code: 5, userInfo: [NSLocalizedDescriptionKey: "QEMU is already running"])
}
// Create a process
let process = Process()
// Get the path to QEMU executable from PATH or use a default path
let qemuPath = findExecutablePath(forCommand: self.qemuSystemCmd)
if qemuPath != nil {
process.executableURL = URL(fileURLWithPath: qemuPath!)
process.arguments = arguments
} else {
// Fallback to using env if we can't find the executable directly
process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
// Combine the QEMU command with its arguments
var fullArguments = [self.qemuSystemCmd]
fullArguments.append(contentsOf: arguments)
process.arguments = fullArguments
}
// Set up environment variables
var environment = ProcessInfo.processInfo.environment
// Ensure PATH includes common locations where QEMU might be installed
if var path = environment["PATH"] {
if !path.contains("/usr/local/bin") {
path = "/usr/local/bin:" + path
}
if !path.contains("/opt/homebrew/bin") {
path = "/opt/homebrew/bin:" + path
}
environment["PATH"] = path
} else {
environment["PATH"] = "/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin"
}
// Add proxy-related environment variables if the proxy is running
if isProxyRunning {
environment["http_proxy"] = "http://localhost:3128"
environment["https_proxy"] = "http://localhost:3128"
environment["HTTP_PROXY"] = "http://localhost:3128"
environment["HTTPS_PROXY"] = "http://localhost:3128"
}
process.environment = environment
// Set up pipes for standard output and error
let outputPipe = Pipe()
let errorPipe = Pipe()
process.standardOutput = outputPipe
process.standardError = errorPipe
// Set up asynchronous reading of output and error
let outputHandle = outputPipe.fileHandleForReading
let errorHandle = errorPipe.fileHandleForReading
// Setup output handling
outputHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.capturedOutput += output
}
}
}
// Setup error handling
errorHandle.readabilityHandler = { [weak self] handle in
guard let self = self else { return }
let data = handle.availableData
if !data.isEmpty, let output = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.capturedOutput += "ERROR: " + output
}
}
}
// Set up process termination handler
process.terminationHandler = { [weak self] process in
guard let self = self else { return }
// Clean up file handles
outputHandle.readabilityHandler = nil
errorHandle.readabilityHandler = nil
DispatchQueue.main.async {
self.isRunning = false
self.exitCode = process.terminationStatus
self.statusMessage = "QEMU exited with code: \(process.terminationStatus)"
print("QEMU exited with code: \(process.terminationStatus)")
// Terminate the proxy server when QEMU exits
self.terminateProxyServer()
}
}
// Start the process
do {
self.statusMessage = "Launching QEMU..."
print("Launching QEMU at path: \(process.executableURL?.path ?? "unknown")")
try process.run()
self.qemuProcess = process
self.isRunning = true
self.statusMessage = "QEMU is running..."
} catch {
self.statusMessage = "Failed to launch QEMU"
self.errorMessage = error.localizedDescription
throw NSError(domain: "QEMULauncher", code: 4, userInfo: [NSLocalizedDescriptionKey: "Failed to launch QEMU: \(error.localizedDescription)"])
}
}
/// Terminates the QEMU process if it's running
func terminateQEMU() {
guard let process = qemuProcess, isRunning else { return }
process.terminate()
// The terminationHandler will update isRunning and exitCode
}
/// Asks the user for confirmation before terminating QEMU
///
/// - Parameter completionHandler: Called with true if the user confirms, false otherwise
func confirmTerminateQEMU(completionHandler: @escaping (Bool) -> Void) {
guard isRunning else {
completionHandler(true)
return
}
let alert = NSAlert()
alert.messageText = "QEMU is still running"
alert.informativeText = "Do you want to terminate QEMU and quit?"
alert.alertStyle = .warning
alert.addButton(withTitle: "Terminate and Quit")
alert.addButton(withTitle: "Cancel")
let response = alert.runModal()
if response == .alertFirstButtonReturn {
terminateQEMU()
completionHandler(true)
} else {
completionHandler(false)
}
}
/// Main entry point for the launcher
///
/// - Throws: Error if any step fails
func run() throws {
do {
// First, try to launch the proxy server
try launchProxyServer()
// Then, launch QEMU
let arguments = try readOptionsFromPlist()
print("Launching QEMU with arguments: \(arguments.joined(separator: " "))")
try launchQEMU(with: arguments)
} catch {
errorMessage = error.localizedDescription
print("Error: \(error.localizedDescription)")
throw error
}
}
}
/// The main content view for the QEMU launcher
struct QEMULauncherView: NSViewRepresentable {
@ObservedObject var launcher: QEMULauncher
func makeNSView(context: Context) -> NSScrollView {
// Create a text field for output
let textView = NSTextView()
textView.isEditable = false
textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
textView.textColor = NSColor.textColor
textView.backgroundColor = NSColor.textBackgroundColor
textView.drawsBackground = true
textView.isAutomaticQuoteSubstitutionEnabled = false
textView.isAutomaticLinkDetectionEnabled = true
textView.isAutomaticTextCompletionEnabled = false
textView.isAutomaticTextReplacementEnabled = false
textView.isAutomaticDashSubstitutionEnabled = false
textView.isAutomaticSpellingCorrectionEnabled = false
// Create a scroll view to contain the text view
let scrollView = NSScrollView()
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = true
scrollView.autohidesScrollers = true
return scrollView
}
func updateNSView(_ scrollView: NSScrollView, context: Context) {
guard let textView = scrollView.documentView as? NSTextView else { return }
// Update the text view content
textView.string = launcher.capturedOutput
// Scroll to the bottom
textView.scrollToEndOfDocument(nil)
}
}
/// The app delegate to handle application lifecycle events
class AppDelegate: NSObject, NSApplicationDelegate {
var launcher: QEMULauncher!
var window: NSWindow!
var statusTextField: NSTextField!
var errorTextField: NSTextField!
var contentView: NSView!
var terminateButton: NSButton!
var proxyToggleButton: NSButton!
// Timer for UI updates
private var uiTimer: Timer?
/// Checks if either squid or oldssl-proxy is available
@objc func checkProxyAvailable() -> Bool {
// Safety check - if launcher is not initialized, return false
guard launcher != nil else {
print("DEBUG: checkProxyAvailable called but launcher is nil")
return false
}
print("DEBUG: Checking for proxy in \(launcher.resourcesPath)")
// Check for oldssl-proxy binary in Resources
let proxyPath = launcher.resourcesPath + "/oldssl-proxy"
if FileManager.default.fileExists(atPath: proxyPath) {
print("DEBUG: Found oldssl-proxy binary at \(proxyPath)")
return true
} else {
print("DEBUG: oldssl-proxy binary not found at \(proxyPath)")
}
// Check for squid in PATH
print("DEBUG: Checking for squid in PATH")
// Create process safely
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/which")
process.arguments = ["squid"]
let pipe = Pipe()
process.standardOutput = pipe
do {
try process.run()
process.waitUntilExit()
if process.terminationStatus == 0 {
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if let path = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !path.isEmpty {
print("DEBUG: Found squid at \(path)")
return true
}
} else {
print("DEBUG: squid not found in PATH (exit code \(process.terminationStatus))")
}
} catch {
print("DEBUG: Error checking for squid: \(error.localizedDescription)")
}
print("DEBUG: No proxy server found")
return false
}
/// Terminates QEMU
@objc func terminateQEMU() {
launcher.terminateQEMU()
}
/// Toggles the proxy server from the button
@objc func toggleProxyButton(_ sender: NSButton) {
if launcher.isProxyRunning {
launcher.terminateProxyServer()
sender.state = .off
} else {
do {
try launcher.launchProxyServer()
sender.state = .on
} catch {
let alert = NSAlert()
alert.messageText = "Failed to Start Proxy"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
// Update the proxy menu item state to match
updateUI()
}
/// Toggles the proxy server from menu
@objc func toggleProxy(_ sender: NSMenuItem) {
if launcher.isProxyRunning {
launcher.terminateProxyServer()
print("Terminating proxy server from menu item")
} else {
do {
try launcher.launchProxyServer()
print("Launching proxy server from menu item")
} catch {
let alert = NSAlert()
alert.messageText = "Failed to Start Proxy"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
// Update UI to reflect changes
DispatchQueue.main.async {
self.updateUI()
}
}
/// Exports the SSL certificate
@objc func exportCertificate(_ sender: NSMenuItem) {
// Path to the certificate
let certPath = launcher.resourcesPath + "/squid/ssl_cert/public/OldSSL.crt"
let derPath = launcher.resourcesPath + "/squid/ssl_cert/public/OldSSL.der"
// Check if the certificate exists
if !FileManager.default.fileExists(atPath: certPath) && !FileManager.default.fileExists(atPath: derPath) {
let alert = NSAlert()
alert.messageText = "Certificate Not Found"
alert.informativeText = "The SSL certificate has not been generated yet."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
return
}
// Create save panel
let savePanel = NSSavePanel()
savePanel.title = "Export SSL Certificate"
savePanel.nameFieldStringValue = "OldSSL"
// Use modern UTType approach
if #available(macOS 11.0, *) {
savePanel.allowedContentTypes = [.data]
} else {
// Fallback for older macOS versions
savePanel.allowedFileTypes = ["crt", "der"]
}
savePanel.allowsOtherFileTypes = true
savePanel.isExtensionHidden = false
// Show save panel
savePanel.beginSheetModal(for: window) { response in
if response == .OK, let url = savePanel.url {
do {
if FileManager.default.fileExists(atPath: certPath) {
try FileManager.default.copyItem(atPath: certPath, toPath: url.path)
} else if FileManager.default.fileExists(atPath: derPath) {
try FileManager.default.copyItem(atPath: derPath, toPath: url.path)
}
// Show success dialog
let alert = NSAlert()
alert.messageText = "Certificate Exported"
alert.informativeText = "The certificate has been exported. You can now import it into your operating system."
alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
} catch {
// Show error dialog
let alert = NSAlert()
alert.messageText = "Export Failed"
alert.informativeText = error.localizedDescription
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
}
}
}
}
/// Shows proxy information
@objc func showProxyInfo(_ sender: NSMenuItem) {
// Create info window with proper ownership
let infoWindow = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 400, height: 300),
styleMask: [.titled, .closable],
backing: .buffered,
defer: false
)
infoWindow.title = "Proxy Information"
// Make window automatically release when closed
infoWindow.isReleasedWhenClosed = true
// Create text view
let textView = NSTextView(frame: NSRect(x: 0, y: 0, width: 380, height: 280))
textView.isEditable = false
textView.font = NSFont.systemFont(ofSize: 12)
textView.textContainerInset = NSSize(width: 10, height: 10)
// Prepare info text
var infoText = "Proxy Status: \(launcher.isProxyRunning ? "Running" : "Not Running")\n\n"
infoText += "Proxy Address: localhost\n"
infoText += "Proxy Port: 3128\n\n"
infoText += "Configuration:\n"
infoText += "- Set HTTP Proxy to localhost:3128\n"
infoText += "- Set HTTPS Proxy to localhost:3128\n\n"
infoText += "This proxy enables older versions of Mac OS X to access modern HTTPS websites by downgrading SSL/TLS protocols and adding support for older cipher suites. You must import the certificate into your system keychain to avoid certificate warnings."
// Set text
textView.string = infoText
// Create scroll view
let scrollView = NSScrollView(frame: NSRect(x: 10, y: 10, width: 380, height: 280))
scrollView.documentView = textView
scrollView.hasVerticalScroller = true
scrollView.hasHorizontalScroller = false
scrollView.autoresizingMask = [.width, .height]
// Add to window
infoWindow.contentView?.addSubview(scrollView)
// Center and show window
infoWindow.center()
infoWindow.makeKeyAndOrderFront(nil)
}
/// Updates the UI based on launcher state
@objc func updateUI() {
// Update text fields
statusTextField.stringValue = launcher.statusMessage
errorTextField.stringValue = launcher.errorMessage
terminateButton.isHidden = !launcher.isRunning
// Check proxy availability
let proxyAvailable = checkProxyAvailable()
// Update proxy toggle button
proxyToggleButton.isEnabled = proxyAvailable
proxyToggleButton.state = launcher.isProxyRunning ? .on : .off
// Update proxy menu if it exists
if let mainMenu = NSApplication.shared.mainMenu {
// Find the proxy menu
for menuItem in mainMenu.items {
if menuItem.title == "Proxy", let proxyMenu = menuItem.submenu {
// Update Enable Proxy item
if let enableProxyItem = proxyMenu.items.first(where: { $0.title == "Enable Old SSL Proxy" }) {
enableProxyItem.isEnabled = proxyAvailable
enableProxyItem.state = launcher.isProxyRunning ? .on : .off
}
// Update Export Certificate item
if let exportCertItem = proxyMenu.items.first(where: { $0.title == "Export SSL Certificate..." }) {
exportCertItem.isEnabled = launcher.isProxyRunning
}
break
}
}
}
// Debug output
print("UI updated: Proxy running: \(launcher.isProxyRunning), Proxy available: \(proxyAvailable)")
}
func applicationDidFinishLaunching(_ notification: Notification) {
do {
// Configure the application menu first (without relying on launcher)
setupMenus()
// Determine the bundle and resource paths
let bundleURL = Bundle.main.bundleURL
let resourcesPath = bundleURL.appendingPathComponent("Contents/Resources").path
let plistPath = resourcesPath + "/options.plist"
// Check if plist file exists
if !FileManager.default.fileExists(atPath: plistPath) {
throw NSError(domain: "QEMULauncher", code: 100,
userInfo: [NSLocalizedDescriptionKey: "Options file not found: \(plistPath). Make sure options.plist exists in the app's Resources directory."])
}
// Create the launcher
launcher = QEMULauncher(plistPath: plistPath, resourcesPath: resourcesPath)
// Create the window
window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 600, height: 400),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered,
defer: false
)
window.title = "QEMU Launcher"
window.center()
window.minSize = NSSize(width: 500, height: 300)
// Create content view
contentView = NSView(frame: window.contentView!.bounds)
contentView.autoresizingMask = [.width, .height]
window.contentView = contentView
// Create status text field
statusTextField = NSTextField(frame: NSRect(x: 20, y: window.contentView!.bounds.height - 60, width: window.contentView!.bounds.width - 40, height: 30))
statusTextField.isEditable = false
statusTextField.isBordered = false
statusTextField.backgroundColor = .clear
statusTextField.alignment = .center
statusTextField.font = NSFont.systemFont(ofSize: 14, weight: .bold)
statusTextField.stringValue = "Initializing..."
statusTextField.autoresizingMask = [.width, .maxYMargin]
contentView.addSubview(statusTextField)
// Create error text field
errorTextField = NSTextField(frame: NSRect(x: 20, y: window.contentView!.bounds.height - 90, width: window.contentView!.bounds.width - 40, height: 20))
errorTextField.isEditable = false
errorTextField.isBordered = false
errorTextField.backgroundColor = .clear
errorTextField.alignment = .center
errorTextField.textColor = .red
errorTextField.font = NSFont.systemFont(ofSize: 12)
errorTextField.stringValue = ""
errorTextField.autoresizingMask = [.width, .maxYMargin]
contentView.addSubview(errorTextField)
// Create output view
let outputView = NSHostingView(rootView: QEMULauncherView(launcher: launcher))
outputView.frame = NSRect(x: 20, y: 50, width: window.contentView!.bounds.width - 40, height: window.contentView!.bounds.height - 150)
outputView.autoresizingMask = [.width, .height]
contentView.addSubview(outputView)
// Create terminate button
terminateButton = NSButton(frame: NSRect(x: window.contentView!.bounds.width / 2 - 120, y: 15, width: 120, height: 24))
terminateButton.title = "Terminate QEMU"
terminateButton.bezelStyle = .rounded
terminateButton.target = self
terminateButton.action = #selector(terminateQEMU)
terminateButton.isHidden = true
terminateButton.autoresizingMask = [NSView.AutoresizingMask.minXMargin, NSView.AutoresizingMask.maxXMargin, NSView.AutoresizingMask.maxYMargin]
contentView.addSubview(terminateButton)
// Create proxy toggle button
proxyToggleButton = NSButton(frame: NSRect(x: window.contentView!.bounds.width / 2 + 5, y: 15, width: 120, height: 24))
proxyToggleButton.title = "Toggle Proxy"
proxyToggleButton.bezelStyle = .rounded
proxyToggleButton.target = self
proxyToggleButton.action = #selector(toggleProxyButton(_:))
proxyToggleButton.isEnabled = false // Will enable in updateUI if proxy is available
proxyToggleButton.autoresizingMask = [NSView.AutoresizingMask.minXMargin, NSView.AutoresizingMask.maxXMargin, NSView.AutoresizingMask.maxYMargin]
contentView.addSubview(proxyToggleButton)
// Set the window delegate to handle close events
window.delegate = self
// Show the window
window.makeKeyAndOrderFront(nil)
// Now that everything is initialized, update the UI which will check proxy availability
updateUI()
// Launch QEMU in a background thread
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
try self.launcher.run()
} catch {
// Display error in the UI
DispatchQueue.main.async {
self.errorTextField.stringValue = "Error launching QEMU: \(error.localizedDescription)"
self.statusTextField.stringValue = "Failed to start"
// Show an alert with the error details
let alert = NSAlert(error: error)
alert.runModal()
print("Error launching QEMU: \(error.localizedDescription)")
}
}
}
// Set up a timer to periodically update the UI
uiTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in
DispatchQueue.main.async {
self?.updateUI()
}
}
} catch {
// Show error alert for initialization errors
let alert = NSAlert(error: error)
alert.runModal()
// Log error details
print("Failed to initialize application: \(error.localizedDescription)")
// Exit with error code
exit(1)
}
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
// Check if QEMU is running and ask for confirmation if needed
if launcher.isRunning {
launcher.confirmTerminateQEMU { shouldTerminate in
if shouldTerminate {
NSApplication.shared.reply(toApplicationShouldTerminate: true)
} else {
NSApplication.shared.reply(toApplicationShouldTerminate: false)
}
}
return .terminateLater
}
return .terminateNow
}
/// Sets up the application menus
private func setupMenus() {
let mainMenu = NSMenu()
// Application menu
let appMenu = NSMenu()
let appMenuItem = NSMenuItem()
appMenuItem.submenu = appMenu
let appName = ProcessInfo.processInfo.processName
// About item
let aboutMenuItem = NSMenuItem(
title: "About \(appName)",
action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)),
keyEquivalent: ""
)
// Separator
let separator1 = NSMenuItem.separator()
// Quit item
let quitMenuItem = NSMenuItem(
title: "Quit \(appName)",
action: #selector(NSApplication.terminate(_:)),
keyEquivalent: "q"
)
// Add items to app menu
appMenu.addItem(aboutMenuItem)
appMenu.addItem(separator1)
appMenu.addItem(quitMenuItem)
// Add app menu to main menu
mainMenu.addItem(appMenuItem)
// Create proxy menu
let proxyMenu = NSMenu()
proxyMenu.title = "Proxy"
let proxyMenuItem = NSMenuItem()
proxyMenuItem.title = "Proxy"
proxyMenuItem.submenu = proxyMenu
// Enable Proxy item - initially disabled, will be updated in updateUI
let enableProxyItem = NSMenuItem(
title: "Enable Old SSL Proxy",
action: #selector(toggleProxy(_:)),
keyEquivalent: "p"
)
enableProxyItem.isEnabled = false // Will update this after launcher is initialized
// Export Certificate item
let exportCertItem = NSMenuItem(
title: "Export SSL Certificate...",
action: #selector(exportCertificate(_:)),
keyEquivalent: "e"
)
exportCertItem.isEnabled = false // Will update this after launcher is initialized
// Show Proxy Info item
let proxyInfoItem = NSMenuItem(
title: "Show Proxy Information",
action: #selector(showProxyInfo(_:)),
keyEquivalent: "i"
)
// Add items to proxy menu
proxyMenu.addItem(enableProxyItem)
proxyMenu.addItem(exportCertItem)
proxyMenu.addItem(NSMenuItem.separator())
proxyMenu.addItem(proxyInfoItem)
// Add proxy menu to main menu
mainMenu.addItem(proxyMenuItem)
// Set the menu
NSApplication.shared.mainMenu = mainMenu
}
}
// Make AppDelegate conform to NSWindowDelegate
extension AppDelegate: NSWindowDelegate {
func windowShouldClose(_ sender: NSWindow) -> Bool {
// Check if QEMU is running and ask for confirmation if needed
if launcher.isRunning {
launcher.confirmTerminateQEMU { shouldClose in
if shouldClose {
NSApplication.shared.terminate(nil)
}
}
return false
}
// If QEMU is not running, close the window and terminate the app
NSApplication.shared.terminate(nil)
return false
}
}
// Main function for the application entry point
func main() {
let app = NSApplication.shared
let delegate = AppDelegate()
// Set up top-level exception handler
NSSetUncaughtExceptionHandler { exception in
let alert = NSAlert()
alert.messageText = "An unhandled exception occurred"
alert.informativeText = "Details: \(exception.name) - \(exception.reason ?? "")\n\nStack Trace:\n\(exception.callStackSymbols.joined(separator: "\n"))"
alert.alertStyle = .critical
alert.addButton(withTitle: "OK")
alert.runModal()
// Log to console
print("FATAL ERROR: \(exception.name) - \(exception.reason ?? "")")
print("Stack Trace:\n\(exception.callStackSymbols.joined(separator: "\n"))")
// Write to log file in app bundle resources
do {
let bundleURL = Bundle.main.bundleURL
let resourcesPath = bundleURL.appendingPathComponent("Contents/Resources").path
let logPath = resourcesPath + "/error.log"
let logContent = """
Time: \(Date())
Error: \(exception.name) - \(exception.reason ?? "")
Stack Trace:
\(exception.callStackSymbols.joined(separator: "\n"))
"""
try logContent.write(toFile: logPath, atomically: true, encoding: .utf8)
} catch {
print("Failed to write error log: \(error)")
}
}
// Set the delegate and run the app
app.delegate = delegate
do {
// Instead of calling app.run() directly, wrap it in a try-catch block
try {
app.run()
return 0 // Never reached in normal operation
}()
} catch {
// Show error alert
let alert = NSAlert(error: error)
alert.runModal()
// Log the error
print("FATAL ERROR: \(error)")
// Write to log file
do {
let bundleURL = Bundle.main.bundleURL
let resourcesPath = bundleURL.appendingPathComponent("Contents/Resources").path
let logPath = resourcesPath + "/error.log"
let logContent = """
Time: \(Date())
Error: \(error)
"""
try logContent.write(toFile: logPath, atomically: true, encoding: .utf8)
} catch {
print("Failed to write error log: \(error)")
}
return 1 // Return error code
}
}
// Run the application
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment