Change how agent launch/relaunch is performed (#737)

This commit is contained in:
Max Goedjen 2025-09-30 23:57:06 -07:00 committed by GitHub
parent 84d5a56fb0
commit d9a3f0c813
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 89 additions and 134 deletions

View File

@ -32,7 +32,6 @@
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; }; 504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; }; 504789232E697DD300B4556F /* BoxBackgroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */; };
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; }; 50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; }; 50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; }; 50617D8523FCE48E0099B055 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8423FCE48E0099B055 /* ContentView.swift */; };
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; }; 50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
@ -194,7 +193,6 @@
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; }; 504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; }; 504789222E697DD300B4556F /* BoxBackgroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BoxBackgroundStyle.swift; sourceTree = "<group>"; };
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; }; 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; }; 5059933F2E7A3B5B0092CFFA /* en */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = en; path = en.lproj/Main.storyboard; sourceTree = "<group>"; };
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; }; 50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; }; 50617D8223FCE48E0099B055 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
@ -448,7 +446,6 @@
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */, 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */, 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */, 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
50571E0424393D1500F76F6C /* LaunchAgentController.swift */,
); );
path = Controllers; path = Controllers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -707,7 +704,6 @@
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */, 5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */,
50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */, 50AE97002E5C1A420018C710 /* IntegrationsView.swift in Sources */,
50153E20250AFCB200525160 /* UpdateView.swift in Sources */, 50153E20250AFCB200525160 /* UpdateView.swift in Sources */,
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */,
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */, 5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */,
50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */, 50B8550D24138C4F009958AC /* DeleteSecretView.swift in Sources */,
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */, 50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,

View File

