diff --git a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved index 9d2675d..31438e5 100644 --- a/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Copilot for Xcode.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -131,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "87e4fcbac39912f9cdb9a9acf205cad60e1ca3bc", - "version" : "2.4.2" + "revision" : "0ef1ee0220239b3776f433314515fd849025673f", + "version" : "2.6.4" } }, { diff --git a/Copilot for Xcode/App.swift b/Copilot for Xcode/App.swift index a00e4f2..cc07bc2 100644 --- a/Copilot for Xcode/App.swift +++ b/Copilot for Xcode/App.swift @@ -15,6 +15,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { true } } +class AppUpdateCheckerDelegate: UpdateCheckerDelegate { + func prepareForRelaunch(finish: @escaping () -> Void) { + Task { + let service = try? getService() + try? await service?.quitService() + finish() + } + } +} + @main struct CopilotForXcodeApp: App { @NSApplicationDelegateAdaptor private var appDelegate: AppDelegate @@ -28,9 +38,12 @@ struct CopilotForXcodeApp: App { UserDefaults.setupDefaultSettings() } .copilotIntroSheet() + .environment(\.updateChecker, UpdateChecker( + hostBundle: Bundle.main, + checkerDelegate: AppUpdateCheckerDelegate() + )) } } } var isPreview: Bool { ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" } - diff --git a/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift b/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift index 643266b..e49c123 100644 --- a/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift +++ b/Core/Sources/HostApp/GeneralSettings/AppInfoView.swift @@ -16,7 +16,6 @@ struct AppInfoView: View { @StateObject var settings = Settings() @StateObject var viewModel: GitHubCopilotViewModel - @State var automaticallyCheckForUpdate: Bool? @State var appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String let store: StoreOf @@ -48,11 +47,8 @@ struct AppInfoView: View { } HStack { Toggle(isOn: .init( - get: { automaticallyCheckForUpdate ?? updateChecker.automaticallyChecksForUpdates }, - set: { - updateChecker.automaticallyChecksForUpdates = $0 - automaticallyCheckForUpdate = $0 - } + get: { updateChecker.getAutomaticallyChecksForUpdates() }, + set: { updateChecker.setAutomaticallyChecksForUpdates($0) } )) { Text("Automatically Check for Updates") } diff --git a/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift b/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift index 6fcfd10..8511859 100644 --- a/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift +++ b/Core/Sources/HostApp/GeneralSettings/GeneralSettingsView.swift @@ -2,7 +2,6 @@ import ComposableArchitecture import SwiftUI struct GeneralSettingsView: View { - @Environment(\.updateChecker) var updateChecker @AppStorage(\.extensionPermissionShown) var extensionPermissionShown: Bool @AppStorage(\.quitXPCServiceOnXcodeAndAppQuit) var quitXPCServiceOnXcodeAndAppQuit: Bool @State private var shouldPresentExtensionPermissionAlert = false diff --git a/Core/Sources/HostApp/TabContainer.swift b/Core/Sources/HostApp/TabContainer.swift index 069a410..0d3f0a8 100644 --- a/Core/Sources/HostApp/TabContainer.swift +++ b/Core/Sources/HostApp/TabContainer.swift @@ -201,11 +201,11 @@ private extension EnvironmentValues { } struct UpdateCheckerKey: EnvironmentKey { - static var defaultValue: UpdateChecker = .init(hostBundle: Bundle.main) + static var defaultValue: UpdateCheckerProtocol = NoopUpdateChecker() } public extension EnvironmentValues { - var updateChecker: UpdateChecker { + var updateChecker: UpdateCheckerProtocol { get { self[UpdateCheckerKey.self] } set { self[UpdateCheckerKey.self] = newValue } } diff --git a/Core/Sources/UpdateChecker/UpdateChecker.swift b/Core/Sources/UpdateChecker/UpdateChecker.swift index d280bdd..e477817 100644 --- a/Core/Sources/UpdateChecker/UpdateChecker.swift +++ b/Core/Sources/UpdateChecker/UpdateChecker.swift @@ -2,24 +2,35 @@ import Logger import Preferences import Sparkle -public final class UpdateChecker { +public protocol UpdateCheckerProtocol { + func checkForUpdates() + func getAutomaticallyChecksForUpdates() -> Bool + func setAutomaticallyChecksForUpdates(_ value: Bool) +} + +public protocol UpdateCheckerDelegate: AnyObject { + func prepareForRelaunch(finish: @escaping () -> Void) +} + +public final class NoopUpdateChecker: UpdateCheckerProtocol { + public init() {} + public func checkForUpdates() {} + public func getAutomaticallyChecksForUpdates() -> Bool { false } + public func setAutomaticallyChecksForUpdates(_ value: Bool) {} +} + +public final class UpdateChecker: UpdateCheckerProtocol { let updater: SPUUpdater - let hostBundleFound: Bool let delegate = UpdaterDelegate() - public init(hostBundle: Bundle?) { - if hostBundle == nil { - hostBundleFound = false - Logger.updateChecker.error("Host bundle not found") - } else { - hostBundleFound = true - } + public init(hostBundle: Bundle, checkerDelegate: UpdateCheckerDelegate) { updater = SPUUpdater( - hostBundle: hostBundle ?? Bundle.main, + hostBundle: hostBundle, applicationBundle: Bundle.main, - userDriver: SPUStandardUserDriver(hostBundle: hostBundle ?? Bundle.main, delegate: nil), + userDriver: SPUStandardUserDriver(hostBundle: hostBundle, delegate: nil), delegate: delegate ) + delegate.updateCheckerDelegate = checkerDelegate do { try updater.start() } catch { @@ -27,17 +38,38 @@ public final class UpdateChecker { } } + public convenience init?(hostBundle: Bundle?, checkerDelegate: UpdateCheckerDelegate) { + guard let hostBundle = hostBundle else { return nil } + self.init(hostBundle: hostBundle, checkerDelegate: checkerDelegate) + } + public func checkForUpdates() { updater.checkForUpdates() } - public var automaticallyChecksForUpdates: Bool { - get { updater.automaticallyChecksForUpdates } - set { updater.automaticallyChecksForUpdates = newValue } + public func getAutomaticallyChecksForUpdates() -> Bool { + updater.automaticallyChecksForUpdates + } + + public func setAutomaticallyChecksForUpdates(_ value: Bool) { + updater.automaticallyChecksForUpdates = value } } class UpdaterDelegate: NSObject, SPUUpdaterDelegate { + weak var updateCheckerDelegate: UpdateCheckerDelegate? + + func updater( + _ updater: SPUUpdater, + shouldPostponeRelaunchForUpdate item: SUAppcastItem, + untilInvokingBlock installHandler: @escaping () -> Void) -> Bool { + if let updateCheckerDelegate { + updateCheckerDelegate.prepareForRelaunch(finish: installHandler) + return true + } + return false + } + func allowedChannels(for updater: SPUUpdater) -> Set { if UserDefaults.shared.value(for: \.installPrereleases) { Set(["prerelease"]) diff --git a/ExtensionService/AppDelegate.swift b/ExtensionService/AppDelegate.swift index 325f131..8b8304f 100644 --- a/ExtensionService/AppDelegate.swift +++ b/ExtensionService/AppDelegate.swift @@ -17,6 +17,15 @@ let bundleIdentifierBase = Bundle.main .object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String let serviceIdentifier = bundleIdentifierBase + ".ExtensionService" +class ExtensionUpdateCheckerDelegate: UpdateCheckerDelegate { + func prepareForRelaunch(finish: @escaping () -> Void) { + Task { + await Service.shared.prepareForExit() + finish() + } + } +} + @main class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { let service = Service.shared @@ -24,8 +33,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { var xpcController: XPCController? let updateChecker = UpdateChecker( - hostBundle: locateHostBundleURL(url: Bundle.main.bundleURL) - .flatMap(Bundle.init(url:)) + hostBundle: Bundle(url: locateHostBundleURL(url: Bundle.main.bundleURL)), + checkerDelegate: ExtensionUpdateCheckerDelegate() ) let statusChecker: AuthStatusChecker = AuthStatusChecker() var xpcExtensionService: XPCExtensionService? @@ -57,12 +66,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { @objc func openCopilotForXcode() { let task = Process() - if let appPath = locateHostBundleURL(url: Bundle.main.bundleURL)?.absoluteString { - task.launchPath = "/usr/bin/open" - task.arguments = [appPath] - task.launch() - task.waitUntilExit() - } + let appPath = locateHostBundleURL(url: Bundle.main.bundleURL) + task.launchPath = "/usr/bin/open" + task.arguments = [appPath.absoluteString] + task.launch() + task.waitUntilExit() } @objc func openGlobalChat() { @@ -140,6 +148,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { } @objc func checkForUpdate() { + guard let updateChecker = updateChecker else { + Logger.service.error("Unable to check for updates: updateChecker is nil.") + return + } updateChecker.checkForUpdates() } @@ -160,7 +172,7 @@ extension NSRunningApplication { } } -func locateHostBundleURL(url: URL) -> URL? { +func locateHostBundleURL(url: URL) -> URL { var nextURL = url while nextURL.path != "/" { nextURL = nextURL.deletingLastPathComponent()