Skip to content

Commit 363cf04

Browse files
authored
fix(mac): keep launch-at-login app running (#102465)
* fix(mac): avoid launch-agent self-reload * chore: leave release changelog generation * style(mac): use conditional expression
1 parent c20ce00 commit 363cf04

2 files changed

Lines changed: 34 additions & 2 deletions

File tree

apps/macos/Sources/OpenClaw/LaunchAgentManager.swift

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,32 @@ enum LaunchAgentManager {
88

99
static func status() async -> Bool {
1010
guard FileManager().fileExists(atPath: self.plistURL.path) else { return false }
11+
return await self.isLoaded()
12+
}
13+
14+
private static func isLoaded() async -> Bool {
1115
let result = await self.runLaunchctl(["print", "gui/\(getuid())/\(launchdLabel)"])
1216
return result == 0
1317
}
1418

15-
static func set(enabled: Bool, bundlePath: String) async {
19+
@discardableResult
20+
static func set(
21+
enabled: Bool,
22+
bundlePath: String,
23+
loaded: Bool? = nil,
24+
writePlist: ((String) -> Void)? = nil) async -> Bool
25+
{
1626
if enabled {
17-
self.writePlist(bundlePath: bundlePath)
27+
let persist = writePlist ?? { self.writePlist(bundlePath: $0) }
28+
persist(bundlePath)
29+
let alreadyLoaded = if let loaded {
30+
loaded
31+
} else {
32+
await self.isLoaded()
33+
}
34+
// Startup hydrates the toggle from launchd. Reinstalling the active job here
35+
// would boot out the app that is still responsible for bootstrapping it again.
36+
guard !alreadyLoaded else { return false }
1837
_ = await self.runLaunchctl(["bootout", "gui/\(getuid())/\(launchdLabel)"])
1938
_ = await self.runLaunchctl(["bootstrap", "gui/\(getuid())", self.plistURL.path])
2039
_ = await self.runLaunchctl(["kickstart", "-k", "gui/\(getuid())/\(launchdLabel)"])
@@ -23,6 +42,7 @@ enum LaunchAgentManager {
2342
// bootout would terminate the launchd job immediately (and crash the app if launched via agent).
2443
try? FileManager().removeItem(at: self.plistURL)
2544
}
45+
return true
2646
}
2747

2848
private static func writePlist(bundlePath: String) {

apps/macos/Tests/OpenClawIPCTests/LaunchAgentManagerTests.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ import Testing
33
@testable import OpenClaw
44

55
struct LaunchAgentManagerTests {
6+
@Test func `enabling an already loaded login job only refreshes its plist`() async {
7+
var persistedBundlePaths: [String] = []
8+
let reloaded = await LaunchAgentManager.set(
9+
enabled: true,
10+
bundlePath: "/Applications/OpenClaw.app",
11+
loaded: true,
12+
writePlist: { persistedBundlePaths.append($0) })
13+
14+
#expect(reloaded == false)
15+
#expect(persistedBundlePaths == ["/Applications/OpenClaw.app"])
16+
}
17+
618
@Test func `launch at login plist does not keep app alive after manual quit`() throws {
719
let plist = LaunchAgentManager.plistContents(bundlePath: "/Applications/OpenClaw.app")
820
let data = try #require(plist.data(using: .utf8))

0 commit comments

Comments
 (0)