Skip to content

Commit 9ec061c

Browse files
committed
Adjust implementation of communication bridge
1 parent 52947e2 commit 9ec061c

6 files changed

Lines changed: 144 additions & 20 deletions

File tree

CommunicationBridge/ServiceDelegate.swift

Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import AppKit
12
import Foundation
23
import Logger
34
import XPCShared
@@ -14,35 +15,130 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate {
1415
let exportedObject = XPCService()
1516
newConnection.exportedObject = exportedObject
1617
newConnection.resume()
17-
18-
Logger.temp.debug("Accepted new connection.")
19-
18+
19+
Logger.communicationBridge.info("Accepted new connection.")
20+
2021
return true
2122
}
2223
}
2324

2425
class XPCService: CommunicationBridgeXPCServiceProtocol {
25-
static var endpoint: NSXPCListenerEndpoint?
26+
static let eventHandler = EventHandler()
2627

2728
func launchExtensionServiceIfNeeded(
2829
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
2930
) {
31+
Task {
32+
await Self.eventHandler.launchExtensionServiceIfNeeded(withReply: reply)
33+
}
34+
}
35+
36+
func quit(withReply reply: @escaping () -> Void) {
37+
Task {
38+
await Self.eventHandler.quit(withReply: reply)
39+
}
40+
}
41+
42+
func updateServiceEndpoint(
43+
endpoint: NSXPCListenerEndpoint,
44+
withReply reply: @escaping () -> Void
45+
) {
46+
Task {
47+
await Self.eventHandler.updateServiceEndpoint(endpoint: endpoint, withReply: reply)
48+
}
49+
}
50+
}
51+
52+
actor EventHandler {
53+
var endpoint: NSXPCListenerEndpoint?
54+
let launcher = ExtensionServiceLauncher()
55+
var exitTask: Task<Void, Error>?
56+
57+
init() {
58+
Task { await rescheduleExitTask() }
59+
}
60+
61+
func launchExtensionServiceIfNeeded(
62+
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
63+
) async {
64+
rescheduleExitTask()
3065
#if DEBUG
31-
reply(Self.endpoint)
32-
#else
33-
// launch the app
3466
reply(endpoint)
67+
#else
68+
if await launcher.isApplicationValid {
69+
reply(endpoint)
70+
} else {
71+
endpoint = nil
72+
reply(nil)
73+
await launcher.launch()
74+
}
3575
#endif
3676
}
3777

3878
func quit(withReply reply: () -> Void) {
79+
Logger.communicationBridge.info("Exiting service.")
3980
listener.invalidate()
4081
exit(0)
4182
}
4283

4384
func updateServiceEndpoint(endpoint: NSXPCListenerEndpoint, withReply reply: () -> Void) {
44-
Self.endpoint = endpoint
85+
rescheduleExitTask()
86+
self.endpoint = endpoint
4587
reply()
4688
}
89+
90+
/// The bridge will kill itself when it's not used for a period.
91+
/// It's fine that the bridge is killed because it will be launched again when needed.
92+
private func rescheduleExitTask() {
93+
exitTask?.cancel()
94+
exitTask = Task {
95+
#if DEBUG
96+
try await Task.sleep(nanoseconds: 60_000_000_000)
97+
Logger.communicationBridge.info("Exit will be called in release build.")
98+
#else
99+
try await Task.sleep(nanoseconds: 1_800_000_000_000)
100+
Logger.communicationBridge.info("Exiting service.")
101+
listener.invalidate()
102+
exit(0)
103+
#endif
104+
}
105+
}
106+
}
107+
108+
actor ExtensionServiceLauncher {
109+
let appIdentifier = bundleIdentifierBase.appending(".ExtensionService")
110+
let appURL = Bundle.main.bundleURL.appendingPathComponent(
111+
"CopilotForXcodeExtensionService.app"
112+
)
113+
var isLaunching: Bool = false
114+
var application: NSRunningApplication?
115+
var isApplicationValid: Bool {
116+
if let application, !application.isTerminated { return true }
117+
return false
118+
}
119+
120+
func launch() {
121+
guard !isLaunching else { return }
122+
isLaunching = true
123+
124+
Logger.communicationBridge.info("Launching extension service app.")
125+
NSWorkspace.shared.openApplication(
126+
at: appURL,
127+
configuration: .init()
128+
) { app, error in
129+
if let error = error {
130+
Logger.communicationBridge.error(
131+
"Failed to launch extension service app: \(error)"
132+
)
133+
} else {
134+
Logger.communicationBridge.info(
135+
"Finished launching extension service app."
136+
)
137+
}
138+
139+
self.application = app
140+
self.isLaunching = false
141+
}
142+
}
47143
}
48144

