diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 7bfd0185..ba342fac 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -6,6 +6,8 @@ updates: interval: "weekly" time: "06:00" timezone: "America/Chicago" + cooldown: + default-days: 7 labels: [] commit-message: prefix: "ci" diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..5630c94e --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,5 @@ +rules: + # `uses: $/...` isn't understood by actionlint yet, so keep using `./...` + # until https://github.com/rhysd/actionlint/issues/711 is resolved. + self-repository: + disable: true diff --git a/Coder-Desktop/.swiftformat b/Coder-Desktop/.swiftformat index 388c4a61..aeb673e1 100644 --- a/Coder-Desktop/.swiftformat +++ b/Coder-Desktop/.swiftformat @@ -1,4 +1,4 @@ --selfrequired log,info,error,debug,critical,fault --exclude **.pb.swift,**.grpc.swift --condassignment always ---disable unusedArguments \ No newline at end of file +--disable unusedArguments,wrapIfStatementBodies,wrapPropertyBodies \ No newline at end of file diff --git a/Coder-Desktop/Coder-Desktop/About.swift b/Coder-Desktop/Coder-Desktop/About.swift index 902ef409..f2f38c2f 100644 --- a/Coder-Desktop/Coder-Desktop/About.swift +++ b/Coder-Desktop/Coder-Desktop/About.swift @@ -1,7 +1,7 @@ import SwiftUI enum About { - public static let repo: String = "https://github.com/coder/coder-desktop-macos" + static let repo: String = "https://github.com/coder/coder-desktop-macos" private static var credits: NSAttributedString { let coder = NSMutableAttributedString( string: "Coder.com", diff --git a/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift b/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift index b663533d..0e014979 100644 --- a/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift +++ b/Coder-Desktop/Coder-Desktop/AppHelperXPCClient.swift @@ -3,7 +3,7 @@ import NetworkExtension import os import VPNLib -// This is the client for the app to communicate with the privileged helper. +/// This is the client for the app to communicate with the privileged helper. @objc final class HelperXPCClient: NSObject, @unchecked Sendable { private var svc: CoderVPNService private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperXPCClient") @@ -43,7 +43,7 @@ import VPNLib return connection } - // Establishes a connection to the Helper, so it can send messages back. + /// Establishes a connection to the Helper, so it can send messages back. func ping() async throws { let conn = connect() return try await withCheckedThrowingContinuation { continuation in @@ -83,7 +83,7 @@ import VPNLib } } -// These methods are called by the Helper over XPC +/// These methods are called by the Helper over XPC extension HelperXPCClient: AppXPCInterface { func onPeerUpdate(_ diff: Data, reply: @escaping () -> Void) { let reply = CompletionWrapper(reply) diff --git a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift index 4f4a6032..a5bd1781 100644 --- a/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift +++ b/Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift @@ -141,8 +141,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { NotificationCenter.default.removeObserver(self) } - // This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` - // or return `.terminateNow` + /// This function MUST eventually call `NSApp.reply(toApplicationShouldTerminate: true)` + /// or return `.terminateNow` func applicationShouldTerminate(_: NSApplication) -> NSApplication.TerminateReply { Task { async let vpnTask: Void = { @@ -214,15 +214,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { alert.addButton(withTitle: "OK") let response = alert.runModal() state.skipHiddenIconAlert = suppressCheckbox.state == .on - if #available(macOS 26, *), response == .alertFirstButtonReturn { - // We'll need to ensure this continues to work in future macOS versions - if !NSWorkspace.shared.open( - URL(string: "x-apple.systempreferences:com.apple.ControlCenter-Settings.extension?MenuBar")! - ) { - NSWorkspace.shared.open( - URL(string: "x-apple.systempreferences:com.apple.ControlCenter-Settings.extension")! - ) - } + if #available(macOS 26, *), response == .alertFirstButtonReturn, + // We'll need to ensure this continues to work in future macOS versions + !NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.ControlCenter-Settings.extension?MenuBar")! + ) + { + NSWorkspace.shared.open( + URL(string: "x-apple.systempreferences:com.apple.ControlCenter-Settings.extension")! + ) } } } diff --git a/Coder-Desktop/Coder-Desktop/Notifications.swift b/Coder-Desktop/Coder-Desktop/Notifications.swift index 041cc363..3602c819 100644 --- a/Coder-Desktop/Coder-Desktop/Notifications.swift +++ b/Coder-Desktop/Coder-Desktop/Notifications.swift @@ -9,8 +9,8 @@ extension AppDelegate: UNUserNotificationCenterDelegate { ) } - // This function is required for notifications to appear as banners whilst the app is running. - // We're effectively forwarding the notification back to the OS + /// This function is required for notifications to appear as banners whilst the app is running. + /// We're effectively forwarding the notification back to the OS nonisolated func userNotificationCenter( _: UNUserNotificationCenter, willPresent _: UNNotification diff --git a/Coder-Desktop/Coder-Desktop/State.swift b/Coder-Desktop/Coder-Desktop/State.swift index bb7c0094..67f8fdf5 100644 --- a/Coder-Desktop/Coder-Desktop/State.swift +++ b/Coder-Desktop/Coder-Desktop/State.swift @@ -12,7 +12,7 @@ class AppState: ObservableObject { let dangerousDisableCoderSignatureValidation: Bool - // Stored in UserDefaults + /// Stored in UserDefaults @Published private(set) var hasSession: Bool { didSet { guard persistent else { return } @@ -31,7 +31,7 @@ class AppState: ObservableObject { static let defaultHostnameSuffix: String = "coder" - // Stored in Keychain + /// Stored in Keychain @Published private(set) var sessionToken: String? { didSet { guard persistent else { return } @@ -39,7 +39,7 @@ class AppState: ObservableObject { } } - public var client: Client? + var client: Client? @Published var useLiteralHeaders: Bool = UserDefaults.standard.bool(forKey: Keys.useLiteralHeaders) { didSet { @@ -57,7 +57,7 @@ class AppState: ObservableObject { } } - // Defaults to `true` + /// Defaults to `true` @Published var stopVPNOnQuit: Bool = UserDefaults.standard.optionalBool(forKey: Keys.stopVPNOnQuit) ?? true { didSet { guard persistent else { return } @@ -110,13 +110,13 @@ class AppState: ObservableObject { private let onChange: ((NETunnelProviderProtocol?) -> Void)? - // reconfigure must be called when any property used to configure the VPN changes - public func reconfigure() { + /// reconfigure must be called when any property used to configure the VPN changes + func reconfigure() { if let onChange { onChange(tunnelProviderProtocol()) } } - public init(onChange: ((NETunnelProviderProtocol?) -> Void)? = nil, - persistent: Bool = true) + init(onChange: ((NETunnelProviderProtocol?) -> Void)? = nil, + persistent: Bool = true) { self.persistent = persistent self.onChange = onChange @@ -151,7 +151,7 @@ class AppState: ObservableObject { } } - public func login(baseAccessURL: URL, sessionToken: String) { + func login(baseAccessURL: URL, sessionToken: String) { hasSession = true self.baseAccessURL = baseAccessURL self.sessionToken = sessionToken @@ -164,7 +164,7 @@ class AppState: ObservableObject { reconfigure() } - public func handleTokenExpiry() async { + func handleTokenExpiry() async { if hasSession { do { _ = try await client!.user("me") @@ -186,13 +186,13 @@ class AppState: ObservableObject { @Published var dashboardURL: URL? private var refreshTask: Task? - public func refreshDeploymentConfig() async { + func refreshDeploymentConfig() async { // Client is non-nil if there's a sesssion if hasSession, let client { refreshTask?.cancel() refreshTask = Task { - let res = try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { + try? await retry(floor: .milliseconds(100), ceil: .seconds(10)) { do { let config = try await client.agentConnectionInfoGeneric() return config.hostname_suffix @@ -201,7 +201,6 @@ class AppState: ObservableObject { throw error } } - return res } hostnameSuffix = await refreshTask?.value ?? Self.defaultHostnameSuffix @@ -213,7 +212,7 @@ class AppState: ObservableObject { } } - public func clearSession() { + func clearSession() { hasSession = false sessionToken = nil dashboardURL = nil @@ -258,11 +257,6 @@ struct LiteralHeader: Hashable, Identifiable, Equatable, Codable { var id: String { "\(name):\(value)" } - - init(name: String, value: String) { - self.name = name - self.value = value - } } extension LiteralHeader { @@ -272,8 +266,8 @@ extension LiteralHeader { } extension UserDefaults { - // Unlike the exisitng `bool(forKey:)` method which returns `false` for both - // missing values this method can return `nil`. + /// Unlike the exisitng `bool(forKey:)` method which returns `false` for both + /// missing values this method can return `nil`. func optionalBool(forKey key: String) -> Bool? { guard object(forKey: key) != nil else { return nil diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index c0f5eaa6..78fc2ad5 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -2,9 +2,9 @@ import Sparkle import SwiftUI final class UpdaterService: NSObject, ObservableObject { - // The auto-updater can be entirely disabled by setting the - // `disableUpdater` UserDefaults key to `true`. This is designed for use in - // MDM configurations, where the value can be set to `true` permanently. + /// The auto-updater can be entirely disabled by setting the + /// `disableUpdater` UserDefaults key to `true`. This is designed for use in + /// MDM configurations, where the value can be set to `true` permanently. let disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater) @Published var canCheckForUpdates = true diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift index 83ff709a..d3c5b10f 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift @@ -34,7 +34,7 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable { self.primaryHost = primaryHost } - // Agents are sorted by status, and then by name + /// Agents are sorted by status, and then by name static func < (lhs: Agent, rhs: Agent) -> Bool { if lhs.status != rhs.status { return lhs.status < rhs.status @@ -113,7 +113,7 @@ enum AgentStatus: Int, Equatable, Comparable { case no_recent_handshake = 3 case off = 4 - public var description: String { + var description: String { switch self { case .okay: "Connected" case .connecting: "Connecting..." @@ -123,7 +123,7 @@ enum AgentStatus: Int, Equatable, Comparable { } } - public var color: Color { + var color: Color { switch self { case .okay: .green case .high_latency: .yellow @@ -150,15 +150,15 @@ struct Workspace: Identifiable, Equatable, Comparable { struct VPNMenuState { var agents: [UUID: Agent] = [:] var workspaces: [UUID: Workspace] = [:] - // Upserted agents that don't belong to any known workspace, have no FQDNs, - // or have any invalid UUIDs. + /// Upserted agents that don't belong to any known workspace, have no FQDNs, + /// or have any invalid UUIDs. var invalidAgents: [Vpn_Agent] = [] - public func findAgent(workspaceID: UUID, name: String) -> Agent? { + func findAgent(workspaceID: UUID, name: String) -> Agent? { agents.values.first { $0.wsID == workspaceID && $0.name.caseInsensitiveCompare(name) == .orderedSame } } - public func findWorkspace(name: String) -> Workspace? { + func findWorkspace(name: String) -> Workspace? { workspaces.values.first { $0.name.caseInsensitiveCompare(name) == .orderedSame } } diff --git a/Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift b/Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift index 3f325cdb..16f76e1c 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/NetworkExtension.swift @@ -24,7 +24,7 @@ enum NetworkExtensionState: Equatable { /// An actor that handles configuring, enabling, and disabling the VPN tunnel via the /// NetworkExtension APIs. extension CoderVPNService { - // Attempts to load the NetworkExtension configuration, returning true if successful. + /// Attempts to load the NetworkExtension configuration, returning true if successful. func loadNetworkExtensionConfig() async -> Bool { do { let tm = try await getTunnelManager() @@ -126,6 +126,6 @@ extension CoderVPNService { } } -// we're going to mark NETunnelProviderManager as Sendable since there are official APIs that return -// it async. +/// we're going to mark NETunnelProviderManager as Sendable since there are official APIs that return +/// it async. extension NETunnelProviderManager: @unchecked @retroactive Sendable {} diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 2cf10208..6215ca3e 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -37,7 +37,7 @@ enum VPNServiceError: Error, Equatable { case systemExtensionError(SystemExtensionState) case networkExtensionError(NetworkExtensionState) - public var description: String { + var description: String { switch self { case let .internalError(description): "Internal Error: \(description)" @@ -48,7 +48,7 @@ enum VPNServiceError: Error, Equatable { } } - public var localizedDescription: String { description } + var localizedDescription: String { description } } @MainActor @@ -108,9 +108,9 @@ final class CoderVPNService: NSObject, VPNService { var onStart: (() -> Void)? var onFailure: ((VPNServiceError) -> Void)? - // systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get - // garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework - // only stores a weak reference to the delegate. + /// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get + /// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework + /// only stores a weak reference to the delegate. var systemExtnDelegate: SystemExtensionDelegate? var serverAddress: String? @@ -200,7 +200,7 @@ final class CoderVPNService: NSObject, VPNService { } extension CoderVPNService { - public func vpnDidUpdate(_ connection: NETunnelProviderSession) { + func vpnDidUpdate(_ connection: NETunnelProviderSession) { switch (tunnelState, connection.status) { // Any -> Disconnected: Update UI w/ error if present case (_, .disconnected): diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift index c5e4ea08..2bd97dbc 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift @@ -82,10 +82,10 @@ class SystemExtensionDelegate: { private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn-installer") private var asyncDelegate: AsyncDelegate - // The `didFinishWithResult` function is called for both activation, - // deactivation, and replacement requests. The API provides no way to - // differentiate them. https://developer.apple.com/forums/thread/684021 - // This tracks the last request type made, to handle them accordingly. + /// The `didFinishWithResult` function is called for both activation, + /// deactivation, and replacement requests. The API provides no way to + /// differentiate them. https://developer.apple.com/forums/thread/684021 + /// This tracks the last request type made, to handle them accordingly. private var action: SystemExtensionDelegateAction = .none init(asyncDelegate: AsyncDelegate) { @@ -158,7 +158,8 @@ class SystemExtensionDelegate: logger.error("System extension request failed: \(error.localizedDescription)") Task { [asyncDelegate] in await asyncDelegate.recordSystemExtensionState( - .failed(error.localizedDescription)) + .failed(error.localizedDescription) + ) } } diff --git a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift index 3f97aa15..fa7fe148 100644 --- a/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift +++ b/Coder-Desktop/Coder-Desktop/Views/CircularProgressView.swift @@ -98,8 +98,8 @@ extension CircularProgressView { } } -// We note a constant >10% CPU usage when using a SwiftUI rotation animation that -// repeats forever, while this implementation, using Core Animation, uses <1% CPU. +/// We note a constant >10% CPU usage when using a SwiftUI rotation animation that +/// repeats forever, while this implementation, using Core Animation, uses <1% CPU. struct IndeterminateSpinnerView: NSViewRepresentable { var diameter: CGFloat var strokeWidth: CGFloat diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift index e7bdc2b2..e12bfbf8 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift @@ -82,8 +82,8 @@ class FilePickerModel: ObservableObject { @Published private(set) var rootIsLoading: Bool = false @Published private(set) var error: SDKError? - // It's important that `AgentClient` is a reference type (class) - // as we were having performance issues with a struct (unless it was a binding). + /// It's important that `AgentClient` is a reference type (class) + /// as we were having performance issues with a struct (unless it was a binding). let client: AgentClient init(host: String) { @@ -111,15 +111,13 @@ struct FilePickerEntry: View { let showHiddenFiles: Bool var body: some View { - Group { - if entry.dir { - directory - } else { - Label(entry.name, systemImage: "doc") - .help(entry.absolute_path) - .selectionDisabled() - .foregroundColor(.secondary) - } + if entry.dir { + directory + } else { + Label(entry.name, systemImage: "doc") + .help(entry.absolute_path) + .selectionDisabled() + .foregroundColor(.secondary) } } @@ -229,8 +227,8 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject { } } +@MainActor extension LSResponse { - @MainActor func toModels(client: AgentClient) -> [FilePickerEntryModel] { contents.map { entry in FilePickerEntryModel( diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift index c0750567..e4645ae5 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncConfig.swift @@ -15,103 +15,99 @@ struct FileSyncConfig: View { @State private var dontRetry: Bool = false var body: some View { - Group { - Table(fileSync.sessionState, selection: $selection) { - TableColumn("Local Path") { - Text($0.alphaPath).help($0.alphaPath) - }.width(min: 200, ideal: 240) - TableColumn("Workspace", value: \.agentHost) - .width(min: 100, ideal: 120) - TableColumn("Remote Path") { Text($0.betaPath).help($0.betaPath) } - .width(min: 100, ideal: 120) - TableColumn("Status") { $0.status.column.help($0.statusAndErrors) } - .width(min: 80, ideal: 100) - TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) } - .width(min: 60, ideal: 80) + Table(fileSync.sessionState, selection: $selection) { + TableColumn("Local Path") { + Text($0.alphaPath).help($0.alphaPath) + }.width(min: 200, ideal: 240) + TableColumn("Workspace", value: \.agentHost) + .width(min: 100, ideal: 120) + TableColumn("Remote Path") { Text($0.betaPath).help($0.betaPath) } + .width(min: 100, ideal: 120) + TableColumn("Status") { $0.status.column.help($0.statusAndErrors) } + .width(min: 80, ideal: 100) + TableColumn("Size") { Text($0.localSize.humanSizeBytes).help($0.sizeDescription) } + .width(min: 60, ideal: 80) + } + .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { selections in + // TODO: We only support single selections for now + if let selected = selections.first, + let session = fileSync.sessionState.first(where: { $0.id == selected }) + { + Button("Edit") { editingSession = session } + Button(session.status.isResumable ? "Resume" : "Pause") + { Task { await pauseResume(session: session) } } + Button("Reset") { Task { await reset(session: session) } } + Button("Terminate") { Task { await delete(session: session) } } } - .contextMenu(forSelectionType: FileSyncSession.ID.self, menu: { selections in - // TODO: We only support single selections for now - if let selected = selections.first, - let session = fileSync.sessionState.first(where: { $0.id == selected }) - { - Button("Edit") { editingSession = session } - Button(session.status.isResumable ? "Resume" : "Pause") - { Task { await pauseResume(session: session) } } - Button("Reset") { Task { await reset(session: session) } } - Button("Terminate") { Task { await delete(session: session) } } - } - }, - primaryAction: { selectedSessions in - if let session = selectedSessions.first { - editingSession = fileSync.sessionState.first(where: { $0.id == session }) - } - }) - .frame(minWidth: 400, minHeight: 200) - .padding(.bottom, Theme.Size.tableFooterIconSize) - .overlay(alignment: .bottom) { - tableFooter + }, + primaryAction: { selectedSessions in + if let session = selectedSessions.first { + editingSession = fileSync.sessionState.first(where: { $0.id == session }) } - // Only the table & footer should be disabled if the daemon has crashed - // otherwise the alert buttons will be disabled too + }) + .frame(minWidth: 400, minHeight: 200) + .padding(.bottom, Theme.Size.tableFooterIconSize) + .overlay(alignment: .bottom) { + tableFooter }.disabled(fileSync.state.isFailed) - .sheet(isPresented: $addingNewSession) { - FileSyncSessionModal() - .frame(width: 700) - }.sheet(item: $editingSession) { session in - FileSyncSessionModal(existingSession: session) - .frame(width: 700) - }.alert("Error", isPresented: Binding( - get: { actionError != nil }, - set: { isPresented in - if !isPresented { - actionError = nil - } + .sheet(isPresented: $addingNewSession) { + FileSyncSessionModal() + .frame(width: 700) + }.sheet(item: $editingSession) { session in + FileSyncSessionModal(existingSession: session) + .frame(width: 700) + }.alert("Error", isPresented: Binding( + get: { actionError != nil }, + set: { isPresented in + if !isPresented { + actionError = nil } - )) {} message: { - Text(actionError?.description ?? "An unknown error occurred.") - }.alert("Error", isPresented: Binding( - // We only show the alert if the file config window is open - // Users will see the alert symbol on the menu bar to prompt them to - // open it. The requirement on `!loading` prevents the alert from - // re-opening immediately. - get: { !loading && isVisible && fileSync.state.isFailed }, - set: { isPresented in - if !isPresented { - if dontRetry { - dontRetry = false - return - } - loading = true - Task { - await fileSync.tryStart() - loading = false - } + } + )) {} message: { + Text(actionError?.description ?? "An unknown error occurred.") + }.alert("Error", isPresented: Binding( + // We only show the alert if the file config window is open + // Users will see the alert symbol on the menu bar to prompt them to + // open it. The requirement on `!loading` prevents the alert from + // re-opening immediately. + get: { !loading && isVisible && fileSync.state.isFailed }, + set: { isPresented in + if !isPresented { + if dontRetry { + dontRetry = false + return + } + loading = true + Task { + await fileSync.tryStart() + loading = false } } - )) { - Button("Retry") {} - // This gives the user an out if the daemon is crashing on launch, - // they can cancel the alert, and it will reappear if they re-open the - // file sync window. - Button("Cancel", role: .cancel) { - dontRetry = true - } - } message: { - Text(""" - File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened. - """).onAppear { - // Opens the log file in Console - NSWorkspace.shared.open(fileSync.logFile) - } - }.onAppear { - isVisible = true - }.onDisappear { - isVisible = false - // If the failure alert is dismissed without restarting the daemon, - // (by clicking cancel) this makes it clear that the daemon - // is still in a failed state. - }.navigationTitle("Coder File Sync \(fileSync.state.isFailed ? "- Failed" : "")") - .disabled(loading) + } + )) { + Button("Retry") {} + // This gives the user an out if the daemon is crashing on launch, + // they can cancel the alert, and it will reappear if they re-open the + // file sync window. + Button("Cancel", role: .cancel) { + dontRetry = true + } + } message: { + Text(""" + File sync daemon failed. The daemon log file at\n\(fileSync.logFile.path)\nhas been opened. + """).onAppear { + // Opens the log file in Console + NSWorkspace.shared.open(fileSync.logFile) + } + }.onAppear { + isVisible = true + }.onDisappear { + isVisible = false + // If the failure alert is dismissed without restarting the daemon, + // (by clicking cancel) this makes it clear that the daemon + // is still in a failed state. + }.navigationTitle("Coder File Sync \(fileSync.state.isFailed ? "- Failed" : "")") + .disabled(loading) } var tableFooter: some View { @@ -132,34 +128,33 @@ struct FileSyncConfig: View { .fixedSize(horizontal: false, vertical: true) } + @ViewBuilder var sessionControls: some View { - Group { - if let selection { - if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) { - Divider() - Button { Task { await delete(session: selectedSession) } } - label: { - FooterIcon(systemName: "minus") - .help("Terminate") - } - Divider() - Button { Task { await pauseResume(session: selectedSession) } } - label: { - if selectedSession.status.isResumable { - FooterIcon(systemName: "play") - .help("Resume") - } else { - FooterIcon(systemName: "pause") - .help("Pause") - } - } - Divider() - Button { Task { await reset(session: selectedSession) } } - label: { - FooterIcon(systemName: "arrow.clockwise") - .help("Reset") + if let selection { + if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) { + Divider() + Button { Task { await delete(session: selectedSession) } } + label: { + FooterIcon(systemName: "minus") + .help("Terminate") + } + Divider() + Button { Task { await pauseResume(session: selectedSession) } } + label: { + if selectedSession.status.isResumable { + FooterIcon(systemName: "play") + .help("Resume") + } else { + FooterIcon(systemName: "pause") + .help("Pause") } - } + } + Divider() + Button { Task { await reset(session: selectedSession) } } + label: { + FooterIcon(systemName: "arrow.clockwise") + .help("Reset") + } } } } diff --git a/Coder-Desktop/Coder-Desktop/Views/Util.swift b/Coder-Desktop/Coder-Desktop/Views/Util.swift index 8987f54c..af25a915 100644 --- a/Coder-Desktop/Coder-Desktop/Views/Util.swift +++ b/Coder-Desktop/Coder-Desktop/Views/Util.swift @@ -1,7 +1,7 @@ import Combine import SwiftUI -// This is required for inspecting stateful views +/// This is required for inspecting stateful views final class Inspection { let notice = PassthroughSubject() var callbacks = [UInt: (V) -> Void]() diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift index 9e27fa52..79945b1a 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/InvalidAgents.swift @@ -16,8 +16,8 @@ struct InvalidAgentsButton: View { }.buttonStyle(.plain) } - // `.alert` from SwiftUI doesn't play nice when the calling view is in the - // menu bar. + /// `.alert` from SwiftUI doesn't play nice when the calling view is in the + /// menu bar. private func showAlert() { let formattedAgents = vpn.menuState.invalidAgents.map { agent in let agent_id = if let agent_id = UUID(uuidData: agent.id) { diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift index f3b5522f..0a07613b 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift @@ -2,7 +2,7 @@ import CoderSDK import os import SwiftUI -// Each row in the workspaces list is an agent or an offline workspace +/// Each row in the workspaces list is an agent or an offline workspace enum VPNMenuItem: Equatable, Comparable, Identifiable { case agent(Agent) case offlineWorkspace(Workspace) diff --git a/Coder-Desktop/Coder-Desktop/Windows.swift b/Coder-Desktop/Coder-Desktop/Windows.swift index 24a5a9cc..801486d9 100644 --- a/Coder-Desktop/Coder-Desktop/Windows.swift +++ b/Coder-Desktop/Coder-Desktop/Windows.swift @@ -1,13 +1,13 @@ import SwiftUI -// Window IDs +/// Window IDs enum Windows: String { case login case fileSync } extension OpenWindowAction { - // Type-safe wrapper for opening windows that also focuses the new window + /// Type-safe wrapper for opening windows that also focuses the new window func callAsFunction(id: Windows) { appActivate() callAsFunction(id: id.rawValue) diff --git a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift index 02375ef1..f079493e 100644 --- a/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift +++ b/Coder-Desktop/Coder-DesktopHelper/HelperXPCListeners.swift @@ -3,14 +3,14 @@ import Foundation import os import VPNLib -// This listener handles XPC connections from the Coder Desktop System Network -// Extension (`com.coder.Coder-Desktop.VPN`). +/// This listener handles XPC connections from the Coder Desktop System Network +/// Extension (`com.coder.Coder-Desktop.VPN`). class HelperNEXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable { private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperNEXPCServer") private var conns: [NSXPCConnection] = [] - // Hold a reference to the tun file handle - // to prevent it from being closed. + /// Hold a reference to the tun file handle + /// to prevent it from being closed. private var tunFile: FileHandle? override init() { @@ -134,8 +134,8 @@ extension HelperNEXPCServer: HelperNEXPCInterface { } } -// This listener handles XPC connections from the Coder Desktop App -// (`com.coder.Coder-Desktop`). +/// This listener handles XPC connections from the Coder Desktop App +/// (`com.coder.Coder-Desktop`). class HelperAppXPCServer: NSObject, NSXPCListenerDelegate, @unchecked Sendable { private var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "HelperAppXPCServer") private var conns: [NSXPCConnection] = [] diff --git a/Coder-Desktop/Coder-DesktopHelper/Manager.swift b/Coder-Desktop/Coder-DesktopHelper/Manager.swift index 0bcb7c2c..a94bbf92 100644 --- a/Coder-Desktop/Coder-DesktopHelper/Manager.swift +++ b/Coder-Desktop/Coder-DesktopHelper/Manager.swift @@ -17,7 +17,7 @@ actor Manager { private static let binaryName = "coder-darwin-amd64" #endif - // /var/root/Library/Application Support/com.coder.Coder-Desktop/coder-darwin-{arm64,amd64} + /// /var/root/Library/Application Support/com.coder.Coder-Desktop/coder-darwin-{arm64,amd64} private let dest = try? FileManager.default .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent(Bundle.main.bundleIdentifier ?? "com.coder.Coder-Desktop", isDirectory: true) @@ -102,8 +102,7 @@ actor Manager { do { try tunnelDaemon = await TunnelDaemon(binaryPath: dest) { err in Task { try? await NEXPCServerDelegate.cancelProvider(error: - makeNSError(suffix: "TunnelDaemon", desc: "Tunnel daemon: \(err.description)") - ) } + makeNSError(suffix: "TunnelDaemon", desc: "Tunnel daemon: \(err.description)")) } } } catch { throw .tunnelSetup(error) @@ -137,8 +136,7 @@ actor Manager { logger.error("tunnel read loop failed: \(error.localizedDescription, privacy: .public)") try await tunnelDaemon.close() try await NEXPCServerDelegate.cancelProvider(error: - makeNSError(suffix: "Manager", desc: "Tunnel read loop failed: \(error.localizedDescription)") - ) + makeNSError(suffix: "Manager", desc: "Tunnel read loop failed: \(error.localizedDescription)")) return } logger.info("tunnel read loop exited") @@ -207,7 +205,8 @@ actor Manager { } req = telemetryEnricher.enrich(req) } - }) + } + ) } catch { logger.error("rpc failed \(error)") throw .failedRPC(error) @@ -223,7 +222,7 @@ actor Manager { logger.info("startVPN done") } - // Notifies the tunnel that the system has woken from sleep. + /// Notifies the tunnel that the system has woken from sleep. func wake() async { logger.info("sending wake rpc") do { @@ -240,7 +239,8 @@ actor Manager { resp = try await speaker.unaryRPC( .with { msg in msg.stop = .init() - }) + } + ) } catch { throw .failedRPC(error) } @@ -258,8 +258,8 @@ actor Manager { readLoop.cancel() } - // Retrieves the current state of all peers, - // as required when starting the app whilst the network extension is already running + /// Retrieves the current state of all peers, + /// as required when starting the app whilst the network extension is already running func getPeerState() async throws(ManagerError) -> Vpn_PeerUpdate { logger.info("sending peer state request") let resp: Vpn_TunnelMessage @@ -267,7 +267,8 @@ actor Manager { resp = try await speaker.unaryRPC( .with { msg in msg.getPeerUpdate = .init() - }) + } + ) } catch { throw .failedRPC(error) } diff --git a/Coder-Desktop/Coder-DesktopHelper/main.swift b/Coder-Desktop/Coder-DesktopHelper/main.swift index aacdaf4b..453619ac 100644 --- a/Coder-Desktop/Coder-DesktopHelper/main.swift +++ b/Coder-Desktop/Coder-DesktopHelper/main.swift @@ -15,7 +15,7 @@ let appXPCServer = NSXPCListener(machServiceName: helperAppMachServiceName) appXPCServer.delegate = appXPCServerDelegate appXPCServer.resume() -// Nudge the tunnel to rediscover network paths on system wake. +/// Nudge the tunnel to rediscover network paths on system wake. let wakeObserver = SystemWakeObserver { Task { @MainActor in await globalManager?.wake() diff --git a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift index b34fcd5f..a78f635c 100644 --- a/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FilePickerTests.swift @@ -24,19 +24,19 @@ struct FilePickerTests { } @Test - func testLoadError() async throws { + func loadError() async throws { let host = "test-error.coder" let sut = FilePicker(host: host, outputAbsPath: .constant("")) let view = sut - let url = URL(string: "http://\(host):4")! + let url = try #require(URL(string: "http://\(host):4")) let errorMessage = "Connection failed" - Mock( + try Mock( url: url.appendingPathComponent("/api/v0/list-directory"), contentType: .json, statusCode: 500, - data: [.post: errorMessage.data(using: .utf8)!] + data: [.post: #require(errorMessage.data(using: .utf8))] ).register() try await ViewHosting.host(view) { @@ -50,12 +50,12 @@ struct FilePickerTests { } @Test - func testSuccessfulFileLoad() async throws { + func successfulFileLoad() async throws { let host = "test-success.coder" let sut = FilePicker(host: host, outputAbsPath: .constant("")) let view = sut - let url = URL(string: "http://\(host):4")! + let url = try #require(URL(string: "http://\(host):4")) try Mock( url: url.appendingPathComponent("/api/v0/list-directory"), @@ -78,7 +78,7 @@ struct FilePickerTests { } @Test - func testHiddenFilesToggle() async throws { + func hiddenFilesToggle() async throws { let key = "ShowHiddenFilesInRemoteFilePicker" UserDefaults.standard.removeObject(forKey: key) defer { UserDefaults.standard.removeObject(forKey: key) } @@ -96,7 +96,7 @@ struct FilePickerTests { let sut = FilePicker(host: host, outputAbsPath: .constant("")) let view = sut - let url = URL(string: "http://\(host):4")! + let url = try #require(URL(string: "http://\(host):4")) try Mock( url: url.appendingPathComponent("/api/v0/list-directory"), @@ -135,12 +135,12 @@ struct FilePickerTests { } @Test - func testDirectoryExpansion() async throws { + func directoryExpansion() async throws { let host = "test-expansion.coder" let sut = FilePicker(host: host, outputAbsPath: .constant("")) let view = sut - let url = URL(string: "http://\(host):4")! + let url = try #require(URL(string: "http://\(host):4")) try Mock( url: url.appendingPathComponent("/api/v0/list-directory"), @@ -161,7 +161,7 @@ struct FilePickerTests { // Disclosure group should expand out to 3 more directories #expect(await eventually { @MainActor in - return view.findAll(ViewType.DisclosureGroup.self).count == 6 + view.findAll(ViewType.DisclosureGroup.self).count == 6 }) } } diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift index 85c0bcfa..39f10ee1 100644 --- a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift @@ -16,7 +16,7 @@ class FileSyncDaemonTests { let mutagenAlphaDirectory: URL let mutagenBetaDirectory: URL - // Before each test + /// Before each test init() throws { tempDir = FileManager.default.makeTempDir()! #if arch(arm64) @@ -32,7 +32,7 @@ class FileSyncDaemonTests { try FileManager.default.createDirectory(at: mutagenBetaDirectory, withIntermediateDirectories: true) } - // After each test + /// After each test deinit { try? FileManager.default.removeItem(at: tempDir) } @@ -91,10 +91,11 @@ class FileSyncDaemonTests { try "Hello, World!".write(to: alphaFile, atomically: true, encoding: .utf8) #expect( await eventually(timeout: .seconds(5), interval: .milliseconds(100)) { @MainActor in - return FileManager.default.fileExists( + FileManager.default.fileExists( atPath: self.mutagenBetaDirectory.appending(path: "test.txt").path() ) - }) + } + ) try await daemon.deleteSessions(ids: daemon.sessionState.map(\.id)) #expect(daemon.sessionState.count == 0) @@ -160,8 +161,7 @@ class FileSyncDaemonTests { path: mutagenBetaDirectory.path(), protocolKind: .local ) - ) - ) + )) #expect(statesEqual(daemon1.state, .running)) #expect(daemon1.sessionState.count == 1) diff --git a/Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift b/Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift index 85716eb1..ec039863 100644 --- a/Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/LiteralHeadersSettingTests.swift @@ -19,7 +19,7 @@ struct LiteralHeadersSettingTests { } @Test - func testToggleDisabledWhenVPNEnabled() async throws { + func toggleDisabledWhenVPNEnabled() async throws { vpn.state = .connected try await ViewHosting.host(view) { @@ -32,7 +32,7 @@ struct LiteralHeadersSettingTests { } @Test - func testToggleEnabledWhenVPNDisabled() async throws { + func toggleEnabledWhenVPNDisabled() async throws { vpn.state = .disabled try await ViewHosting.host(view) { diff --git a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift index 78f34d9b..311010b2 100644 --- a/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/LoginFormTests.swift @@ -22,7 +22,7 @@ struct LoginTests { } @Test - func testInitialView() async throws { + func initialView() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in #expect(throws: Never.self) { try view.find(text: "Coder Desktop") } @@ -33,7 +33,7 @@ struct LoginTests { } @Test - func testInvalidServerURL() async throws { + func invalidServerURL() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("http://") @@ -44,7 +44,7 @@ struct LoginTests { } @Test - func testValidServerURL() async throws { + func validServerURL() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") @@ -58,7 +58,7 @@ struct LoginTests { } @Test - func testBackButton() async throws { + func backButton() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in try view.find(ViewType.TextField.self).setInput("https://coder.example.com") @@ -72,8 +72,8 @@ struct LoginTests { } @Test - func testFailedAuthentication() async throws { - let url = URL(string: "https://testFailedAuthentication.com")! + func failedAuthentication() async throws { + let url = try #require(URL(string: "https://testFailedAuthentication.com")) let buildInfo = BuildInfoResponse( version: "v2.20.0" ) @@ -97,8 +97,8 @@ struct LoginTests { } @Test - func testOutdatedServer() async throws { - let url = URL(string: "https://testOutdatedServer.com")! + func outdatedServer() async throws { + let url = try #require(URL(string: "https://testOutdatedServer.com")) let buildInfo = BuildInfoResponse( version: "v2.19.0" ) @@ -127,8 +127,8 @@ struct LoginTests { } @Test - func testSuccessfulLogin() async throws { - let url = URL(string: "https://testSuccessfulLogin.com")! + func successfulLogin() async throws { + let url = try #require(URL(string: "https://testSuccessfulLogin.com")) let user = User( id: UUID(), diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift index dd9acf52..5821deac 100644 --- a/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuStateTests.swift @@ -3,12 +3,11 @@ import Testing @testable import VPNLib @MainActor -@Suite struct VPNMenuStateTests { var state = VPNMenuState() @Test - mutating func testUpsertAgent_addsAgent() async throws { + mutating func upsertAgent_addsAgent() throws { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -39,7 +38,7 @@ struct VPNMenuStateTests { } @Test - mutating func testDeleteAgent_removesAgent() async throws { + mutating func deleteAgent_removesAgent() { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -59,7 +58,7 @@ struct VPNMenuStateTests { } @Test - mutating func testDeleteWorkspace_removesWorkspaceAndAgents() async throws { + mutating func deleteWorkspace_removesWorkspaceAndAgents() { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -80,7 +79,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_poorConnection() async throws { + mutating func upsertAgent_poorConnection() throws { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -103,7 +102,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_connecting() async throws { + mutating func upsertAgent_connecting() throws { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -123,7 +122,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_unhealthyAgent() async throws { + mutating func upsertAgent_unhealthyAgent() throws { let agentID = UUID() let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "foo" }) @@ -143,7 +142,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_replacesOldAgent() async throws { + mutating func upsertAgent_replacesOldAgent() throws { let workspaceID = UUID() let oldAgentID = UUID() let newAgentID = UUID() @@ -184,7 +183,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertWorkspace_addsOfflineWorkspace() async throws { + mutating func upsertWorkspace_addsOfflineWorkspace() throws { let workspaceID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "Foo" }) @@ -226,7 +225,7 @@ struct VPNMenuStateTests { } @Test - mutating func testFind_ignoresNameCase() async throws { + mutating func find_ignoresNameCase() { let workspaceID = UUID() let agentID = UUID() state.upsertWorkspace(Vpn_Workspace.with { $0.id = workspaceID.uuidData; $0.name = "My-Workspace" }) @@ -243,7 +242,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_invalidAgent_noUUID() async throws { + mutating func upsertAgent_invalidAgent_noUUID() { let agent = Vpn_Agent.with { $0.name = "invalidAgent" $0.fqdn = ["invalid.coder"] @@ -256,7 +255,7 @@ struct VPNMenuStateTests { } @Test - mutating func testUpsertAgent_outOfOrder() async throws { + mutating func upsertAgent_outOfOrder() { let agentID = UUID() let workspaceID = UUID() @@ -275,7 +274,7 @@ struct VPNMenuStateTests { } @Test - mutating func testDeleteInvalidAgent_removesInvalid() async throws { + mutating func deleteInvalidAgent_removesInvalid() { let agentID = UUID() let workspaceID = UUID() diff --git a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift index 322952bf..6ddeb49c 100644 --- a/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/VPNMenuTests.swift @@ -21,7 +21,7 @@ struct VPNMenuTests { } @Test - func testVPNLoggedOut() async throws { + func vPNLoggedOut() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in let toggle = try view.find(ViewType.Toggle.self) @@ -33,7 +33,7 @@ struct VPNMenuTests { } @Test - func testVPNLoggedOutUnconfigured() async throws { + func vPNLoggedOutUnconfigured() async throws { vpn.state = .failed(.networkExtensionError(.unconfigured)) try await ViewHosting.host(view) { try await sut.inspection.inspect { view in @@ -48,7 +48,7 @@ struct VPNMenuTests { } @Test - func testStartStopCalled() async throws { + func startStopCalled() async throws { try await ViewHosting.host(view) { try await sut.inspection.inspect { view in var toggle = try view.find(ViewType.Toggle.self) @@ -72,9 +72,9 @@ struct VPNMenuTests { } @Test - func testVPNDisabledWhileConnecting() async throws { + func vPNDisabledWhileConnecting() async throws { vpn.state = .disabled - state.login(baseAccessURL: URL(string: "https://coder.example.com")!, sessionToken: "fake-token") + try state.login(baseAccessURL: #require(URL(string: "https://coder.example.com")), sessionToken: "fake-token") try await ViewHosting.host(view) { try await sut.inspection.inspect { view in @@ -93,9 +93,9 @@ struct VPNMenuTests { } @Test - func testVPNDisabledWhileDisconnecting() async throws { + func vPNDisabledWhileDisconnecting() async throws { vpn.state = .disabled - state.login(baseAccessURL: URL(string: "https://coder.example.com")!, sessionToken: "fake-token") + try state.login(baseAccessURL: #require(URL(string: "https://coder.example.com")), sessionToken: "fake-token") try await ViewHosting.host(view) { try await sut.inspection.inspect { view in @@ -120,8 +120,8 @@ struct VPNMenuTests { } @Test - func testOffWhenFailed() async throws { - state.login(baseAccessURL: URL(string: "https://coder.example.com")!, sessionToken: "fake-token") + func offWhenFailed() async throws { + try state.login(baseAccessURL: #require(URL(string: "https://coder.example.com")), sessionToken: "fake-token") try await ViewHosting.host(view) { try await sut.inspection.inspect { view in diff --git a/Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift b/Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift index abad6abd..2381e344 100644 --- a/Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/VPNStateTests.swift @@ -20,7 +20,7 @@ struct VPNStateTests { } @Test - func testDisabledState() async throws { + func disabledState() async throws { vpn.state = .disabled try await ViewHosting.host(view) { @@ -33,7 +33,7 @@ struct VPNStateTests { } @Test - func testConnectingState() async throws { + func connectingState() async throws { vpn.state = .connecting try await ViewHosting.host(view) { @@ -44,7 +44,7 @@ struct VPNStateTests { } @Test - func testDisconnectingState() async throws { + func disconnectingState() async throws { vpn.state = .disconnecting try await ViewHosting.host(view) { @@ -55,7 +55,7 @@ struct VPNStateTests { } @Test - func testFailedState() async throws { + func failedState() async throws { let errMsg = "Internal error occured!" vpn.state = .failed(.internalError(errMsg)) @@ -68,7 +68,7 @@ struct VPNStateTests { } @Test - func testDefaultState() async throws { + func defaultState() async throws { vpn.state = .connected try await ViewHosting.host(view.environmentObject(vpn)) { diff --git a/Coder-Desktop/Coder-DesktopTests/WorkspaceAppTests.swift b/Coder-Desktop/Coder-DesktopTests/WorkspaceAppTests.swift index 599d4a4e..dc6e92e9 100644 --- a/Coder-Desktop/Coder-DesktopTests/WorkspaceAppTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/WorkspaceAppTests.swift @@ -4,7 +4,6 @@ import os import Testing @MainActor -@Suite struct WorkspaceAppTests { let logger = Logger(subsystem: "com.coder.Coder-Desktop-Tests", category: "WorkspaceAppTests") let baseAccessURL = URL(string: "https://coder.example.com")! @@ -15,15 +14,15 @@ struct WorkspaceAppTests { } @Test - func testCreateWorkspaceApp_Success() throws { - let sdkApp = CoderSDK.WorkspaceApp( + func createWorkspaceApp_Success() throws { + let sdkApp = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "vscode://myworkspace.coder/foo")!, + url: #require(URL(string: "vscode://myworkspace.coder/foo")) as URL, external: true, slug: "test-app", display_name: "Test App", command: nil, - icon: URL(string: "/icon/test-app.svg")!, + icon: #require(URL(string: "/icon/test-app.svg")) as URL, subdomain: false, subdomain_name: nil ) @@ -41,15 +40,15 @@ struct WorkspaceAppTests { } @Test - func testCreateWorkspaceApp_SessionTokenReplacement() throws { + func createWorkspaceApp_SessionTokenReplacement() throws { let sdkApp = try CoderSDK.WorkspaceApp( id: UUID(), - url: #require(URL(string: "vscode://coder.coder-remote/open?url=https://coder.example.com&token=$SESSION_TOKEN")), + url: #require(URL(string: "vscode://coder.coder-remote/open?url=https://coder.example.com&token=$SESSION_TOKEN")) as URL, external: true, slug: "token-app", display_name: "Token App", command: nil, - icon: #require(URL(string: "/icon/test-app.svg")), + icon: #require(URL(string: "/icon/test-app.svg")) as URL, subdomain: false, subdomain_name: nil ) @@ -68,7 +67,7 @@ struct WorkspaceAppTests { } @Test - func testCreateWorkspaceApp_MissingURL() throws { + func createWorkspaceApp_MissingURL() throws { let sdkApp = CoderSDK.WorkspaceApp( id: UUID(), url: nil, @@ -90,10 +89,10 @@ struct WorkspaceAppTests { } @Test - func testCreateWorkspaceApp_CommandApp() throws { - let sdkApp = CoderSDK.WorkspaceApp( + func createWorkspaceApp_CommandApp() throws { + let sdkApp = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "vscode://myworkspace.coder/foo")!, + url: #require(URL(string: "vscode://myworkspace.coder/foo")) as URL, external: true, slug: "command-app", display_name: "Command App", @@ -112,7 +111,7 @@ struct WorkspaceAppTests { } @Test - func testDisplayApps_VSCode() throws { + func displayApps_VSCode() { let agent = createMockAgent(displayApps: [.vscode, .web_terminal, .ssh_helper, .port_forwarding_helper]) let apps = agentToApps(logger, agent, host, session) @@ -126,7 +125,7 @@ struct WorkspaceAppTests { } @Test - func testDisplayApps_VSCodeInsiders() throws { + func displayApps_VSCodeInsiders() { let agent = createMockAgent( displayApps: [ .vscode_insiders, @@ -150,15 +149,15 @@ struct WorkspaceAppTests { } @Test - func testCreateWorkspaceApp_WebAppFilter() throws { - let sdkApp = CoderSDK.WorkspaceApp( + func createWorkspaceApp_WebAppFilter() throws { + let sdkApp = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "https://myworkspace.coder/foo")!, + url: #require(URL(string: "https://myworkspace.coder/foo")) as URL, external: false, slug: "web-app", display_name: "Web App", command: nil, - icon: URL(string: "/icon/web-app.svg")!, + icon: #require(URL(string: "/icon/web-app.svg")) as URL, subdomain: false, subdomain_name: nil ) @@ -172,35 +171,35 @@ struct WorkspaceAppTests { } @Test - func testAgentToApps_MultipleApps() throws { - let sdkApp1 = CoderSDK.WorkspaceApp( + func agentToApps_MultipleApps() throws { + let sdkApp1 = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "vscode://myworkspace.coder/foo1")!, + url: #require(URL(string: "vscode://myworkspace.coder/foo1")) as URL, external: true, slug: "app1", display_name: "App 1", command: nil, - icon: URL(string: "/icon/foo1.svg")!, + icon: #require(URL(string: "/icon/foo1.svg")) as URL, subdomain: false, subdomain_name: nil ) - let sdkApp2 = CoderSDK.WorkspaceApp( + let sdkApp2 = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "jetbrains://myworkspace.coder/foo2")!, + url: #require(URL(string: "jetbrains://myworkspace.coder/foo2")) as URL, external: true, slug: "app2", display_name: "App 2", command: nil, - icon: URL(string: "/icon/foo2.svg")!, + icon: #require(URL(string: "/icon/foo2.svg")) as URL, subdomain: false, subdomain_name: nil ) // Command app; skipped - let sdkApp3 = CoderSDK.WorkspaceApp( + let sdkApp3 = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "vscode://myworkspace.coder/foo3")!, + url: #require(URL(string: "vscode://myworkspace.coder/foo3")) as URL, external: true, slug: "app3", display_name: "App 3", @@ -211,14 +210,14 @@ struct WorkspaceAppTests { ) // Web app skipped - let sdkApp4 = CoderSDK.WorkspaceApp( + let sdkApp4 = try CoderSDK.WorkspaceApp( id: UUID(), - url: URL(string: "https://myworkspace.coder/foo4")!, + url: #require(URL(string: "https://myworkspace.coder/foo4")) as URL, external: true, slug: "app4", display_name: "App 4", command: nil, - icon: URL(string: "/icon/foo4.svg")!, + icon: #require(URL(string: "/icon/foo4.svg")) as URL, subdomain: false, subdomain_name: nil ) @@ -249,7 +248,7 @@ struct WorkspaceAppTests { extension WorkspaceAppTests { private func externalApp(_ url: String) throws -> CoderSDK.WorkspaceApp { try CoderSDK.WorkspaceApp( - id: UUID(), url: #require(URL(string: url)), external: true, slug: "app", display_name: "App", + id: UUID(), url: #require(URL(string: url)) as URL, external: true, slug: "app", display_name: "App", command: nil, icon: nil, subdomain: false, subdomain_name: nil ) } @@ -269,7 +268,7 @@ extension WorkspaceAppTests { let sdkApp = try externalApp("vscode://coder.coder-remote/open?url=https://dogfood.example&token=$SESSION_TOKEN") let session = try CoderSession( baseAccessURL: baseAccessURL, - dashboardURL: #require(URL(string: "https://dogfood.example/")), + dashboardURL: #require(URL(string: "https://dogfood.example/")) as URL, sessionToken: sessionToken ) diff --git a/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift index 4b563d24..b0631a64 100644 --- a/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift +++ b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITests.swift @@ -16,14 +16,14 @@ final class Coder_DesktopUITests: XCTestCase { } @MainActor - func testStatusItemExists() throws { + func testStatusItemExists() { let app = XCUIApplication() app.launch() app.statusItems.firstMatch.tap() } @MainActor - func testLaunchPerformance() throws { + func testLaunchPerformance() { if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { // This measures how long it takes to launch your application. measure(metrics: [XCTApplicationLaunchMetric()]) { diff --git a/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift index 27b4832b..ba805b1f 100644 --- a/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift +++ b/Coder-Desktop/Coder-DesktopUITests/Coder_DesktopUITestsLaunchTests.swift @@ -10,7 +10,7 @@ final class Coder_DesktopUITestsLaunchTests: XCTestCase { } @MainActor - func testLaunch() throws { + func testLaunch() { let app = XCUIApplication() app.launch() diff --git a/Coder-Desktop/CoderSDK/AgentLS.swift b/Coder-Desktop/CoderSDK/AgentLS.swift index 0d9a2bc3..f104c58f 100644 --- a/Coder-Desktop/CoderSDK/AgentLS.swift +++ b/Coder-Desktop/CoderSDK/AgentLS.swift @@ -9,10 +9,10 @@ public extension AgentClient { } public struct LSRequest: Sendable, Codable { - // e.g. [], ["repos", "coder"] + /// e.g. [], ["repos", "coder"] public let path: [String] - // Whether the supplied path is relative to the user's home directory, - // or the root directory. + /// Whether the supplied path is relative to the user's home directory, + /// or the root directory. public let relativity: LSRelativity public init(path: [String], relativity: LSRelativity) { diff --git a/Coder-Desktop/CoderSDK/Client.swift b/Coder-Desktop/CoderSDK/Client.swift index 233c2aea..5f5731be 100644 --- a/Coder-Desktop/CoderSDK/Client.swift +++ b/Coder-Desktop/CoderSDK/Client.swift @@ -203,7 +203,7 @@ func responseAsError(_ resp: HTTPResponse) -> SDKError { } } -// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`. +/// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`. func decode(_: T.Type, from data: Data) throws(SDKError) -> T { do { return try decoder.decode(T.self, from: data) diff --git a/Coder-Desktop/CoderSDK/Deployment.swift b/Coder-Desktop/CoderSDK/Deployment.swift index e5178842..32d65a92 100644 --- a/Coder-Desktop/CoderSDK/Deployment.swift +++ b/Coder-Desktop/CoderSDK/Deployment.swift @@ -19,7 +19,7 @@ public struct BuildInfoResponse: Codable, Equatable, Sendable { self.dashboard_url = dashboard_url } - // `version` in the form `[0-9]+.[0-9]+.[0-9]+` + /// `version` in the form `[0-9]+.[0-9]+.[0-9]+` public var semver: String? { try? NSRegularExpression(pattern: #"v(\d+\.\d+\.\d+)"#) .firstMatch(in: version, range: NSRange(version.startIndex ..< version.endIndex, in: version)) diff --git a/Coder-Desktop/CoderSDKTests/CoderSDKTests.swift b/Coder-Desktop/CoderSDKTests/CoderSDKTests.swift index ba4194c5..f87bbe43 100644 --- a/Coder-Desktop/CoderSDKTests/CoderSDKTests.swift +++ b/Coder-Desktop/CoderSDKTests/CoderSDKTests.swift @@ -12,7 +12,7 @@ struct CoderSDKTests { username: "johndoe" ) - let url = URL(string: "https://example.com")! + let url = try #require(URL(string: "https://example.com")) let token = "fake-token" let client = Client(url: url, token: token, headers: [.init(name: "X-Test-Header", value: "foo")]) var mock = try Mock( @@ -39,7 +39,7 @@ struct CoderSDKTests { version: "v2.18.2-devel+630fd7c0a" ) - let url = URL(string: "https://example.com")! + let url = try #require(URL(string: "https://example.com")) let client = Client(url: url) try Mock( url: url.appending(path: "api/v2/buildinfo"), diff --git a/Coder-Desktop/CoderSDKTests/UserAgentTests.swift b/Coder-Desktop/CoderSDKTests/UserAgentTests.swift index 7e9f46ed..25a32ef4 100644 --- a/Coder-Desktop/CoderSDKTests/UserAgentTests.swift +++ b/Coder-Desktop/CoderSDKTests/UserAgentTests.swift @@ -5,7 +5,7 @@ import Testing @Suite(.timeLimit(.minutes(1))) struct UserAgentTests { - // The pattern operators are expected to match against every Coder client. + /// The pattern operators are expected to match against every Coder client. static let grammar = #"^coder-desktop(-core)?/[0-9]+\.[0-9]+\.[0-9]+ \(darwin/(amd64|arm64)\)$"# static let stubVersion = "0.8.4" @@ -27,7 +27,7 @@ struct UserAgentTests { ) } - // The CLI and the vpn-daemon report `amd64`, so an Intel build must too. + /// The CLI and the vpn-daemon report `amd64`, so an Intel build must too. @Test func intelReportsGoArchNotSwiftArch() { let ua = userAgent(component: .app, version: Self.stubVersion, arch: .amd64) @@ -64,7 +64,7 @@ struct UserAgentTests { func setOnRequest() async throws { // A distinct host per test: Mocker's registry is process-global and // suites run in parallel. - let url = URL(string: "https://ua-default.example.com")! + let url = try #require(URL(string: "https://ua-default.example.com")) let client = Client(url: url, component: .helper) let sentUA = try await capturedUserAgent(client: client, url: url) @@ -72,11 +72,11 @@ struct UserAgentTests { #expect(sentUA.hasPrefix("coder-desktop-core/")) } - // A user-configured literal header must replace ours outright. Caller headers - // are applied with `addValue`, which would otherwise comma-join the two. + /// A user-configured literal header must replace ours outright. Caller headers + /// are applied with `addValue`, which would otherwise comma-join the two. @Test func callerSuppliedUserAgentWins() async throws { - let url = URL(string: "https://ua-override.example.com")! + let url = try #require(URL(string: "https://ua-override.example.com")) let custom = "my-own-agent/1.2.3" let client = Client( url: url, diff --git a/Coder-Desktop/VPN/NEHelperXPCClient.swift b/Coder-Desktop/VPN/NEHelperXPCClient.swift index 2d1d77a6..b90aab75 100644 --- a/Coder-Desktop/VPN/NEHelperXPCClient.swift +++ b/Coder-Desktop/VPN/NEHelperXPCClient.swift @@ -94,7 +94,7 @@ final class HelperXPCClient: @unchecked Sendable { } } -// These methods are called over XPC by the helper. +/// These methods are called over XPC by the helper. extension HelperXPCClient: NEXPCInterface { func applyTunnelNetworkSettings(diff: Data, reply: @escaping () -> Void) { let reply = CompletionWrapper(reply) diff --git a/Coder-Desktop/VPN/PacketTunnelProvider.swift b/Coder-Desktop/VPN/PacketTunnelProvider.swift index 20bb02f1..3222cdb5 100644 --- a/Coder-Desktop/VPN/PacketTunnelProvider.swift +++ b/Coder-Desktop/VPN/PacketTunnelProvider.swift @@ -3,12 +3,12 @@ import NetworkExtension import os import VPNLib -/* From */ +/** From */ let CTLIOCGINFO: UInt = 0xC064_4E03 class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "provider") - // a `tunnelRemoteAddress` is required, but not currently used. + /// a `tunnelRemoteAddress` is required, but not currently used. private var currentSettings: NEPacketTunnelNetworkSettings = .init(tunnelRemoteAddress: "127.0.0.1") var tunnelFileDescriptor: Int32? { @@ -90,7 +90,7 @@ class PacketTunnelProvider: NEPacketTunnelProvider, @unchecked Sendable { } } - // Wrapper around `setTunnelNetworkSettings` that supports merging updates + /// Wrapper around `setTunnelNetworkSettings` that supports merging updates func applyTunnelNetworkSettings(_ diff: Vpn_NetworkSettingsRequest) async throws { logger.debug("applying settings diff: \(diff.debugDescription, privacy: .public)") diff --git a/Coder-Desktop/VPNLib/CoderRouter.swift b/Coder-Desktop/VPNLib/CoderRouter.swift index d562e39e..a140934a 100644 --- a/Coder-Desktop/VPNLib/CoderRouter.swift +++ b/Coder-Desktop/VPNLib/CoderRouter.swift @@ -1,8 +1,8 @@ import Foundation import URLRouting -// This is in VPNLib to avoid depending on `swift-collections` in both the app & extension. -// https://github.com/coder/coder-desktop-macos/issues/149 +/// This is in VPNLib to avoid depending on `swift-collections` in both the app & extension. +/// https://github.com/coder/coder-desktop-macos/issues/149 public struct CoderRouter: ParserPrinter { public init() {} @@ -82,8 +82,8 @@ public enum OpenRoute: Equatable, Sendable { case rdp(RDPCredentials) } -// Due to a Swift Result builder limitation, we can't flatten this out to `case rdp(String?, String?)` -// https://github.com/pointfreeco/swift-url-routing/issues/50 +/// Due to a Swift Result builder limitation, we can't flatten this out to `case rdp(String?, String?)` +/// https://github.com/pointfreeco/swift-url-routing/issues/50 public struct RDPCredentials: Equatable, Sendable { public let username: String? public let password: String? diff --git a/Coder-Desktop/VPNLib/Download.swift b/Coder-Desktop/VPNLib/Download.swift index 680bb3bb..d3d8185a 100644 --- a/Coder-Desktop/VPNLib/Download.swift +++ b/Coder-Desktop/VPNLib/Download.swift @@ -48,8 +48,8 @@ public enum DownloadError: Error { public var localizedDescription: String { description } } -// The async `URLSession.download` api ignores the passed-in delegate, so we -// wrap the older delegate methods in an async adapter with a continuation. +/// The async `URLSession.download` api ignores the passed-in delegate, so we +/// wrap the older delegate methods in an async adapter with a continuation. private final class DownloadManager: NSObject, @unchecked Sendable { private var continuation: CheckedContinuation! private var progressHandler: ((DownloadProgress) -> Void)? @@ -68,10 +68,10 @@ private final class DownloadManager: NSObject, @unchecked Sendable { for header in headers { req.addValue(header.value, forHTTPHeaderField: header.name) } - if FileManager.default.fileExists(atPath: dest.path) { - if let existingFileData = try? Data(contentsOf: dest, options: .mappedIfSafe) { - req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match") - } + if FileManager.default.fileExists(atPath: dest.path), + let existingFileData = try? Data(contentsOf: dest, options: .mappedIfSafe) + { + req.setValue(etag(data: existingFileData), forHTTPHeaderField: "If-None-Match") } let downloadTask = urlSession.downloadTask(with: req) @@ -92,7 +92,7 @@ private final class DownloadManager: NSObject, @unchecked Sendable { } extension DownloadManager: URLSessionDownloadDelegate { - // Progress + /// Progress func urlSession( _: URLSession, downloadTask: URLSessionDownloadTask, @@ -106,7 +106,7 @@ extension DownloadManager: URLSessionDownloadDelegate { progressHandler?(.init(totalBytesWritten: totalBytesWritten, totalBytesToWrite: maybeLength)) } - // Completion + /// Completion func urlSession(_: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { guard let httpResponse = downloadTask.response as? HTTPURLResponse else { continuation.resume(throwing: DownloadError.invalidResponse) @@ -141,7 +141,7 @@ extension DownloadManager: URLSessionDownloadDelegate { continuation.resume() } - // Failure + /// Failure func urlSession(_: URLSession, task _: URLSessionTask, didCompleteWithError error: Error?) { if let error { continuation.resume(throwing: error) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index d4b36065..afaf5c58 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -24,8 +24,8 @@ public protocol FileSyncDaemon: ObservableObject { func resetSessions(ids: [String]) async throws(DaemonError) } -// File Sync related code is in VPNLib to workaround a linking issue -// https://github.com/coder/coder-desktop-macos/issues/149 +/// File Sync related code is in VPNLib to workaround a linking issue +/// https://github.com/coder/coder-desktop-macos/issues/149 @MainActor public class MutagenDaemon: FileSyncDaemon { let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "mutagen") @@ -52,7 +52,7 @@ public class MutagenDaemon: FileSyncDaemon { public let logFile: URL - // Managing sync sessions could take a while, especially with prompting + /// Managing sync sessions could take a while, especially with prompting let sessionMgmtReqTimeout: TimeAmount = .seconds(15) // Non-nil when the daemon is running @@ -61,7 +61,7 @@ public class MutagenDaemon: FileSyncDaemon { private var channel: GRPCChannel? private var waitForExit: (@Sendable () async -> Void)? - // Protect start & stop transitions against re-entrancy + /// Protect start & stop transitions against re-entrancy private let transition = AsyncSemaphore(value: 1) public init(mutagenPath: URL? = nil, @@ -148,8 +148,8 @@ public class MutagenDaemon: FileSyncDaemon { ) } - // The daemon takes a moment to open the socket, and we don't want to hog the main actor - // so poll for it on a background thread + /// The daemon takes a moment to open the socket, and we don't want to hog the main actor + /// so poll for it on a background thread private func waitForDaemonStart( maxAttempts: Int = 5, attemptInterval: Duration = .milliseconds(100) @@ -334,8 +334,8 @@ public enum DaemonState { } } - // `if case`s are a pain to work with: they're not bools (such as for ORing) - // and you can't negate them without doing `if case .. {} else`. + /// `if case`s are a pain to work with: they're not bools (such as for ORing) + /// and you can't negate them without doing `if case .. {} else`. public var isFailed: Bool { if case .failed = self { return true diff --git a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift index b422d86a..f2349938 100644 --- a/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift +++ b/Coder-Desktop/VPNLib/FileSync/MutagenConvert.swift @@ -93,7 +93,7 @@ extension Prompting_HostResponse { } } -// Translated from `cmd/mutagen/sync/list_monitor_common.go` +/// Translated from `cmd/mutagen/sync/list_monitor_common.go` func formatConflicts(conflicts: [Core_Conflict], excludedConflicts: UInt64) -> String { var result = "" for (i, conflict) in conflicts.enumerated() { diff --git a/Coder-Desktop/VPNLib/Speaker+Wake.swift b/Coder-Desktop/VPNLib/Speaker+Wake.swift index bb039e00..32211663 100644 --- a/Coder-Desktop/VPNLib/Speaker+Wake.swift +++ b/Coder-Desktop/VPNLib/Speaker+Wake.swift @@ -10,7 +10,8 @@ public extension Speaker where SendMsg == Vpn_ManagerMessage, RecvMsg == Vpn_Tun _ = try await unaryRPC( .with { msg in msg.wake = .init() - }) + } + ) } catch { throw .failedRPC(error) } diff --git a/Coder-Desktop/VPNLib/Speaker.swift b/Coder-Desktop/VPNLib/Speaker.swift index e2f60f55..1cc9842f 100644 --- a/Coder-Desktop/VPNLib/Speaker.swift +++ b/Coder-Desktop/VPNLib/Speaker.swift @@ -42,10 +42,7 @@ public struct ProtoVersion: CustomStringConvertible, Equatable, Comparable, Coda if parts.count != 2 { throw .invalidVersion(str) } - guard let major = parts[0] else { - throw .invalidVersion(str) - } - guard let minor = parts[1] else { + guard let major = parts[0], let minor = parts[1] else { throw .invalidVersion(str) } self.major = major @@ -167,7 +164,8 @@ extension Speaker: AsyncSequence, AsyncIteratorProtocol { try await secretary.route(reply: msg) } catch { logger.error( - "couldn't route RPC reply for \(msg.rpc.responseTo): \(error)") + "couldn't route RPC reply for \(msg.rpc.responseTo): \(error)" + ) } continue } @@ -239,7 +237,7 @@ actor Handshaker { } } - // resumes must only ever throw HandshakeError + /// resumes must only ever throw HandshakeError private func handleRead(_: Bool, _ data: DispatchData?, _ error: Int32) { guard error == 0 else { let errStrPtr = strerror(error) diff --git a/Coder-Desktop/VPNLib/TunnelDaemon.swift b/Coder-Desktop/VPNLib/TunnelDaemon.swift index f7a5118a..b21640f4 100644 --- a/Coder-Desktop/VPNLib/TunnelDaemon.swift +++ b/Coder-Desktop/VPNLib/TunnelDaemon.swift @@ -70,7 +70,7 @@ public actor TunnelDaemon { deinit { logger.debug("tunnel daemon deinit") } - // This could be an isolated deinit in Swift 6.1 + /// This could be an isolated deinit in Swift 6.1 public func close() throws(TunnelDaemonError) { state = .stopped diff --git a/Coder-Desktop/VPNLib/Validate.swift b/Coder-Desktop/VPNLib/Validate.swift index 8fbf40bd..ea990272 100644 --- a/Coder-Desktop/VPNLib/Validate.swift +++ b/Coder-Desktop/VPNLib/Validate.swift @@ -39,14 +39,14 @@ public enum ValidationError: Error { } public class Validator { - // This version of the app has a strict version requirement. + /// This version of the app has a strict version requirement. public static let minimumCoderVersion = "2.24.3" private static let expectedIdentifier = "com.coder.cli" - // The Coder team identifier + /// The Coder team identifier private static let expectedTeamIdentifier = "4399GN35BJ" - // Apple-issued certificate chain + /// Apple-issued certificate chain public static let anchorRequirement = "anchor apple generic" private static let signInfoFlags: SecCSFlags = .init(rawValue: kSecCSSigningInformation) @@ -94,8 +94,8 @@ public class Validator { } } - // This function executes the binary to read its version, and so it assumes - // the signature has already been validated. + /// This function executes the binary to read its version, and so it assumes + /// the signature has already been validated. public static func validateVersion(binaryPath: URL, serverVersion: String) async throws(ValidationError) { guard FileManager.default.fileExists(atPath: binaryPath.path) else { throw .fileNotFound diff --git a/Coder-Desktop/VPNLib/XPC.swift b/Coder-Desktop/VPNLib/XPC.swift index 75d3cd2f..8c9c50a9 100644 --- a/Coder-Desktop/VPNLib/XPC.swift +++ b/Coder-Desktop/VPNLib/XPC.swift @@ -6,7 +6,7 @@ import Foundation public let helperAppMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperApp" public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperNE" -// This is the XPC interface the Network Extension exposes to the Helper. +/// This is the XPC interface the Network Extension exposes to the Helper. @preconcurrency @objc public protocol NEXPCInterface { // diff is a serialized Vpn_NetworkSettingsRequest @@ -14,7 +14,7 @@ public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperN func cancelProvider(error: Error?, reply: @escaping () -> Void) } -// This is the XPC interface the GUI app exposes to the Helper. +/// This is the XPC interface the GUI app exposes to the Helper. @preconcurrency @objc public protocol AppXPCInterface { // diff is a serialized `Vpn_PeerUpdate` @@ -22,7 +22,7 @@ public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperN func onProgress(stage: ProgressStage, downloadProgress: DownloadProgress?, reply: @escaping () -> Void) } -// This is the XPC interface the Helper exposes to the Network Extension. +/// This is the XPC interface the Helper exposes to the Network Extension. @preconcurrency @objc public protocol HelperNEXPCInterface { // headers is a JSON `[HTTPHeader]` @@ -38,11 +38,11 @@ public let helperNEMachServiceName = "4399GN35BJ.com.coder.Coder-Desktop.HelperN func stopDaemon(reply: @escaping (Error?) -> Void) } -// This is the XPC interface the Helper exposes to the GUI app. +/// This is the XPC interface the Helper exposes to the GUI app. @preconcurrency @objc public protocol HelperAppXPCInterface { func ping(reply: @escaping () -> Void) - // Data is a serialized `Vpn_PeerUpdate` + /// Data is a serialized `Vpn_PeerUpdate` func getPeerState(with reply: @escaping (Data?) -> Void) } diff --git a/Coder-Desktop/VPNLibTests/CoderRouterTests.swift b/Coder-Desktop/VPNLibTests/CoderRouterTests.swift index 2d47dd6f..c4df5a12 100644 --- a/Coder-Desktop/VPNLibTests/CoderRouterTests.swift +++ b/Coder-Desktop/VPNLibTests/CoderRouterTests.swift @@ -93,8 +93,8 @@ struct CoderRouterTests { description: "Wrong scheme" ), ]) - func testRdpRoutes(testCase: RouteTestCase) throws { - let url = URL(string: testCase.urlString)! + func rdpRoutes(testCase: RouteTestCase) throws { + let url = try #require(URL(string: testCase.urlString)) if let expectedRoute = testCase.expectedRoute { let route = try router.match(url: url) diff --git a/Coder-Desktop/VPNLibTests/ConvertTests.swift b/Coder-Desktop/VPNLibTests/ConvertTests.swift index 9bf35f47..d0f7c8e3 100644 --- a/Coder-Desktop/VPNLibTests/ConvertTests.swift +++ b/Coder-Desktop/VPNLibTests/ConvertTests.swift @@ -4,7 +4,7 @@ import Testing @Suite(.timeLimit(.minutes(1))) struct ConvertNetworkSettingsTests { @Test - func testConvertDnsSettings() async throws { + func testConvertDnsSettings() { let req: Vpn_NetworkSettingsRequest.DNSSettings = .with { dns in dns.servers = ["8.8.8.8"] dns.searchDomains = ["example.com"] @@ -23,7 +23,7 @@ struct ConvertNetworkSettingsTests { } @Test - func testConvertIPv4Settings() async throws { + func testConvertIPv4Settings() throws { let req: Vpn_NetworkSettingsRequest.IPv4Settings = .with { ipv4 in ipv4.addrs = ["192.168.1.1"] ipv4.subnetMasks = ["255.255.255.0"] @@ -51,14 +51,14 @@ struct ConvertNetworkSettingsTests { #expect(result.router == req.router) try #require(result.includedRoutes?.count == req.includedRoutes.count) - let includedRoute = result.includedRoutes![0] + let includedRoute = try #require(result.includedRoutes?[0]) let expectedIncludedRoute = req.includedRoutes[0] #expect(includedRoute.destinationAddress == expectedIncludedRoute.destination) #expect(includedRoute.destinationSubnetMask == expectedIncludedRoute.mask) #expect(includedRoute.gatewayAddress == expectedIncludedRoute.router) try #require(result.excludedRoutes?.count == req.excludedRoutes.count) - let excludedRoute = result.excludedRoutes![0] + let excludedRoute = try #require(result.excludedRoutes?[0]) let expectedExcludedRoute = req.excludedRoutes[0] #expect(excludedRoute.destinationAddress == expectedExcludedRoute.destination) #expect(excludedRoute.destinationSubnetMask == expectedExcludedRoute.mask) @@ -66,7 +66,7 @@ struct ConvertNetworkSettingsTests { } @Test - func testConvertIPv6Settings() async throws { + func testConvertIPv6Settings() throws { let req: Vpn_NetworkSettingsRequest.IPv6Settings = .with { ipv6 in ipv6.addrs = ["2001:db8::1"] ipv6.prefixLengths = [64] @@ -92,14 +92,14 @@ struct ConvertNetworkSettingsTests { #expect(result.networkPrefixLengths == req.prefixLengths.map { NSNumber(value: $0) }) try #require(result.includedRoutes?.count == req.includedRoutes.count) - let includedRoute = result.includedRoutes![0] + let includedRoute = try #require(result.includedRoutes?[0]) let expectedIncludedRoute = req.includedRoutes[0] #expect(includedRoute.destinationAddress == expectedIncludedRoute.destination) #expect(includedRoute.destinationNetworkPrefixLength == NSNumber(value: 64)) #expect(includedRoute.gatewayAddress == expectedIncludedRoute.router) try #require(result.excludedRoutes?.count == req.excludedRoutes.count) - let excludedRoute = result.excludedRoutes![0] + let excludedRoute = try #require(result.excludedRoutes?[0]) let expectedExcludedRoute = req.excludedRoutes[0] #expect(excludedRoute.destinationAddress == expectedExcludedRoute.destination) #expect(excludedRoute.destinationNetworkPrefixLength == NSNumber(value: 128)) diff --git a/Coder-Desktop/VPNLibTests/DownloadTests.swift b/Coder-Desktop/VPNLibTests/DownloadTests.swift index 84661ab9..5283b9e0 100644 --- a/Coder-Desktop/VPNLibTests/DownloadTests.swift +++ b/Coder-Desktop/VPNLibTests/DownloadTests.swift @@ -10,7 +10,7 @@ struct DownloadTests { let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) let testData = Data("foo".utf8) - let fileURL = URL(string: "http://example.com/test1.txt")! + let fileURL = try #require(URL(string: "http://example.com/test1.txt")) Mock(url: fileURL, contentType: .html, statusCode: 200, data: [.get: testData]).register() try await download(src: fileURL, dest: destinationURL, urlSession: URLSession.shared) @@ -25,7 +25,7 @@ struct DownloadTests { @Test func fileNotModified() async throws { let testData = Data("foo bar".utf8) - let fileURL = URL(string: "http://example.com/test2.txt")! + let fileURL = try #require(URL(string: "http://example.com/test2.txt")) let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: destinationURL) } @@ -55,7 +55,7 @@ struct DownloadTests { let ogData = Data("foo bar".utf8) let newData = Data("foo bar qux".utf8) - let fileURL = URL(string: "http://example.com/test3.txt")! + let fileURL = try #require(URL(string: "http://example.com/test3.txt")) let destinationURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) defer { try? FileManager.default.removeItem(at: destinationURL) } diff --git a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift index becf6b37..e64d1598 100644 --- a/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift +++ b/Coder-Desktop/VPNLibTests/TelemetryEnricherTests.swift @@ -3,7 +3,7 @@ import Testing @Suite(.timeLimit(.minutes(1))) struct TelemetryEnricherTests { - @Test func testEnrichStartRequest() throws { + @Test func enrichStartRequest() throws { let enricher0 = TelemetryEnricher() let original = Vpn_StartRequest.with { req in req.coderURL = "https://example.com" diff --git a/flake.lock b/flake.lock index 03f26c8c..e854f735 100644 --- a/flake.lock +++ b/flake.lock @@ -45,7 +45,7 @@ ], "grpc-swift-src": "grpc-swift-src", "nixpkgs": [ - "nixpkgs" + "nixpkgs-grpc-swift" ] }, "locked": { @@ -80,6 +80,22 @@ } }, "nixpkgs": { + "locked": { + "lastModified": 1793110635, + "narHash": "sha256-dhoW6VgFWlaodSY5jrBn+CF2FLmaUvDVgM7+C7T2+NM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "31ecbaca1bdefa45dffc409f0250bd8039ea0010", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-grpc-swift": { "locked": { "lastModified": 1742889210, "narHash": "sha256-hw63HnwnqU3ZQfsMclLhMvOezpM7RSB0dMAtD5/sOiw=", @@ -90,8 +106,8 @@ }, "original": { "owner": "nixos", - "ref": "nixos-unstable", "repo": "nixpkgs", + "rev": "698214a32beb4f4c8e3942372c694f40848b360d", "type": "github" } }, @@ -100,7 +116,8 @@ "flake-parts": "flake-parts", "flake-utils": "flake-utils", "grpc-swift": "grpc-swift", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "nixpkgs-grpc-swift": "nixpkgs-grpc-swift" } }, "systems": { diff --git a/flake.nix b/flake.nix index 10af339f..a6dae091 100644 --- a/flake.nix +++ b/flake.nix @@ -8,9 +8,15 @@ url = "github:hercules-ci/flake-parts"; inputs.nixpkgs-lib.follows = "nixpkgs"; }; + # The protoc plugins must stay in lockstep with the SwiftProtobuf and + # grpc-swift (v1) runtime versions pinned in Coder-Desktop/project.yml. + # They're built from source with nixpkgs' Swift toolchain, which newer + # nixpkgs no longer supports on Darwin, so they get their own pin that's + # independent of the rest of the toolchain. + nixpkgs-grpc-swift.url = "github:nixos/nixpkgs/698214a32beb4f4c8e3942372c694f40848b360d"; grpc-swift = { url = "github:i10416/grpc-swift-flake"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.follows = "nixpkgs-grpc-swift"; inputs.flake-parts.follows = "flake-parts"; }; }; @@ -34,7 +40,7 @@ inherit system; }; - formatter = pkgs.nixfmt-rfc-style; + formatter = pkgs.nixfmt; in { inherit formatter; @@ -49,7 +55,7 @@ gh git gnumake - protobuf_28 + protobuf_36 grpc-swift.packages.${system}.protoc-gen-grpc-swift grpc-swift.packages.${system}.protoc-gen-swift swiftformat diff --git a/scripts/update-appcast/Sources/main.swift b/scripts/update-appcast/Sources/main.swift index 17a57bc6..349bc7bc 100644 --- a/scripts/update-appcast/Sources/main.swift +++ b/scripts/update-appcast/Sources/main.swift @@ -1,10 +1,10 @@ import ArgumentParser import Foundation +import Parsley import RegexBuilder #if canImport(FoundationXML) import FoundationXML #endif -import Parsley /// UpdateAppcast /// ------------- @@ -80,7 +80,7 @@ struct UpdateAppcast: AsyncParsableCommand { guard let item = node as? XMLElement, item.name == "item", item.elements(forName: "sparkle:channel") - .first?.stringValue == channel.rawValue + .first?.stringValue == channel.rawValue else { return false } return true })?.offset @@ -173,13 +173,18 @@ enum UpdateChannel: String { case stable, preview } struct RuntimeError: Error, CustomStringConvertible { var message: String - var description: String { message } - init(_ message: String) { self.message = message } + var description: String { + message + } + + init(_ message: String) { + self.message = message + } } extension Regex: @retroactive @unchecked Sendable {} -// Matches CFBundleVersion format: X.Y.Z or X.Y.Z.N +/// Matches CFBundleVersion format: X.Y.Z or X.Y.Z.N let versionRegex = Regex { Anchor.startOfLine Capture {