Skip to content

feat: support disabling the built-in updater #219

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 30 additions & 13 deletions Coder-Desktop/Coder-Desktop/UpdaterService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,59 @@ 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.
let disabled: Bool = UserDefaults.standard.bool(forKey: Keys.disableUpdater)
Copy link
Preview

Copilot AI Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UserDefaults.bool(forKey:) method returns false when the key doesn't exist, which means the updater will be enabled by default. However, the comment suggests the intent is to disable the updater when the key is set to true. This logic appears inverted - consider using !UserDefaults.standard.bool(forKey: Keys.disableUpdater) if you want the updater enabled by default.

Copilot uses AI. Check for mistakes.


@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"
}
}

Expand Down
6 changes: 3 additions & 3 deletions Coder-Desktop/Coder-Desktop/VPN/VPNService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions Coder-Desktop/Coder-Desktop/Views/FileSync/FilePicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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 }
Expand Down
27 changes: 17 additions & 10 deletions Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@ 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)
} else {
Section {
Text("The app updater has been disabled by a device management policy.")
Copy link
Preview

Copilot AI Aug 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The error message assumes the updater was disabled by device management policy, but the UserDefaults key could be set by other means. Consider making the message more generic like 'The app updater has been disabled.' or 'Automatic updates are not available.'

Suggested change
Text("The app updater has been disabled by a device management policy.")
Text("The app updater has been disabled.")

Copilot uses AI. Check for mistakes.

.foregroundColor(.secondary)
}
}
}.formStyle(.grouped)
Expand Down
Loading