Copilot for Xcode.xcodeproj/project.pbxproj

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
C8738B7F2BE5363900609E7F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B7E2BE5363900609E7F /* Assets.xcassets */; };
4141
C8738B822BE5363900609E7F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C8738B812BE5363900609E7F /* Preview Assets.xcassets */; };
4242
C8738B882BE5365000609E7F /* Client in Frameworks */ = {isa = PBXBuildFile; productRef = C8738B872BE5365000609E7F /* Client */; };
43+
C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */ = {isa = PBXBuildFile; fileRef = C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */; };
44+
C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C8738B632BE4D4B900609E7F /* CommunicationBridge */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
4345
C8758E7029F04BFF00D29C1C /* CustomCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E6F29F04BFF00D29C1C /* CustomCommand.swift */; };
4446
C8758E7229F04CF100D29C1C /* SeparatorCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8758E7129F04CF100D29C1C /* SeparatorCommand.swift */; };
4547
C87B03A5293B261200C77EAE /* AcceptSuggestionCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C87B03A4293B261200C77EAE /* AcceptSuggestionCommand.swift */; };
@@ -53,7 +55,6 @@
5355
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */ = {isa = PBXBuildFile; fileRef = C861E60E2994F6070056CB02 /* CopilotForXcodeExtensionService.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
5456
C8DCF00029CE11D500FDDDD7 /* ChatWithSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DCEFFF29CE11D500FDDDD7 /* ChatWithSelection.swift */; };
5557
C8DD9CB12BC673F80036641C /* CloseIdleTabsCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8DD9CB02BC673F80036641C /* CloseIdleTabsCommand.swift */; };
56-
C8F1032B2A7A39D700D28F4F /* launchAgent.plist in Copy Launch Agent */ = {isa = PBXBuildFile; fileRef = C8F103292A7A365000D28F4F /* launchAgent.plist */; };
5758
/* End PBXBuildFile section */
5859

5960
/* Begin PBXContainerItemProxy section */
@@ -78,6 +79,13 @@
7879
remoteGlobalIDString = C8216B6F298036EC00AD38C7;
7980
remoteInfo = Helper;
8081
};
82+
C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */ = {
83+
isa = PBXContainerItemProxy;
84+
containerPortal = C8189B0E2938972F00C9DCDA /* Project object */;
85+
proxyType = 1;
86+
remoteGlobalIDString = C8738B622BE4D4B900609E7F;
87+
remoteInfo = CommunicationBridge;
88+
};
8189
/* End PBXContainerItemProxy section */
8290

8391
/* Begin PBXCopyFilesBuildPhase section */
@@ -118,6 +126,7 @@
118126
dstPath = ../Applications;
119127
dstSubfolderSpec = 6;
120128
files = (
129+
C8738B8B2BE540DD00609E7F /* CommunicationBridge in Embed XPCService */,
121130
C8216B802980378300AD38C7 /* Helper in Embed XPCService */,
122131
C8C8B60929AFA35F00034BEE /* CopilotForXcodeExtensionService.app in Embed XPCService */,
123132
);
@@ -160,7 +169,7 @@
160169
dstPath = Contents/Library/LaunchAgents;
161170
dstSubfolderSpec = 1;
162171
files = (
163-
C8F1032B2A7A39D700D28F4F /* launchAgent.plist in Copy Launch Agent */,
172+
C8738B8A2BE540D000609E7F /* bridgeLaunchAgent.plist in Copy Launch Agent */,
164173
);
165174
name = "Copy Launch Agent";
166175
runOnlyForDeploymentPostprocessing = 0;
@@ -210,7 +219,6 @@
210219
C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceDelegate.swift; sourceTree = "<group>"; };
211220
C8738B6D2BE4F3E800609E7F /* bridgeLaunchAgent.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = bridgeLaunchAgent.plist; sourceTree = "<group>"; };
212221
C8738B702BE4F8B700609E7F /* XPCController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XPCController.swift; sourceTree = "<group>"; };
213-
C8738B722BE4FC3200609E7F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
214222
C8738B782BE5363800609E7F /* SandboxedClientTester.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SandboxedClientTester.app; sourceTree = BUILT_PRODUCTS_DIR; };
215223
C8738B7A2BE5363800609E7F /* SandboxedClientTesterApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxedClientTesterApp.swift; sourceTree = "<group>"; };
216224
C8738B7C2BE5363800609E7F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
@@ -408,7 +416,6 @@
408416
C8738B642BE4D4B900609E7F /* CommunicationBridge */ = {
409417
isa = PBXGroup;
410418
children = (
411-
C8738B722BE4FC3200609E7F /* Info.plist */,
412419
C8738B652BE4D4B900609E7F /* main.swift */,
413420
C8738B6A2BE4D56F00609E7F /* ServiceDelegate.swift */,
414421
);
@@ -475,6 +482,7 @@
475482
buildRules = (
476483
);
477484
dependencies = (
485+
C8738B8D2BE540F900609E7F /* PBXTargetDependency */,
478486
C81291B02994F92700196E12 /* PBXTargetDependency */,
479487
C8216B7F2980377E00AD38C7 /* PBXTargetDependency */,
480488
C814589A2939EFDC00135263 /* PBXTargetDependency */,
@@ -750,6 +758,11 @@
750758
target = C8216B6F298036EC00AD38C7 /* Helper */;
751759
targetProxy = C8216B7E2980377E00AD38C7 /* PBXContainerItemProxy */;
752760
};
761+
C8738B8D2BE540F900609E7F /* PBXTargetDependency */ = {
762+
isa = PBXTargetDependency;
763+
target = C8738B622BE4D4B900609E7F /* CommunicationBridge */;
764+
targetProxy = C8738B8C2BE540F900609E7F /* PBXContainerItemProxy */;
765+
};
753766
/* End PBXTargetDependency section */
754767

755768
/* Begin XCBuildConfiguration section */
@@ -1088,6 +1101,7 @@
10881101
MACOSX_DEPLOYMENT_TARGET = 12.0;
10891102
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge";
10901103
PRODUCT_NAME = "$(TARGET_NAME)";
1104+
SKIP_INSTALL = YES;
10911105
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
10921106
SWIFT_VERSION = 5.0;
10931107
};
@@ -1106,6 +1120,7 @@
11061120
MACOSX_DEPLOYMENT_TARGET = 12.0;
11071121
PRODUCT_BUNDLE_IDENTIFIER = "$(BUNDLE_IDENTIFIER_BASE).CommunicationBridge";
11081122
PRODUCT_NAME = "$(TARGET_NAME)";
1123+
SKIP_INSTALL = YES;
11091124
SWIFT_VERSION = 5.0;
11101125
};
11111126
name = Release;

Tool/Sources/Logger/Logger.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public final class Logger {
2222
public static let retrieval = Logger(category: "Retrieval")
2323
public static let license = Logger(category: "License")
2424
public static let `extension` = Logger(category: "Extension")
25+
public static let communicationBridge = Logger(category: "CommunicationBridge")
2526
#if DEBUG
2627
/// Use a temp logger to log something temporary. I won't be available in release builds.
2728
public static let temp = Logger(category: "Temp")

Tool/Sources/XPCShared/XPCCommunicationBridge.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ public class XPCCommunicationBridge {
2121
let logger: Logger
2222
var serviceEndpoint: NSXPCListenerEndpoint?
2323

24-
nonisolated
25-
public init(logger: Logger) {
24+
public nonisolated
25+
init(logger: Logger) {
2626
service = .init(
2727
kind: .machService(
2828
identifier: Bundle(for: XPCService.self)
@@ -35,6 +35,10 @@ public class XPCCommunicationBridge {
3535
self.logger = logger
3636
}
3737

38+
public func setDelegate(_ delegate: XPCServiceDelegate) {
39+
service.delegate = delegate
40+
}
41+
3842
public func launchExtensionServiceIfNeeded() async throws -> NSXPCListenerEndpoint? {
3943
try await withXPCServiceConnected { service, continuation in
4044
service.launchExtensionServiceIfNeeded { endpoint in

Tool/Sources/XPCShared/XPCExtensionService.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ public class XPCExtensionService {
196196
}
197197

198198
extension XPCExtensionService: XPCServiceDelegate {
199-
func connectionDidInterrupt() async {
199+
public func connectionDidInterrupt() async {
200200
// do nothing
201201
}
202202

203-
func connectionDidInvalidate() async {
203+
public func connectionDidInvalidate() async {
204204
service = nil
205205
}
206206
}

Tool/Sources/XPCShared/XPCService.swift

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class XPCService {
1919
let logger: Logger
2020
weak var delegate: XPCServiceDelegate?
2121
private var isInvalidated = false
22-
22+
2323
private lazy var _connection: InvalidatingConnection? = buildConnection()
2424

2525
var connection: NSXPCConnection? {
@@ -29,10 +29,16 @@ class XPCService {
2929
}
3030

3131
nonisolated
32-
init(kind: Kind, interface: NSXPCInterface, logger: Logger) {
32+
init(
33+
kind: Kind,
34+
interface: NSXPCInterface,
35+
logger: Logger,
36+
delegate: XPCServiceDelegate? = nil
37+
) {
3338
self.kind = kind
3439
self.interface = interface
3540
self.logger = logger
41+
self.delegate = delegate
3642
}
3743

3844
private func buildConnection() -> InvalidatingConnection {
@@ -59,7 +65,7 @@ class XPCService {
5965
connection.resume()
6066
return .init(connection)
6167
}
62-
68+
6369
private func markAsInvalidated() {
6470
isInvalidated = true
6571
}
@@ -69,7 +75,7 @@ class XPCService {
6975
}
7076
}
7177

72-
protocol XPCServiceDelegate: AnyObject {
78+
public protocol XPCServiceDelegate: AnyObject {
7379
func connectionDidInvalidate() async
7480
func connectionDidInterrupt() async
7581
}
@@ -79,6 +85,7 @@ private class InvalidatingConnection {
7985
init(_ connection: NSXPCConnection) {
8086
self.connection = connection
8187
}
88+
8289
deinit {
8390
connection.invalidationHandler = {}
8491
connection.interruptionHandler = {}
@@ -118,3 +125,4 @@ func withXPCServiceConnected<T, P>(
118125
}
119126
return try await stream.first(where: { _ in true })!
120127
}
128+

0 commit comments

Comments
 (0)