From 55463a980623e25117e17f00b969546ea8b78037 Mon Sep 17 00:00:00 2001 From: Jake Hillion Date: Thu, 8 Jan 2026 12:48:00 +0000 Subject: [PATCH] fmt: add swift formatting Swift code currently has no auto formatting. Add `swift-format` to the `treefmt-nix` config to get this formatted. As our existing Swift code uses 4-space formatting instead of the default 2-space, also adds a custom `.swift-format Test plan: - CI --- .swift-format | 6 + app/EXO/EXO/ContentView.swift | 38 ++-- app/EXO/EXO/EXOApp.swift | 12 +- app/EXO/EXO/ExoProcessController.swift | 7 +- app/EXO/EXO/Models/ClusterState.swift | 19 +- app/EXO/EXO/Services/BugReportService.swift | 79 +++++--- .../EXO/Services/ClusterStateService.swift | 14 +- .../EXO/Services/LocalNetworkChecker.swift | 5 +- app/EXO/EXO/Services/NetworkSetupHelper.swift | 181 +++++++++--------- .../EXO/Services/NetworkStatusService.swift | 10 +- .../EXO/ViewModels/InstanceViewModel.swift | 12 +- app/EXO/EXO/ViewModels/NodeViewModel.swift | 24 ++- app/EXO/EXO/Views/InstanceRowView.swift | 21 +- app/EXO/EXO/Views/NodeDetailView.swift | 1 - app/EXO/EXO/Views/NodeRowView.swift | 1 - app/EXO/EXO/Views/TopologyMiniView.swift | 17 +- app/EXO/EXOTests/EXOTests.swift | 1 + flake.nix | 17 +- 18 files changed, 273 insertions(+), 192 deletions(-) create mode 100644 .swift-format diff --git a/.swift-format b/.swift-format new file mode 100644 index 00000000..71b901ac --- /dev/null +++ b/.swift-format @@ -0,0 +1,6 @@ +{ + "version": 1, + "indentation": { + "spaces": 4 + } +} diff --git a/app/EXO/EXO/ContentView.swift b/app/EXO/EXO/ContentView.swift index 2125f0f9..4107a938 100644 --- a/app/EXO/EXO/ContentView.swift +++ b/app/EXO/EXO/ContentView.swift @@ -70,10 +70,12 @@ struct ContentView: View { .font(.caption) .fontWeight(.semibold) } - Text("Device discovery won't work. To fix:\n1. Quit EXO\n2. Open System Settings → Privacy & Security → Local Network\n3. Toggle EXO off, then back on\n4. Relaunch EXO") - .font(.caption2) - .foregroundColor(.secondary) - .fixedSize(horizontal: false, vertical: true) + Text( + "Device discovery won't work. To fix:\n1. Quit EXO\n2. Open System Settings → Privacy & Security → Local Network\n3. Toggle EXO off, then back on\n4. Relaunch EXO" + ) + .font(.caption2) + .foregroundColor(.secondary) + .fixedSize(horizontal: false, vertical: true) Button { openLocalNetworkSettings() } label: { @@ -96,14 +98,18 @@ struct ContentView: View { private func openLocalNetworkSettings() { // Open Privacy & Security settings - Local Network section - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork") { + if let url = URL( + string: "x-apple.systempreferences:com.apple.preference.security?Privacy_LocalNetwork") + { NSWorkspace.shared.open(url) } } private var topologySection: some View { Group { - if let topology = stateService.latestSnapshot?.topologyViewModel(localNodeId: stateService.localNodeId), !topology.nodes.isEmpty { + if let topology = stateService.latestSnapshot?.topologyViewModel( + localNodeId: stateService.localNodeId), !topology.nodes.isEmpty + { TopologyMiniView(topology: topology) } } @@ -137,8 +143,10 @@ struct ContentView: View { VStack(alignment: .leading, spacing: 4) { HStack { VStack(alignment: .leading) { - Text("\(overview.usedRam, specifier: "%.0f") / \(overview.totalRam, specifier: "%.0f") GB") - .font(.headline) + Text( + "\(overview.usedRam, specifier: "%.0f") / \(overview.totalRam, specifier: "%.0f") GB" + ) + .font(.headline) Text("Memory") .font(.caption) .foregroundColor(.secondary) @@ -262,7 +270,9 @@ struct ContentView: View { } } - private func controlButton(title: String, tint: Color = .primary, action: @escaping () -> Void) -> some View { + private func controlButton(title: String, tint: Color = .primary, action: @escaping () -> Void) + -> some View + { HoverButton(title: title, tint: tint, trailingSystemImage: nil, action: action) } @@ -293,9 +303,12 @@ struct ContentView: View { Button { isExpanded.wrappedValue.toggle() } label: { - Label(isExpanded.wrappedValue ? "Hide" : "Show All", systemImage: isExpanded.wrappedValue ? "chevron.up" : "chevron.down") - .labelStyle(.titleAndIcon) - .contentTransition(.symbolEffect(.replace)) + Label( + isExpanded.wrappedValue ? "Hide" : "Show All", + systemImage: isExpanded.wrappedValue ? "chevron.up" : "chevron.down" + ) + .labelStyle(.titleAndIcon) + .contentTransition(.symbolEffect(.replace)) } .buttonStyle(.plain) .font(.caption2) @@ -588,4 +601,3 @@ private struct HoverButton: View { .onHover { isHovering = $0 } } } - diff --git a/app/EXO/EXO/EXOApp.swift b/app/EXO/EXO/EXOApp.swift index af1be1b5..82bb5c7b 100644 --- a/app/EXO/EXO/EXOApp.swift +++ b/app/EXO/EXO/EXOApp.swift @@ -8,9 +8,9 @@ import AppKit import CoreImage import CoreImage.CIFilterBuiltins +import ServiceManagement import Sparkle import SwiftUI -import ServiceManagement import UserNotifications import os.log @@ -113,7 +113,7 @@ struct EXOApp: App { filter.contrast = 0.9 guard let output = filter.outputImage, - let rendered = ciContext.createCGImage(output, from: output.extent) + let rendered = ciContext.createCGImage(output, from: output.extent) else { return nil } @@ -126,7 +126,8 @@ struct EXOApp: App { do { try SMAppService.mainApp.register() } catch { - Logger().error("Failed to register EXO for launch at login: \(error.localizedDescription)") + Logger().error( + "Failed to register EXO for launch at login: \(error.localizedDescription)") } } } @@ -151,7 +152,7 @@ final class SparkleUpdater: NSObject, ObservableObject { center.requestAuthorization(options: [.alert, .sound]) { _, _ in } controller.updater.automaticallyChecksForUpdates = true controller.updater.automaticallyDownloadsUpdates = false - controller.updater.updateCheckInterval = 900 // 15 minutes + controller.updater.updateCheckInterval = 900 // 15 minutes DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak controller] in controller?.updater.checkForUpdatesInBackground() } @@ -218,7 +219,8 @@ private final class ExoNotificationDelegate: NSObject, UNUserNotificationCenterD func userNotificationCenter( _ center: UNUserNotificationCenter, willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> + Void ) { completionHandler([.banner, .list, .sound]) } diff --git a/app/EXO/EXO/ExoProcessController.swift b/app/EXO/EXO/ExoProcessController.swift index 431a6d68..69d8e02a 100644 --- a/app/EXO/EXO/ExoProcessController.swift +++ b/app/EXO/EXO/ExoProcessController.swift @@ -31,7 +31,8 @@ final class ExoProcessController: ObservableObject { @Published private(set) var launchCountdownSeconds: Int? @Published var customNamespace: String = { return UserDefaults.standard.string(forKey: customNamespaceKey) ?? "" - }() { + }() + { didSet { UserDefaults.standard.set(customNamespace, forKey: customNamespaceKey) } @@ -221,7 +222,9 @@ final class ExoProcessController: ObservableObject { if let tag = Bundle.main.infoDictionary?["EXOBuildTag"] as? String, !tag.isEmpty { return tag } - if let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, !short.isEmpty { + if let short = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, + !short.isEmpty + { return short } return "dev" diff --git a/app/EXO/EXO/Models/ClusterState.swift b/app/EXO/EXO/Models/ClusterState.swift index 8f750e7e..b82bf7ea 100644 --- a/app/EXO/EXO/Models/ClusterState.swift +++ b/app/EXO/EXO/Models/ClusterState.swift @@ -16,10 +16,13 @@ struct ClusterState: Decodable { self.instances = rawInstances.mapValues(\.instance) self.runners = try container.decode([String: RunnerStatusSummary].self, forKey: .runners) self.nodeProfiles = try container.decode([String: NodeProfile].self, forKey: .nodeProfiles) - let rawTasks = try container.decodeIfPresent([String: TaggedTask].self, forKey: .tasks) ?? [:] + let rawTasks = + try container.decodeIfPresent([String: TaggedTask].self, forKey: .tasks) ?? [:] self.tasks = rawTasks.compactMapValues(\.task) self.topology = try container.decodeIfPresent(Topology.self, forKey: .topology) - let rawDownloads = try container.decodeIfPresent([String: [TaggedNodeDownload]].self, forKey: .downloads) ?? [:] + let rawDownloads = + try container.decodeIfPresent([String: [TaggedNodeDownload]].self, forKey: .downloads) + ?? [:] self.downloads = rawDownloads.mapValues { $0.compactMap(\.status) } } @@ -41,7 +44,8 @@ private struct TaggedInstance: Decodable { let payloads = try container.decode([String: ClusterInstancePayload].self) guard let entry = payloads.first else { throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Empty instance payload") + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Empty instance payload") ) } self.instance = ClusterInstance( @@ -77,7 +81,8 @@ struct RunnerStatusSummary: Decodable { let payloads = try container.decode([String: RunnerStatusDetail].self) guard let entry = payloads.first else { throw DecodingError.dataCorrupted( - DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Empty runner status payload") + DecodingError.Context( + codingPath: decoder.codingPath, debugDescription: "Empty runner status payload") ) } self.status = entry.key @@ -257,7 +262,9 @@ struct ChatCompletionTaskParameters: Decodable, Equatable { func promptPreview() -> String? { guard let messages else { return nil } - if let userMessage = messages.last(where: { $0.role?.lowercased() == "user" && ($0.content?.isEmpty == false) }) { + if let userMessage = messages.last(where: { + $0.role?.lowercased() == "user" && ($0.content?.isEmpty == false) + }) { return userMessage.content } return messages.last?.content @@ -365,5 +372,3 @@ extension ClusterState { func availableModels() -> [ModelOption] { [] } } - - diff --git a/app/EXO/EXO/Services/BugReportService.swift b/app/EXO/EXO/Services/BugReportService.swift index b578deb9..e1ee9031 100644 --- a/app/EXO/EXO/Services/BugReportService.swift +++ b/app/EXO/EXO/Services/BugReportService.swift @@ -66,7 +66,7 @@ struct BugReportService { ("\(prefix)exo.log", logData), ("\(prefix)state.json", stateData), ("\(prefix)events.json", eventsData), - ("\(prefix)report.json", reportJSON) + ("\(prefix)report.json", reportJSON), ] let uploader = try S3Uploader(config: credentials) @@ -78,7 +78,8 @@ struct BugReportService { ) } - return BugReportOutcome(success: true, message: "Bug Report sent. Thank you for helping to improve EXO 1.0.") + return BugReportOutcome( + success: true, message: "Bug Report sent. Thank you for helping to improve EXO 1.0.") } private func loadCredentials() throws -> AWSConfig { @@ -100,7 +101,8 @@ struct BugReportService { private func captureIfconfig() async throws -> String { let result = runCommand(["/sbin/ifconfig"]) guard result.exitCode == 0 else { - throw BugReportError.collectFailed(result.error.isEmpty ? "ifconfig failed" : result.error) + throw BugReportError.collectFailed( + result.error.isEmpty ? "ifconfig failed" : result.error) } return result.output } @@ -113,7 +115,9 @@ struct BugReportService { } private func readThunderboltBridgeDisabled() -> Bool? { - let result = runCommand(["/usr/sbin/networksetup", "-getnetworkserviceenabled", "Thunderbolt Bridge"]) + let result = runCommand([ + "/usr/sbin/networksetup", "-getnetworkserviceenabled", "Thunderbolt Bridge", + ]) guard result.exitCode == 0 else { return nil } let output = result.output.lowercased() if output.contains("enabled") { @@ -156,7 +160,8 @@ struct BugReportService { request.timeoutInterval = 5 do { let (data, response) = try await URLSession.shared.data(for: request) - guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { + guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) + else { return nil } return data @@ -182,7 +187,7 @@ struct BugReportService { "system": system, "exo_version": exo.version as Any, "exo_commit": exo.commit as Any, - "report_type": isManual ? "manual" : "automated" + "report_type": isManual ? "manual" : "automated", ] return try? JSONSerialization.data(withJSONObject: payload, options: [.prettyPrinted]) } @@ -213,10 +218,13 @@ struct BugReportService { let user = safeRunCommand(["/usr/bin/whoami"]) let consoleUser = safeRunCommand(["/usr/bin/stat", "-f%Su", "/dev/console"]) let uptime = safeRunCommand(["/usr/bin/uptime"]) - let diskRoot = safeRunCommand(["/bin/sh", "-c", "/bin/df -h / | awk 'NR==2 {print $1, $2, $3, $4, $5}'"]) + let diskRoot = safeRunCommand([ + "/bin/sh", "-c", "/bin/df -h / | awk 'NR==2 {print $1, $2, $3, $4, $5}'", + ]) let interfacesList = safeRunCommand(["/usr/sbin/ipconfig", "getiflist"]) - let interfacesAndIPs = interfacesList? + let interfacesAndIPs = + interfacesList? .split(whereSeparator: { $0 == " " || $0 == "\n" }) .compactMap { iface -> [String: Any]? in let name = String(iface) @@ -227,7 +235,8 @@ struct BugReportService { } ?? [] let wifiSSID: String? - let airportPath = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport" + let airportPath = + "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport" if FileManager.default.isExecutableFile(atPath: airportPath) { wifiSSID = safeRunCommand([airportPath, "-I"]).flatMap(parseWifiSSID) } else { @@ -255,7 +264,7 @@ struct BugReportService { "disk_root": diskRoot as Any, "interfaces_and_ips": interfacesAndIPs, "ipconfig_getiflist": interfacesList as Any, - "wifi_ssid": wifiSSID as Any + "wifi_ssid": wifiSSID as Any, ] } @@ -313,7 +322,8 @@ struct BugReportService { for line in airportOutput.split(separator: "\n") { let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed.hasPrefix("SSID:") { - return trimmed.replacingOccurrences(of: "SSID:", with: "").trimmingCharacters(in: .whitespaces) + return trimmed.replacingOccurrences(of: "SSID:", with: "").trimmingCharacters( + in: .whitespaces) } } return nil @@ -358,7 +368,7 @@ private struct DebugInfo { func toDictionary() -> [String: Any] { [ "name": name, - "ip": ip as Any + "ip": ip as Any, ] } } @@ -366,7 +376,7 @@ private struct DebugInfo { func toDictionary() -> [String: Any] { [ "thunderbolt_bridge_disabled": thunderboltBridgeDisabled as Any, - "interfaces": interfaces.map { $0.toDictionary() } + "interfaces": interfaces.map { $0.toDictionary() }, ] } } @@ -398,7 +408,7 @@ private struct S3Uploader { let headers = [ "host": host, "x-amz-content-sha256": payloadHash, - "x-amz-date": amzDate + "x-amz-date": amzDate, ] let canonicalRequest = buildCanonicalRequest( @@ -414,18 +424,20 @@ private struct S3Uploader { canonicalRequestHash: sha256Hex(canonicalRequest.data(using: .utf8) ?? Data()) ) - let signingKey = deriveKey(secret: config.secretKey, dateStamp: dateStamp, region: config.region, service: "s3") + let signingKey = deriveKey( + secret: config.secretKey, dateStamp: dateStamp, region: config.region, service: "s3") let signature = hmacHex(key: signingKey, data: Data(stringToSign.utf8)) let signedHeaders = "host;x-amz-content-sha256;x-amz-date" let authorization = """ -AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3/aws4_request, SignedHeaders=\(signedHeaders), Signature=\(signature) -""" + AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3/aws4_request, SignedHeaders=\(signedHeaders), Signature=\(signature) + """ var request = URLRequest(url: url) request.httpMethod = "PUT" request.httpBody = body - request.setValue(headers["x-amz-content-sha256"], forHTTPHeaderField: "x-amz-content-sha256") + request.setValue( + headers["x-amz-content-sha256"], forHTTPHeaderField: "x-amz-content-sha256") request.setValue(headers["x-amz-date"], forHTTPHeaderField: "x-amz-date") request.setValue(host, forHTTPHeaderField: "Host") request.setValue(authorization, forHTTPHeaderField: "Authorization") @@ -433,7 +445,7 @@ AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3 let (data, response) = try await URLSession.shared.data(for: request) guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { let statusText = (response as? HTTPURLResponse)?.statusCode ?? -1 - _ = data // ignore response body for UX + _ = data // ignore response body for UX throw BugReportError.uploadFailed("HTTP status \(statusText)") } } @@ -447,7 +459,8 @@ AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3 let canonicalURI = encodePath(url.path) let canonicalQuery = url.query ?? "" let sortedHeaders = headers.sorted { $0.key < $1.key } - let canonicalHeaders = sortedHeaders + let canonicalHeaders = + sortedHeaders .map { "\($0.key.lowercased()):\($0.value)\n" } .joined() let signedHeaders = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";") @@ -458,15 +471,17 @@ AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3 canonicalQuery, canonicalHeaders, signedHeaders, - payloadHash + payloadHash, ].joined(separator: "\n") } private func encodePath(_ path: String) -> String { - return path + return + path .split(separator: "/") .map { segment in - segment.addingPercentEncoding(withAllowedCharacters: Self.rfc3986) ?? String(segment) + segment.addingPercentEncoding(withAllowedCharacters: Self.rfc3986) + ?? String(segment) } .joined(separator: "/") .prependSlashIfNeeded() @@ -478,14 +493,16 @@ AWS4-HMAC-SHA256 Credential=\(config.accessKey)/\(dateStamp)/\(config.region)/s3 canonicalRequestHash: String ) -> String { """ -AWS4-HMAC-SHA256 -\(amzDate) -\(dateStamp)/\(config.region)/s3/aws4_request -\(canonicalRequestHash) -""" + AWS4-HMAC-SHA256 + \(amzDate) + \(dateStamp)/\(config.region)/s3/aws4_request + \(canonicalRequestHash) + """ } - private func deriveKey(secret: String, dateStamp: String, region: String, service: String) -> Data { + private func deriveKey(secret: String, dateStamp: String, region: String, service: String) + -> Data + { let kDate = hmac(key: Data(("AWS4" + secret).utf8), data: Data(dateStamp.utf8)) let kRegion = hmac(key: kDate, data: Data(region.utf8)) let kService = hmac(key: kRegion, data: Data(service.utf8)) @@ -528,8 +545,8 @@ AWS4-HMAC-SHA256 }() } -private extension String { - func prependSlashIfNeeded() -> String { +extension String { + fileprivate func prependSlashIfNeeded() -> String { if hasPrefix("/") { return self } diff --git a/app/EXO/EXO/Services/ClusterStateService.swift b/app/EXO/EXO/Services/ClusterStateService.swift index 65097e8c..27c7ffdf 100644 --- a/app/EXO/EXO/Services/ClusterStateService.swift +++ b/app/EXO/EXO/Services/ClusterStateService.swift @@ -57,7 +57,9 @@ final class ClusterStateService: ObservableObject { var request = URLRequest(url: url) request.cachePolicy = .reloadIgnoringLocalCacheData let (data, response) = try await session.data(for: request) - guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else { + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) + else { return } if let nodeId = try? decoder.decode(String.self, from: data) { @@ -113,7 +115,9 @@ final class ClusterStateService: ObservableObject { } } - func launchInstance(modelId: String, sharding: String, instanceMeta: String, minNodes: Int) async { + func launchInstance(modelId: String, sharding: String, instanceMeta: String, minNodes: Int) + async + { do { var request = URLRequest(url: baseURL.appendingPathComponent("instance")) request.httpMethod = "POST" @@ -122,7 +126,7 @@ final class ClusterStateService: ObservableObject { "model_id": modelId, "sharding": sharding, "instance_meta": instanceMeta, - "min_nodes": minNodes + "min_nodes": minNodes, ] request.httpBody = try JSONSerialization.data(withJSONObject: payload, options: []) let (_, response) = try await session.data(for: request) @@ -143,7 +147,9 @@ final class ClusterStateService: ObservableObject { do { let url = baseURL.appendingPathComponent("models") let (data, response) = try await session.data(from: url) - guard let httpResponse = response as? HTTPURLResponse, (200..<300).contains(httpResponse.statusCode) else { + guard let httpResponse = response as? HTTPURLResponse, + (200..<300).contains(httpResponse.statusCode) + else { throw URLError(.badServerResponse) } let list = try decoder.decode(ModelListResponse.self, from: data) diff --git a/app/EXO/EXO/Services/LocalNetworkChecker.swift b/app/EXO/EXO/Services/LocalNetworkChecker.swift index 5b3a48d2..20dfd4d8 100644 --- a/app/EXO/EXO/Services/LocalNetworkChecker.swift +++ b/app/EXO/EXO/Services/LocalNetworkChecker.swift @@ -110,8 +110,9 @@ final class LocalNetworkChecker: ObservableObject { } case .failed(let error): let errorStr = "\(error)" - if errorStr.contains("65") || errorStr.contains("EHOSTUNREACH") || - errorStr.contains("permission") || errorStr.contains("denied") { + if errorStr.contains("65") || errorStr.contains("EHOSTUNREACH") + || errorStr.contains("permission") || errorStr.contains("denied") + { resumeOnce(.notWorking(reason: "Permission denied")) } else { resumeOnce(.notWorking(reason: "Failed: \(error.localizedDescription)")) diff --git a/app/EXO/EXO/Services/NetworkSetupHelper.swift b/app/EXO/EXO/Services/NetworkSetupHelper.swift index f62d44b1..c73e744b 100644 --- a/app/EXO/EXO/Services/NetworkSetupHelper.swift +++ b/app/EXO/EXO/Services/NetworkSetupHelper.swift @@ -5,61 +5,62 @@ import os.log enum NetworkSetupHelper { private static let logger = Logger(subsystem: "io.exo.EXO", category: "NetworkSetup") private static let daemonLabel = "io.exo.networksetup" - private static let scriptDestination = "/Library/Application Support/EXO/disable_bridge_enable_dhcp.sh" + private static let scriptDestination = + "/Library/Application Support/EXO/disable_bridge_enable_dhcp.sh" private static let plistDestination = "/Library/LaunchDaemons/io.exo.networksetup.plist" private static let requiredStartInterval: Int = 1791 private static let setupScript = """ -#!/usr/bin/env bash + #!/usr/bin/env bash -set -euo pipefail + set -euo pipefail -PREFS="/Library/Preferences/SystemConfiguration/preferences.plist" + PREFS="/Library/Preferences/SystemConfiguration/preferences.plist" -# Remove bridge0 interface -ifconfig bridge0 &>/dev/null && { - ifconfig bridge0 | grep -q 'member' && { - ifconfig bridge0 | awk '/member/ {print $2}' | xargs -n1 ifconfig bridge0 deletem 2>/dev/null || true - } - ifconfig bridge0 destroy 2>/dev/null || true -} + # Remove bridge0 interface + ifconfig bridge0 &>/dev/null && { + ifconfig bridge0 | grep -q 'member' && { + ifconfig bridge0 | awk '/member/ {print $2}' | xargs -n1 ifconfig bridge0 deletem 2>/dev/null || true + } + ifconfig bridge0 destroy 2>/dev/null || true + } -# Remove Thunderbolt Bridge from VirtualNetworkInterfaces in preferences.plist -/usr/libexec/PlistBuddy -c "Delete :VirtualNetworkInterfaces:Bridge:bridge0" "$PREFS" 2>/dev/null || true + # Remove Thunderbolt Bridge from VirtualNetworkInterfaces in preferences.plist + /usr/libexec/PlistBuddy -c "Delete :VirtualNetworkInterfaces:Bridge:bridge0" "$PREFS" 2>/dev/null || true -networksetup -listlocations | grep -q exo || { - networksetup -createlocation exo -} + networksetup -listlocations | grep -q exo || { + networksetup -createlocation exo + } -networksetup -switchtolocation exo -networksetup -listallhardwareports \\ - | awk -F': ' '/Hardware Port: / {print $2}' \\ - | while IFS=":" read -r name; do - case "$name" in - "Ethernet Adapter"*) - ;; - "Thunderbolt Bridge") - ;; - "Thunderbolt "*) - networksetup -listallnetworkservices \\ - | grep -q "EXO $name" \\ - || networksetup -createnetworkservice "EXO $name" "$name" 2>/dev/null \\ - || continue - networksetup -setdhcp "EXO $name" - ;; - *) - networksetup -listallnetworkservices \\ - | grep -q "$name" \\ - || networksetup -createnetworkservice "$name" "$name" 2>/dev/null \\ - || continue - ;; - esac - done + networksetup -switchtolocation exo + networksetup -listallhardwareports \\ + | awk -F': ' '/Hardware Port: / {print $2}' \\ + | while IFS=":" read -r name; do + case "$name" in + "Ethernet Adapter"*) + ;; + "Thunderbolt Bridge") + ;; + "Thunderbolt "*) + networksetup -listallnetworkservices \\ + | grep -q "EXO $name" \\ + || networksetup -createnetworkservice "EXO $name" "$name" 2>/dev/null \\ + || continue + networksetup -setdhcp "EXO $name" + ;; + *) + networksetup -listallnetworkservices \\ + | grep -q "$name" \\ + || networksetup -createnetworkservice "$name" "$name" 2>/dev/null \\ + || continue + ;; + esac + done -networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { - networksetup -setnetworkserviceenabled "Thunderbolt Bridge" off -} || true -""" + networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { + networksetup -setnetworkserviceenabled "Thunderbolt Bridge" off + } || true + """ static func ensureLaunchDaemonInstalled() { Task.detached { @@ -70,7 +71,9 @@ networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { try await installLaunchDaemon() logger.info("Network setup launch daemon installed and started") } catch { - logger.error("Network setup launch daemon failed: \(error.localizedDescription, privacy: .public)") + logger.error( + "Network setup launch daemon failed: \(error.localizedDescription, privacy: .public)" + ) } } } @@ -82,7 +85,8 @@ networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { guard scriptExists, plistExists else { return false } guard let data = try? Data(contentsOf: URL(fileURLWithPath: plistDestination)), - let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] + let plist = try? PropertyListSerialization.propertyList( + from: data, options: [], format: nil) as? [String: Any] else { return false } @@ -92,7 +96,9 @@ networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { else { return false } - if let programArgs = plist["ProgramArguments"] as? [String], programArgs.contains(scriptDestination) == false { + if let programArgs = plist["ProgramArguments"] as? [String], + programArgs.contains(scriptDestination) == false + { return false } return true @@ -105,58 +111,59 @@ networksetup -listnetworkservices | grep -q "Thunderbolt Bridge" && { private static func makeInstallerScript() -> String { """ -set -euo pipefail + set -euo pipefail -LABEL="\(daemonLabel)" -SCRIPT_DEST="\(scriptDestination)" -PLIST_DEST="\(plistDestination)" + LABEL="\(daemonLabel)" + SCRIPT_DEST="\(scriptDestination)" + PLIST_DEST="\(plistDestination)" -mkdir -p "$(dirname "$SCRIPT_DEST")" + mkdir -p "$(dirname "$SCRIPT_DEST")" -cat > "$SCRIPT_DEST" <<'EOF_SCRIPT' -\(setupScript) -EOF_SCRIPT -chmod 755 "$SCRIPT_DEST" + cat > "$SCRIPT_DEST" <<'EOF_SCRIPT' + \(setupScript) + EOF_SCRIPT + chmod 755 "$SCRIPT_DEST" -cat > "$PLIST_DEST" <<'EOF_PLIST' - - - - - Label - \(daemonLabel) - ProgramArguments - - /bin/bash - \(scriptDestination) - - StartInterval - \(requiredStartInterval) - RunAtLoad - - StandardOutPath - /var/log/\(daemonLabel).log - StandardErrorPath - /var/log/\(daemonLabel).err.log - - -EOF_PLIST + cat > "$PLIST_DEST" <<'EOF_PLIST' + + + + + Label + \(daemonLabel) + ProgramArguments + + /bin/bash + \(scriptDestination) + + StartInterval + \(requiredStartInterval) + RunAtLoad + + StandardOutPath + /var/log/\(daemonLabel).log + StandardErrorPath + /var/log/\(daemonLabel).err.log + + + EOF_PLIST -launchctl bootout system/"$LABEL" >/dev/null 2>&1 || true -launchctl bootstrap system "$PLIST_DEST" -launchctl enable system/"$LABEL" -launchctl kickstart -k system/"$LABEL" -""" + launchctl bootout system/"$LABEL" >/dev/null 2>&1 || true + launchctl bootstrap system "$PLIST_DEST" + launchctl enable system/"$LABEL" + launchctl kickstart -k system/"$LABEL" + """ } private static func runShellAsAdmin(_ script: String) throws { - let escapedScript = script + let escapedScript = + script .replacingOccurrences(of: "\\", with: "\\\\") .replacingOccurrences(of: "\"", with: "\\\"") let appleScriptSource = """ -do shell script "\(escapedScript)" with administrator privileges -""" + do shell script "\(escapedScript)" with administrator privileges + """ guard let appleScript = NSAppleScript(source: appleScriptSource) else { throw NetworkSetupError.scriptCreationFailed diff --git a/app/EXO/EXO/Services/NetworkStatusService.swift b/app/EXO/EXO/Services/NetworkStatusService.swift index 16e09667..1c390121 100644 --- a/app/EXO/EXO/Services/NetworkStatusService.swift +++ b/app/EXO/EXO/Services/NetworkStatusService.swift @@ -85,10 +85,11 @@ private struct NetworkStatusFetcher { private func readBridgeInactive() -> Bool? { let result = runCommand(["ifconfig", "bridge0"]) guard result.exitCode == 0 else { return nil } - guard let statusLine = result.output - .components(separatedBy: .newlines) - .first(where: { $0.contains("status:") })? - .lowercased() + guard + let statusLine = result.output + .components(separatedBy: .newlines) + .first(where: { $0.contains("status:") })? + .lowercased() else { return nil } @@ -171,4 +172,3 @@ private struct NetworkStatusFetcher { ) } } - diff --git a/app/EXO/EXO/ViewModels/InstanceViewModel.swift b/app/EXO/EXO/ViewModels/InstanceViewModel.swift index c472a162..510e85fe 100644 --- a/app/EXO/EXO/ViewModels/InstanceViewModel.swift +++ b/app/EXO/EXO/ViewModels/InstanceViewModel.swift @@ -107,10 +107,13 @@ extension ClusterState { let nodeToRunner = instance.shardAssignments.nodeToRunner let nodeIds = Array(nodeToRunner.keys) let runnerIds = Array(nodeToRunner.values) - let nodeNames = nodeIds.compactMap { nodeProfiles[$0]?.friendlyName ?? nodeProfiles[$0]?.modelId ?? $0 } + let nodeNames = nodeIds.compactMap { + nodeProfiles[$0]?.friendlyName ?? nodeProfiles[$0]?.modelId ?? $0 + } let statuses = runnerIds.compactMap { runners[$0]?.status.lowercased() } let downloadProgress = aggregateDownloadProgress(for: nodeIds) - let state = InstanceViewModel.State(statuses: statuses, hasActiveDownload: downloadProgress != nil) + let state = InstanceViewModel.State( + statuses: statuses, hasActiveDownload: downloadProgress != nil) let chatTasks = (chatTasksByInstance[entry.key] ?? []) .sorted(by: { $0.sortPriority < $1.sortPriority }) .map { InstanceTaskViewModel(task: $0) } @@ -165,8 +168,8 @@ extension ClusterState { } } -private extension InstanceViewModel.State { - init(statuses: [String], hasActiveDownload: Bool = false) { +extension InstanceViewModel.State { + fileprivate init(statuses: [String], hasActiveDownload: Bool = false) { if statuses.contains(where: { $0.contains("failed") }) { self = .failed } else if hasActiveDownload || statuses.contains(where: { $0.contains("downloading") }) { @@ -243,4 +246,3 @@ extension InstanceTaskViewModel { self.parameters = task.parameters } } - diff --git a/app/EXO/EXO/ViewModels/NodeViewModel.swift b/app/EXO/EXO/ViewModels/NodeViewModel.swift index 3c72155d..e52670f0 100644 --- a/app/EXO/EXO/ViewModels/NodeViewModel.swift +++ b/app/EXO/EXO/ViewModels/NodeViewModel.swift @@ -87,7 +87,9 @@ struct TopologyViewModel { extension ClusterState { func topologyViewModel(localNodeId: String?) -> TopologyViewModel? { let topologyNodeIds = Set(topology?.nodes.map(\.nodeId) ?? []) - let allNodes = nodeViewModels().filter { topologyNodeIds.isEmpty || topologyNodeIds.contains($0.id) } + let allNodes = nodeViewModels().filter { + topologyNodeIds.isEmpty || topologyNodeIds.contains($0.id) + } guard !allNodes.isEmpty else { return nil } let nodesById = Dictionary(uniqueKeysWithValues: allNodes.map { ($0.id, $0) }) @@ -106,18 +108,24 @@ extension ClusterState { } // Rotate so the local node (from /node_id API) is first - if let localId = localNodeId, let index = orderedNodes.firstIndex(where: { $0.id == localId }) { + if let localId = localNodeId, + let index = orderedNodes.firstIndex(where: { $0.id == localId }) + { orderedNodes = Array(orderedNodes[index...]) + Array(orderedNodes[.. some View { + private func detailRow( + icon: String? = nil, title: String, value: String, tint: Color = .secondary + ) -> some View { HStack(alignment: .firstTextBaseline, spacing: 6) { if let icon { Image(systemName: icon) @@ -329,4 +335,3 @@ struct InstanceRowView: View { } } } - diff --git a/app/EXO/EXO/Views/NodeDetailView.swift b/app/EXO/EXO/Views/NodeDetailView.swift index 5a38a65b..5e330e2e 100644 --- a/app/EXO/EXO/Views/NodeDetailView.swift +++ b/app/EXO/EXO/Views/NodeDetailView.swift @@ -32,4 +32,3 @@ struct NodeDetailView: View { } } } - diff --git a/app/EXO/EXO/Views/NodeRowView.swift b/app/EXO/EXO/Views/NodeRowView.swift index 730e9ad4..324958d4 100644 --- a/app/EXO/EXO/Views/NodeRowView.swift +++ b/app/EXO/EXO/Views/NodeRowView.swift @@ -28,4 +28,3 @@ struct NodeRowView: View { .padding(.vertical, 4) } } - diff --git a/app/EXO/EXO/Views/TopologyMiniView.swift b/app/EXO/EXO/Views/TopologyMiniView.swift index eccab35b..b7179112 100644 --- a/app/EXO/EXO/Views/TopologyMiniView.swift +++ b/app/EXO/EXO/Views/TopologyMiniView.swift @@ -76,30 +76,33 @@ struct TopologyMiniView: View { private func connectionLines(in size: CGSize) -> some View { let positions = positionedNodes(in: size) - let positionById = Dictionary(uniqueKeysWithValues: positions.map { ($0.node.id, $0.point) }) + let positionById = Dictionary( + uniqueKeysWithValues: positions.map { ($0.node.id, $0.point) }) return Canvas { context, _ in guard !topology.edges.isEmpty else { return } let nodeRadius: CGFloat = 32 let arrowLength: CGFloat = 10 let arrowSpread: CGFloat = .pi / 7 for edge in topology.edges { - guard let start = positionById[edge.sourceId], let end = positionById[edge.targetId] else { continue } + guard let start = positionById[edge.sourceId], let end = positionById[edge.targetId] + else { continue } let dx = end.x - start.x let dy = end.y - start.y let distance = max(CGFloat(hypot(dx, dy)), 1) let ux = dx / distance let uy = dy / distance - let adjustedStart = CGPoint(x: start.x + ux * nodeRadius, y: start.y + uy * nodeRadius) + let adjustedStart = CGPoint( + x: start.x + ux * nodeRadius, y: start.y + uy * nodeRadius) let adjustedEnd = CGPoint(x: end.x - ux * nodeRadius, y: end.y - uy * nodeRadius) var linePath = Path() linePath.move(to: adjustedStart) linePath.addLine(to: adjustedEnd) - context.stroke( + context.stroke( linePath, with: .color(.secondary.opacity(0.3)), - style: StrokeStyle(lineWidth: 1, dash: [4, 4]) - ) + style: StrokeStyle(lineWidth: 1, dash: [4, 4]) + ) let angle = atan2(uy, ux) let tip = adjustedEnd @@ -168,5 +171,3 @@ private struct NodeGlyphView: View { .frame(width: 95) } } - - diff --git a/app/EXO/EXOTests/EXOTests.swift b/app/EXO/EXOTests/EXOTests.swift index d8595d48..2e587804 100644 --- a/app/EXO/EXOTests/EXOTests.swift +++ b/app/EXO/EXOTests/EXOTests.swift @@ -6,6 +6,7 @@ // import Testing + @testable import EXO struct EXOTests { diff --git a/flake.nix b/flake.nix index f1de5df1..94d43688 100644 --- a/flake.nix +++ b/flake.nix @@ -42,11 +42,18 @@ }; treefmtEval = inputs.treefmt-nix.lib.evalModule pkgs { projectRootFile = "flake.nix"; - programs.ruff-format.enable = true; - programs.ruff-format.excludes = [ "rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi" ]; - programs.rustfmt.enable = true; - programs.rustfmt.package = (fenixToolchain system).rustfmt; - programs.nixpkgs-fmt.enable = true; + programs = { + nixpkgs-fmt.enable = true; + ruff-format = { + enable = true; + excludes = [ "rust/exo_pyo3_bindings/exo_pyo3_bindings.pyi" ]; + }; + rustfmt = { + enable = true; + package = (fenixToolchain system).rustfmt; + }; + swift-format.enable = true; + }; }; in {