@ -7,7 +7,7 @@ import Brief
@main @main
struct Secretive: App { struct Secretive: App {
@Environment(\.agentStatusChecker) var agentStatusChecker @Environment(\.agentLaunchController) var agentLaunchController
@Environment(\.justUpdatedChecker) var justUpdatedChecker @Environment(\.justUpdatedChecker) var justUpdatedChecker
@SceneBuilder var body: some Scene { @SceneBuilder var body: some Scene {
@ -15,14 +15,15 @@ struct Secretive: App {
ContentView() ContentView()
.environment(EnvironmentValues._secretStoreList) .environment(EnvironmentValues._secretStoreList)
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
Task {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
guard hasRunSetup else { return } guard hasRunSetup else { return }
agentStatusChecker.check() agentLaunchController.check()
if agentStatusChecker.running && justUpdatedChecker.justUpdatedBuild { guard !agentLaunchController.developmentBuild else { return }
if justUpdatedChecker.justUpdatedBuild || !agentLaunchController.running {
// Relaunch the agent, since it'll be running from earlier update still // Relaunch the agent, since it'll be running from earlier update still
reinstallAgent() try await agentLaunchController.forceLaunch()
} else if !agentStatusChecker.running && !agentStatusChecker.developmentBuild { }
forceLaunchAgent()
} }
} }
} }
@ -79,30 +80,6 @@ extension Secretive {
} }
extension Secretive {
private func reinstallAgent() {
Task {
_ = await LaunchAgentController().install()
try? await Task.sleep(for: .seconds(1))
agentStatusChecker.check()
if !agentStatusChecker.running {
forceLaunchAgent()
}
}
}
private func forceLaunchAgent() {
// We've run setup, we didn't just update, launchd is just not doing it's thing.
// Force a launch directly.
Task {
_ = await LaunchAgentController().forceLaunch()
agentStatusChecker.check()
}
}
}
private enum Constants { private enum Constants {
static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")! static let helpURL = URL(string: "https://github.com/maxgoedjen/secretive/blob/main/FAQ.md")!
} }
@ -121,8 +98,8 @@ extension EnvironmentValues {
return list return list
}() }()
private static let _agentStatusChecker = AgentStatusChecker() private static let _agentLaunchController = AgentLaunchController()
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker @Entry var agentLaunchController: any AgentLaunchControllerProtocol = _agentLaunchController
private static let _updater: any UpdaterProtocol = { private static let _updater: any UpdaterProtocol = {
@AppStorage("defaultsHasRunSetup") var hasRunSetup = false @AppStorage("defaultsHasRunSetup") var hasRunSetup = false
return Updater(checkOnLaunch: hasRunSetup) return Updater(checkOnLaunch: hasRunSetup)

View File

@ -2,18 +2,25 @@ import Foundation
import AppKit import AppKit
import SecretKit import SecretKit
import Observation import Observation
import OSLog
import ServiceManagement
@MainActor protocol AgentStatusCheckerProtocol: Observable, Sendable { @MainActor protocol AgentLaunchControllerProtocol: Observable, Sendable {
var running: Bool { get } var running: Bool { get }
var developmentBuild: Bool { get } var developmentBuild: Bool { get }
var process: NSRunningApplication? { get } var process: NSRunningApplication? { get }
func check() func check()
func install() async throws
func uninstall() async throws
func forceLaunch() async throws
} }
@Observable @MainActor final class AgentStatusChecker: AgentStatusCheckerProtocol { @Observable @MainActor final class AgentLaunchController: AgentLaunchControllerProtocol {
var running: Bool = false var running: Bool = false
var process: NSRunningApplication? = nil var process: NSRunningApplication? = nil
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
private let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
nonisolated init() { nonisolated init() {
Task { @MainActor in Task { @MainActor in
@ -49,6 +56,47 @@ import Observation
Bundle.main.bundleURL.isXcodeURL Bundle.main.bundleURL.isXcodeURL
} }
func install() async throws {
logger.debug("Installing agent")
try? await service.unregister()
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
try await Task.sleep(for: .seconds(1))
try service.register()
try await Task.sleep(for: .seconds(1))
check()
}
func uninstall() async throws {
logger.debug("Uninstalling agent")
try await Task.sleep(for: .seconds(1))
try await service.unregister()
try await Task.sleep(for: .seconds(1))
check()
}
func forceLaunch() async throws {
logger.debug("Agent is not running, attempting to force launch by reinstalling")
try await install()
if running {
logger.debug("Agent successfully force launched by reinstalling")
return
}
logger.debug("Agent is not running, attempting to force launch by launching directly")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try await Task.sleep(for: .seconds(1))
} catch {
logger.error("Error force launching \(error.localizedDescription)")
}
check()
}
} }
extension URL { extension URL {

View File

@ -1,65 +0,0 @@
import Foundation
import ServiceManagement
import AppKit
import OSLog
import SecretKit
struct LaunchAgentController {
private let logger = Logger(subsystem: "com.maxgoedjen.secretive", category: "LaunchAgentController")
func install() async -> Bool {
logger.debug("Installing agent")
_ = setEnabled(false)
// This is definitely a bit of a "seems to work better" thing but:
// Seems to more reliably hit if these are on separate runloops, otherwise it seems like it sometimes doesn't kill old
// and start new?
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(true)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func uninstall() async -> Bool {
logger.debug("Uninstalling agent")
try? await Task.sleep(for: .seconds(1))
let result = await MainActor.run {
setEnabled(false)
}
try? await Task.sleep(for: .seconds(1))
return result
}
func forceLaunch() async -> Bool {
logger.debug("Agent is not running, attempting to force launch")
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Library/LoginItems/SecretAgent.app")
let config = NSWorkspace.OpenConfiguration()
config.activates = false
do {
try await NSWorkspace.shared.openApplication(at: url, configuration: config)
logger.debug("Agent force launched")
try? await Task.sleep(for: .seconds(1))
return true
} catch {
logger.error("Error force launching \(error.localizedDescription)")
return false
}
}
private func setEnabled(_ enabled: Bool) -> Bool {
let service = SMAppService.loginItem(identifier: Bundle.agentBundleID)
do {
if enabled {
try service.register()
} else {
try service.unregister()
}
return true
} catch {
return false
}
}
}

View File

@ -1,7 +1,7 @@
import Foundation import Foundation
import AppKit import AppKit
class PreviewAgentStatusChecker: AgentStatusCheckerProtocol { class PreviewAgentLaunchController: AgentLaunchControllerProtocol {
let running: Bool let running: Bool
let process: NSRunningApplication? let process: NSRunningApplication?
@ -15,4 +15,13 @@ class PreviewAgentStatusChecker: AgentStatusCheckerProtocol {
func check() { func check() {
} }
func install() async throws {
}
func uninstall() async throws {
}
func forceLaunch() async throws {
}
} }

View File

@ -3,6 +3,7 @@ import SwiftUI
struct SetupView: View { struct SetupView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.agentLaunchController) private var agentLaunchController
@Binding var setupComplete: Bool @Binding var setupComplete: Bool
@State var showingIntegrations = false @State var showingIntegrations = false
@ -31,7 +32,7 @@ struct SetupView: View {
) { ) {
installed = true installed = true
Task { Task {
await LaunchAgentController().install() try? await agentLaunchController.install()
} }
} }
} }

View File

@ -2,10 +2,10 @@ import SwiftUI
struct AgentStatusView: View { struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
var body: some View { var body: some View {
if agentStatusChecker.running { if agentLaunchController.running {
AgentRunningView() AgentRunningView()
} else { } else {
AgentNotRunningView() AgentNotRunningView()
@ -14,12 +14,12 @@ struct AgentStatusView: View {
} }
struct AgentRunningView: View { struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @Environment(\.agentLaunchController) private var agentLaunchController: any AgentLaunchControllerProtocol
var body: some View { var body: some View {
Form { Form {
Section { Section {
if let process = agentStatusChecker.process { if let process = agentLaunchController.process {
ConfigurationItemView( ConfigurationItemView(
title: .agentDetailsLocationTitle, title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(), value: process.bundleURL!.path(),
@ -53,19 +53,13 @@ struct AgentRunningView: View {
Menu(.agentDetailsRestartAgentButton) { Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) { Button(.agentDetailsDisableAgentButton) {
Task { Task {
_ = await LaunchAgentController() try? await agentLaunchController
.uninstall() .uninstall()
agentStatusChecker.check()
} }
} }
} primaryAction: { } primaryAction: {
Task { Task {
let controller = LaunchAgentController() try? await agentLaunchController.forceLaunch()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
} }
} }
} }
@ -82,7 +76,7 @@ struct AgentRunningView: View {
struct AgentNotRunningView: View { struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol @Environment(\.agentLaunchController) private var agentLaunchController
@State var triedRestart = false @State var triedRestart = false
@State var loading = false @State var loading = false
@ -103,15 +97,10 @@ struct AgentNotRunningView: View {
guard !loading else { return } guard !loading else { return }
loading = true loading = true
Task { Task {
let controller = LaunchAgentController() try await agentLaunchController.forceLaunch()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false loading = false
if !agentStatusChecker.running { if !agentLaunchController.running {
triedRestart = true triedRestart = true
} }
} }
@ -145,9 +134,9 @@ struct AgentNotRunningView: View {
//#Preview { //#Preview {
// AgentStatusView() // AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false)) // .environment(\.agentLaunchController, PreviewAgentLaunchController(running: false))
//} //}
//#Preview { //#Preview {
// AgentStatusView() // AgentStatusView()
// .environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current)) // .environment(\.agentLaunchController, PreviewAgentLaunchController(running: true, process: .current))
//} //}

View File

@ -14,7 +14,7 @@ struct ContentView: View {
@Environment(\.openWindow) private var openWindow @Environment(\.openWindow) private var openWindow
@Environment(\.secretStoreList) private var storeList @Environment(\.secretStoreList) private var storeList
@Environment(\.updater) private var updater @Environment(\.updater) private var updater
@Environment(\.agentStatusChecker) private var agentStatusChecker @Environment(\.agentLaunchController) private var agentLaunchController
@AppStorage("defaultsHasRunSetup") private var hasRunSetup = false @AppStorage("defaultsHasRunSetup") private var hasRunSetup = false
@State private var showingCreation = false @State private var showingCreation = false
@ -127,7 +127,7 @@ extension ContentView {
showingAgentInfo = true showingAgentInfo = true
}, label: { }, label: {
HStack { HStack {
if agentStatusChecker.running { if agentLaunchController.running {
Text(.agentRunningNoticeTitle) Text(.agentRunningNoticeTitle)
.font(.headline) .font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white) .foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
@ -145,8 +145,8 @@ extension ContentView {
}) })
.buttonStyle( .buttonStyle(
ToolbarStatusButtonStyle( ToolbarStatusButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75), lightColor: agentLaunchController.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5), darkColor: agentLaunchController.running ? .white.opacity(0.05) : .red.opacity(0.5),
) )
) )
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) { .popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {