Skip to content

Commit 6a86c64

Browse files
committed
feat: add auto-updates
1 parent 3c72ff4 commit 6a86c64

File tree

7 files changed

+111
-7
lines changed

7 files changed

+111
-7
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import NetworkExtension
33
import os
44
import SDWebImageSVGCoder
55
import SDWebImageSwiftUI
6+
import Sparkle
67
import SwiftUI
78
import UserNotifications
89
import VPNLib
@@ -26,6 +27,7 @@ struct DesktopApp: App {
2627
.environmentObject(appDelegate.vpn)
2728
.environmentObject(appDelegate.state)
2829
.environmentObject(appDelegate.helper)
30+
.environmentObject(appDelegate.autoUpdater)
2931
}
3032
.windowResizability(.contentSize)
3133
Window("Coder File Sync", id: Windows.fileSync.rawValue) {
@@ -47,11 +49,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4749
let urlHandler: URLHandler
4850
let notifDelegate: NotifDelegate
4951
let helper: HelperService
52+
let autoUpdater: UpdaterService
5053

5154
override init() {
5255
notifDelegate = NotifDelegate()
5356
vpn = CoderVPNService()
5457
helper = HelperService()
58+
autoUpdater = UpdaterService()
5559
let state = AppState(onChange: vpn.configureTunnelProviderProtocol)
5660
vpn.onStart = {
5761
// We don't need this to have finished before the VPN actually starts

Coder-Desktop/Coder-Desktop/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,7 @@
3535
<string>Ae2oQLTcx89/a73XrpOt+IVvqdo+fMTjo3UKEm77VdA=</string>
3636
<key>CommitHash</key>
3737
<string>$(GIT_COMMIT_HASH)</string>
38+
<key>SUFeedURL</key>
39+
<string>https://releases.coder.com/coder-desktop/mac/appcast.xml</string>
3840
</dict>
3941
</plist>
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import Sparkle
2+
import SwiftUI
3+
4+
final class UpdaterService: NSObject, ObservableObject {
5+
private lazy var inner: SPUStandardUpdaterController = .init(
6+
startingUpdater: true,
7+
updaterDelegate: self,
8+
userDriverDelegate: self,
9+
)
10+
private var updater: SPUUpdater!
11+
@Published var canCheckForUpdates = true
12+
13+
@Published var autoCheckForUpdates: Bool = false {
14+
didSet {
15+
if autoCheckForUpdates != oldValue {
16+
updater.automaticallyChecksForUpdates = autoCheckForUpdates
17+
}
18+
}
19+
}
20+
21+
@Published var updateChannel: UpdateChannel {
22+
didSet {
23+
UserDefaults.standard.set(updateChannel.rawValue, forKey: Self.updateChannelKey)
24+
}
25+
}
26+
27+
static let updateChannelKey = "updateChannel"
28+
29+
override init() {
30+
updateChannel = UserDefaults.standard.string(forKey: Self.updateChannelKey)
31+
.flatMap { UpdateChannel(rawValue: $0) } ?? .stable
32+
super.init()
33+
updater = inner.updater
34+
updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates)
35+
}
36+
37+
func checkForUpdates() {
38+
guard canCheckForUpdates else { return }
39+
updater.checkForUpdates()
40+
}
41+
}
42+
43+
enum UpdateChannel: String, CaseIterable, Identifiable {
44+
case stable
45+
case preview
46+
47+
var name: String {
48+
switch self {
49+
case .stable:
50+
"Stable"
51+
case .preview:
52+
"Preview"
53+
}
54+
}
55+
56+
var id: String { rawValue }
57+
}
58+
59+
extension UpdaterService: SPUUpdaterDelegate {
60+
func allowedChannels(for _: SPUUpdater) -> Set<String> {
61+
// There's currently no point in subscribing to both channels, as
62+
// preview >= stable
63+
[updateChannel.rawValue]
64+
}
65+
}
66+
67+
extension UpdaterService: SUVersionDisplay {
68+
func formatUpdateVersion(
69+
fromUpdate update: SUAppcastItem,
70+
andBundleDisplayVersion inOutBundleDisplayVersion: AutoreleasingUnsafeMutablePointer<NSString>,
71+
withBundleVersion bundleVersion: String
72+
) -> String {
73+
// Replace CFBundleShortVersionString with CFBundleVersion, as the
74+
// latter shows build numbers.
75+
inOutBundleDisplayVersion.pointee = bundleVersion as NSString
76+
// This is already CFBundleVersion, as that's the only version in the
77+
// appcast.
78+
return update.displayVersionString
79+
}
80+
}
81+
82+
extension UpdaterService: SPUStandardUserDriverDelegate {
83+
func standardUserDriverRequestsVersionDisplayer() -> (any SUVersionDisplay)? {
84+
self
85+
}
86+
}

Coder-Desktop/Coder-Desktop/VPN/VPNSystemExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ class SystemExtensionDelegate<AsyncDelegate: SystemExtensionAsyncRecorder>:
174174
actionForReplacingExtension existing: OSSystemExtensionProperties,
175175
withExtension extension: OSSystemExtensionProperties
176176
) -> OSSystemExtensionRequest.ReplacementAction {
177-
logger.info("Replacing \(request.identifier) v\(existing.bundleVersion) with v\(`extension`.bundleVersion)")
177+
logger.info("Replacing \(request.identifier) \(existing.bundleVersion) with \(`extension`.bundleVersion)")
178178
// This is counterintuitive, but this function is only called if the
179179
// versions are the same in a dev environment.
180180
// In a release build, this only gets called when the version string is

Coder-Desktop/Coder-Desktop/Views/Settings/GeneralTab.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import SwiftUI
33

44
struct GeneralTab: View {
55
@EnvironmentObject var state: AppState
6+
@EnvironmentObject var updater: UpdaterService
67
var body: some View {
78
Form {
89
Section {
@@ -18,10 +19,20 @@ struct GeneralTab: View {
1819
Text("Start Coder Connect on launch")
1920
}
2021
}
22+
Section {
23+
Toggle(isOn: $updater.autoCheckForUpdates) {
24+
Text("Automatically check for updates")
25+
}
26+
Picker("Update channel", selection: $updater.updateChannel) {
27+
ForEach(UpdateChannel.allCases) { channel in
28+
Text(channel.name).tag(channel)
29+
}
30+
}
31+
HStack {
32+
Spacer()
33+
Button("Check for updates") { updater.checkForUpdates() }.disabled(!updater.canCheckForUpdates)
34+
}
35+
}
2136
}.formStyle(.grouped)
2237
}
2338
}
24-
25-
#Preview {
26-
GeneralTab()
27-
}

Coder-Desktop/project.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ options:
1111

1212
settings:
1313
base:
14-
MARKETING_VERSION: ${MARKETING_VERSION} # Sets the version number.
15-
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # Sets the build number.
14+
MARKETING_VERSION: ${MARKETING_VERSION} # Sets CFBundleShortVersionString
15+
CURRENT_PROJECT_VERSION: ${CURRENT_PROJECT_VERSION} # CFBundleVersion
1616
GIT_COMMIT_HASH: ${GIT_COMMIT_HASH}
1717

1818
ALWAYS_SEARCH_USER_PATHS: NO

scripts/update-cask.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ cask "coder-desktop${SUFFIX}" do
101101
name "Coder Desktop"
102102
desc "Native desktop client for Coder"
103103
homepage "https://github.com/coder/coder-desktop-macos"
104+
auto_updates true
104105
105106
conflicts_with cask: "coder/coder/${CONFLICTS_WITH}"
106107
depends_on macos: ">= :sonoma"

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy