From f34e41562866d7db4dc22925d821296f04eb6c33 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 7 Aug 2025 12:02:10 +1000 Subject: [PATCH 1/2] feat: support disabling the built-in updater --- .../Coder-Desktop/UpdaterService.swift | 47 ++++++++++++++----- .../Views/Settings/GeneralTab.swift | 24 +++++----- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index ce7bc9d2..ed3a2743 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -2,42 +2,63 @@ import Sparkle import SwiftUI final class UpdaterService: NSObject, ObservableObject { - private lazy var inner: SPUStandardUpdaterController = .init( - startingUpdater: true, - updaterDelegate: self, - userDriverDelegate: self - ) - private var updater: SPUUpdater! + // 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. + @Published var disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater) { + didSet { + UserDefaults.standard.set(disabled, forKey: Keys.disableUpdater) + } + } + @Published var canCheckForUpdates = true @Published var autoCheckForUpdates: Bool! { didSet { if let autoCheckForUpdates, autoCheckForUpdates != oldValue { - updater.automaticallyChecksForUpdates = autoCheckForUpdates + inner?.updater.automaticallyChecksForUpdates = autoCheckForUpdates } } } @Published var updateChannel: UpdateChannel { didSet { - UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey) + UserDefaults.standard.set(updateChannel.rawValue, forKey: Keys.updateChannel) } } - static let updateChannelKey = "updateChannel" + private var inner: (controller: SPUStandardUpdaterController, updater: SPUUpdater)? override init() { - updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey) + updateChannel = UserDefaults.standard.string(forKey: Keys.updateChannel) .flatMap { UpdateChannel(rawValue: $0) } ?? .stable super.init() - updater = inner.updater + + guard !disabled else { + return + } + + let inner = SPUStandardUpdaterController( + startingUpdater: true, + updaterDelegate: self, + userDriverDelegate: self + ) + + let updater = inner.updater + self.inner = (inner, updater) + autoCheckForUpdates = updater.automaticallyChecksForUpdates updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates) } func checkForUpdates() { - guard canCheckForUpdates else { return } - updater.checkForUpdates() + guard let inner, canCheckForUpdates else { return } + inner.updater.checkForUpdates() + } + + enum Keys { + static let disableUpdater = "disableUpdater" + static let updateChannel = "updateChannel" } } diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift index 7af41e4b..a177cecc 100644 --- a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift +++ b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift @@ -19,18 +19,20 @@ struct GeneralTab: View { Text("Start Coder Connect on launch") } } - Section { - Toggle(isOn: $updater.autoCheckForUpdates) { - Text("Automatically check for updates") - } - Picker("Update channel", selection: $updater.updateChannel) { - ForEach(UpdateChannel.allCases) { channel in - Text(channel.name).tag(channel) + if !updater.disabled { + Section { + Toggle(isOn: $updater.autoCheckForUpdates) { + Text("Automatically check for updates") + } + Picker("Update channel", selection: $updater.updateChannel) { + ForEach(UpdateChannel.allCases) { channel in + Text(channel.name).tag(channel) + } + } + HStack { + Spacer() + Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates) } - } - HStack { - Spacer() - Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates) } } }.formStyle(.grouped) From ddaf081a4de985420f4c58b4be755f7b356be93e Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 7 Aug 2025 16:29:34 +1000 Subject: [PATCH 2/2] review --- Coder-Desktop/Coder-Desktop/UpdaterService.swift | 6 +----- Coder-Desktop/Coder-Desktop/VPN/VPNService.swift | 6 +++--- .../Coder-Desktop/Views/FileSync/FilePicker.swift | 12 ++++++------ .../Coder-Desktop/Views/Settings/GeneralTab.swift | 5 +++++ 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/UpdaterService.swift b/Coder-Desktop/Coder-Desktop/UpdaterService.swift index ed3a2743..c0f5eaa6 100644 --- a/Coder-Desktop/Coder-Desktop/UpdaterService.swift +++ b/Coder-Desktop/Coder-Desktop/UpdaterService.swift @@ -5,11 +5,7 @@ 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. - @Published var disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater) { - didSet { - UserDefaults.standard.set(disabled, forKey: Keys.disableUpdater) - } - } + let disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater) @Published var canCheckForUpdates = true diff --git a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift index 6ebcba96..de8fa86d 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/VPNService.swift @@ -56,7 +56,7 @@ final class CoderVPNService: NSObject, VPNService { var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn") lazy var xpc: HelperXPCClient = .init(vpn: self) - @Published var tunnelState: VPNServiceState = .disabled { + @Published private(set) var tunnelState: VPNServiceState = .disabled { didSet { if tunnelState == .connecting { progress = .init(stage: .initial, downloadProgress: nil) @@ -80,9 +80,9 @@ final class CoderVPNService: NSObject, VPNService { return tunnelState } - @Published var progress: VPNProgress = .init(stage: .initial, downloadProgress: nil) + @Published private(set) var progress: VPNProgress = .init(stage: .initial, downloadProgress: nil) - @Published var menuState: VPNMenuState = .init() + @Published private(set) var menuState: VPNMenuState = .init() // Whether the VPN should start as soon as possible var startWhenReady: Bool = false diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift index 9ec26231..24e938a4 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift @@ -69,9 +69,9 @@ struct FilePicker: View { @MainActor class FilePickerModel: ObservableObject { - @Published var rootEntries: [FilePickerEntryModel] = [] - @Published var rootIsLoading: Bool = false - @Published var error: SDKError? + @Published private(set) var rootEntries: [FilePickerEntryModel] = [] + @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). @@ -153,9 +153,9 @@ class FilePickerEntryModel: Identifiable, Hashable, ObservableObject { let client: AgentClient - @Published var entries: [FilePickerEntryModel]? - @Published var isLoading = false - @Published var error: SDKError? + @Published private(set) var entries: [FilePickerEntryModel]? + @Published private(set) var isLoading = false + @Published private(set) var error: SDKError? @Published private var innerIsExpanded = false var isExpanded: Bool { get { innerIsExpanded } diff --git a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift index a177cecc..d779a9ac 100644 --- a/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift +++ b/Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift @@ -34,6 +34,11 @@ struct GeneralTab: View { Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates) } } + } else { + Section { + Text("The app updater has been disabled by a device management policy.") + .foregroundColor(.secondary) + } } }.formStyle(.grouped) }