Skip to content

Commit cce1286

Browse files
committed
Support auto restart XPC Service if the executable is detected to be changed
1 parent a473a85 commit cce1286

File tree

6 files changed

+186
-90
lines changed

6 files changed

+186
-90
lines changed
Lines changed: 10 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,14 @@
11
import Foundation
2-
3-
struct LaunchAgentManager {
4-
var serviceIdentifier: String {
5-
Bundle.main
6-
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String +
7-
".XPCService"
8-
}
9-
10-
var location: String {
11-
Bundle.main.executableURL?.deletingLastPathComponent()
12-
.appendingPathComponent("CopilotForXcodeXPCService").path ?? ""
13-
}
14-
15-
var launchAgentDirURL: URL {
16-
FileManager.default.homeDirectoryForCurrentUser
17-
.appendingPathComponent("Library/LaunchAgents")
18-
}
19-
20-
var launchAgentPath: String {
21-
launchAgentDirURL.appendingPathComponent("\(serviceIdentifier).plist").path
22-
}
23-
24-
func setupLaunchAgent() throws {
25-
let content = """
26-
<?xml version="1.0" encoding="UTF-8"?>
27-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
28-
<plist version="1.0">
29-
<dict>
30-
<key>RunAtLoad</key>
31-
<true/>
32-
<key>Label</key>
33-
<string>\(serviceIdentifier)</string>
34-
<key>Program</key>
35-
<string>\(location)</string>
36-
<key>MachServices</key>
37-
<dict>
38-
<key>\(serviceIdentifier)</key>
39-
<true/>
40-
</dict>
41-
</dict>
42-
</plist>
43-
"""
44-
if !FileManager.default.fileExists(atPath: launchAgentDirURL.path) {
45-
try FileManager.default.createDirectory(
46-
at: launchAgentDirURL,
47-
withIntermediateDirectories: false
48-
)
49-
}
50-
FileManager.default.createFile(
51-
atPath: launchAgentPath,
52-
contents: content.data(using: .utf8)
2+
import LaunchAgentManager
3+
4+
extension LaunchAgentManager {
5+
init() {
6+
self.init(
7+
serviceIdentifier: Bundle.main
8+
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String +
9+
".XPCService",
10+
executablePath: Bundle.main.executableURL?.deletingLastPathComponent()
11+
.appendingPathComponent("CopilotForXcodeXPCService").path ?? ""
5312
)
54-
launchctl("load", launchAgentPath)
55-
}
56-
57-
func removeLaunchAgent() throws {
58-
launchctl("unload", launchAgentPath)
59-
try FileManager.default.removeItem(atPath: launchAgentPath)
60-
}
61-
62-
func restartLaunchAgent() {
63-
launchctl("unload", launchAgentPath)
64-
launchctl("load", launchAgentPath)
65-
}
66-
}
67-
68-
private func launchctl(_ args: String...) {
69-
let task = Process()
70-
task.launchPath = "/bin/launchctl"
71-
task.arguments = args
72-
task.environment = [
73-
"PATH": "/usr/bin",
74-
]
75-
let outpipe = Pipe()
76-
task.standardOutput = outpipe
77-
try? task.run()
78-
task.waitUntilExit()
79-
if let data = try? outpipe.fileHandleForReading.readToEnd(),
80-
let text = String(data: data, encoding: .utf8)
81-
{
82-
print(text)
8313
}
8414
}

Copilot for Xcode/LaunchAgentView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import LaunchAgentManager
12
import SwiftUI
23
import XPCShared
34

Core/Package.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ let package = Package(
99
products: [
1010
.library(
1111
name: "Service",
12-
targets: ["Service", "SuggestionInjector"]
12+
targets: ["Service", "SuggestionInjector", "FileChangeChecker", "LaunchAgentManager"]
1313
),
1414
.library(
1515
name: "Client",
16-
targets: ["CopilotModel", "Client", "XPCShared"]
16+
targets: ["CopilotModel", "Client", "XPCShared", "LaunchAgentManager"]
1717
),
1818
],
1919
dependencies: [
@@ -60,5 +60,7 @@ let package = Package(
6060
name: "ServiceTests",
6161
dependencies: ["Service", "Client", "CopilotService", "SuggestionInjector", "XPCShared"]
6262
),
63+
.target(name: "FileChangeChecker"),
64+
.target(name: "LaunchAgentManager"),
6365
]
6466
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import CryptoKit
2+
import Dispatch
3+
import Foundation
4+
5+
/// Check that a file is changed.
6+
public actor FileChangeChecker {
7+
let url: URL
8+
var checksum: Data?
9+
10+
public init(fileURL: URL) async {
11+
url = fileURL
12+
checksum = getChecksum()
13+
}
14+
15+
public func checkIfChanged() -> Bool {
16+
guard let newChecksum = getChecksum() else { return false }
17+
return newChecksum != checksum
18+
}
19+
20+
func getChecksum() -> Data? {
21+
let bufferSize = 16 * 1024
22+
guard let file = try? FileHandle(forReadingFrom: url) else { return nil }
23+
defer { try? file.close() }
24+
var md5 = CryptoKit.Insecure.MD5()
25+
while autoreleasepool(invoking: {
26+
let data = file.readData(ofLength: bufferSize)
27+
if !data.isEmpty {
28+
md5.update(data: data)
29+
return true // Continue
30+
} else {
31+
return false // End of file
32+
}
33+
}) {}
34+
35+
let data = Data(md5.finalize())
36+
37+
return data
38+
}
39+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import Foundation
2+
3+
public struct LaunchAgentManager {
4+
let serviceIdentifier: String
5+
let executablePath: String
6+
7+
var launchAgentDirURL: URL {
8+
FileManager.default.homeDirectoryForCurrentUser
9+
.appendingPathComponent("Library/LaunchAgents")
10+
}
11+
12+
var launchAgentPath: String {
13+
launchAgentDirURL.appendingPathComponent("\(serviceIdentifier).plist").path
14+
}
15+
16+
public init(serviceIdentifier: String, executablePath: String) {
17+
self.serviceIdentifier = serviceIdentifier
18+
self.executablePath = executablePath
19+
}
20+
21+
public func setupLaunchAgent() throws {
22+
let content = """
23+
<?xml version="1.0" encoding="UTF-8"?>
24+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
25+
<plist version="1.0">
26+
<dict>
27+
<key>RunAtLoad</key>
28+
<true/>
29+
<key>Label</key>
30+
<string>\(serviceIdentifier)</string>
31+
<key>Program</key>
32+
<string>\(executablePath)</string>
33+
<key>MachServices</key>
34+
<dict>
35+
<key>\(serviceIdentifier)</key>
36+
<true/>
37+
</dict>
38+
</dict>
39+
</plist>
40+
"""
41+
if !FileManager.default.fileExists(atPath: launchAgentDirURL.path) {
42+
try FileManager.default.createDirectory(
43+
at: launchAgentDirURL,
44+
withIntermediateDirectories: false
45+
)
46+
}
47+
FileManager.default.createFile(
48+
atPath: launchAgentPath,
49+
contents: content.data(using: .utf8)
50+
)
51+
launchctl("load", launchAgentPath)
52+
}
53+
54+
public func removeLaunchAgent() throws {
55+
launchctl("unload", launchAgentPath)
56+
try FileManager.default.removeItem(atPath: launchAgentPath)
57+
}
58+
59+
public func restartLaunchAgent() {
60+
launchctl("unload", launchAgentPath)
61+
launchctl("load", launchAgentPath)
62+
}
63+
}
64+
65+
private func launchctl(_ args: String...) {
66+
let task = Process()
67+
task.launchPath = "/bin/launchctl"
68+
task.arguments = args
69+
task.environment = [
70+
"PATH": "/usr/bin",
71+
]
72+
let outpipe = Pipe()
73+
task.standardOutput = outpipe
74+
try? task.run()
75+
}

XPCService/main.swift

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,61 @@
1+
import AppKit
2+
import FileChangeChecker
13
import Foundation
4+
import LaunchAgentManager
5+
import os.log
26
import Service
37

4-
let listener = NSXPCListener(
5-
machServiceName: Bundle.main.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String
6-
+ ".XPCService"
7-
)
8-
let delegate = ServiceDelegate()
9-
listener.delegate = delegate
10-
listener.resume()
11-
_ = AutoTrigger.shared
8+
let bundleIdentifierBase = Bundle.main
9+
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as! String
10+
11+
let serviceIdentifier = bundleIdentifierBase + ".XPCService"
12+
13+
func setupXPCListener() -> (NSXPCListener, ServiceDelegate) {
14+
let listener = NSXPCListener(machServiceName: serviceIdentifier)
15+
let delegate = ServiceDelegate()
16+
listener.delegate = delegate
17+
listener.resume()
18+
return (listener, delegate)
19+
}
20+
21+
func setupAutoTrigger() {
22+
_ = AutoTrigger.shared
23+
}
24+
25+
func setupRestartOnUpdate() {
26+
Task {
27+
guard let url = Bundle.main.executableURL else { return }
28+
let checker = await FileChangeChecker(fileURL: url)
29+
30+
// If Xcode or Copilot for Xcode is launched, check if the executable of this program is changed.
31+
// If changed, restart the launch agent.
32+
33+
let sequence = NSWorkspace.shared.notificationCenter
34+
.notifications(named: NSWorkspace.didLaunchApplicationNotification)
35+
for await notification in sequence {
36+
try Task.checkCancellation()
37+
guard let app = notification
38+
.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
39+
[
40+
"com.apple.dt.Xcode",
41+
bundleIdentifierBase,
42+
].contains(app.bundleIdentifier)
43+
else { continue }
44+
guard await checker.checkIfChanged() else {
45+
os_log(.info, "XPC Service is not updated, no need to restart.")
46+
continue
47+
}
48+
os_log(.info, "XPC Service will be restarted.")
49+
#if DEBUG
50+
#else
51+
manager.restartLaunchAgent()
52+
#endif
53+
}
54+
}
55+
}
56+
57+
let xpcListener = setupXPCListener()
58+
setupAutoTrigger()
59+
setupRestartOnUpdate()
60+
os_log(.info, "XPC Service started.")
1261
RunLoop.main.run()

0 commit comments

Comments
 (0)