Skip to content

Commit 4838bcf

Browse files
committed
Swap to APIServer for all communications
Today, we have a sort of odd model all things considered. We talk to the APIServer to do the initial container creation, which mostly all of the work is just registering the runtime helper with launchd. After this registration, almost all communication from a client is talking directly to that runtime helper. This has a couple rather annoying issues in that we now need a sort of event/notification channel that the helper will establish with the APIServer for errors/start/exited cases. If instead of talking to the runtime helper directly, we instead took a detour through the APIServer, the APIServer is clued into exactly what order of operations is occurring. This makes "did starting the container fail? Okay we should clean up" scenarios much simpler, and it also simplifies the clients quite a bit as they don't need this split brained client model, everyone just talks to the APIServer. This change is in pursuit of that. I have reworked our clients, the ContainerService and some of our XPC types to accomplish it. The biggest "contract" changes are in the SandboxService. The first is today we have on the flag when we register any runtime helper that makes any xpc messages wake up the registered process. This isn't great in scenarios where the process may have crashed, or it exited normally and we're just trying to invoke an RPC on it. Today the helper would spawn again and try and answer our request. It'd be much nicer if we have a connection object that will become invalid if the process that vended it to us is gone. To accomplish this, now the runtime helpers will listen on an anonymous xpc connection and vend endpoints from this via only one handler exposed by the SandboxService (createEndpoint). From that point onwards all communication will be through the endpoint the service vended a client. The second change is the runtime helper will not exit on its own when the container exits, and the event mechanism has been removed. Now the APIServer simply calls wait() to listen for container exit in the background, and once we get an exit we will explicitly tell the helper to shutdown. The rationale is if shutdown is driven by the APIServer now, we can be certain we received everything we need from the helpers before they power down.
1 parent 449f1d2 commit 4838bcf

File tree

21 files changed

+891
-366
lines changed

21 files changed

+891
-366
lines changed

Sources/CLI/Builder/BuilderStart.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ extension ClientContainer {
249249
defer { try? io.close() }
250250

251251
let process = try await bootstrap(stdio: io.stdio)
252-
_ = try await process.start()
252+
try await process.start()
253253
await taskManager?.finish()
254254
try io.closeAfterStart()
255255

Sources/CLI/Container/ContainerStop.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ extension Application {
6868
)
6969
let failed = try await Self.stopContainers(containers: containers, stopOptions: opts)
7070
if failed.count > 0 {
71-
throw ContainerizationError(.internalError, message: "stop failed for one or more containers \(failed.joined(separator: ","))")
71+
throw ContainerizationError(
72+
.internalError,
73+
message: "stop failed for one or more containers \(failed.joined(separator: ","))"
74+
)
7275
}
7376
}
7477

Sources/CLI/RunCommand.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ extension Application {
109109
)
110110

111111
let detach = self.managementFlags.detach
112-
113112
do {
114113
let io = try ProcessIO.create(
115114
tty: self.processFlags.tty,

Sources/ContainerClient/ContainerEvents.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,5 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
public enum ContainerEvent: Sendable, Codable {
18-
case containerStart(id: String)
1918
case containerExit(id: String, exitCode: Int64)
2019
}

Sources/ContainerClient/Core/ClientContainer.swift

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ import TerminalProgress
2525
public struct ClientContainer: Sendable, Codable {
2626
static let serviceIdentifier = "com.apple.container.apiserver"
2727

28-
private var sandboxClient: SandboxClient {
29-
SandboxClient(id: configuration.id, runtime: configuration.runtimeHandler)
30-
}
31-
3228
/// Identifier of the container.
3329
public var id: String {
3430
configuration.id
@@ -58,10 +54,6 @@ public struct ClientContainer: Sendable, Codable {
5854
self.status = snapshot.status
5955
self.networks = snapshot.networks
6056
}
61-
62-
public var initProcess: ClientProcess {
63-
ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
64-
}
6557
}
6658

6759
extension ClientContainer {
@@ -85,7 +77,7 @@ extension ClientContainer {
8577
) async throws -> ClientContainer {
8678
do {
8779
let client = Self.newClient()
88-
let request = XPCMessage(route: .createContainer)
80+
let request = XPCMessage(route: .containerCreate)
8981

9082
let data = try JSONEncoder().encode(configuration)
9183
let kdata = try JSONEncoder().encode(kernel)
@@ -108,7 +100,7 @@ extension ClientContainer {
108100
public static func list() async throws -> [ClientContainer] {
109101
do {
110102
let client = Self.newClient()
111-
let request = XPCMessage(route: .listContainer)
103+
let request = XPCMessage(route: .containerList)
112104

113105
let response = try await xpcSend(
114106
client: client,
@@ -145,16 +137,67 @@ extension ClientContainer {
145137

146138
extension ClientContainer {
147139
public func bootstrap(stdio: [FileHandle?]) async throws -> ClientProcess {
148-
let client = self.sandboxClient
149-
try await client.bootstrap(stdio: stdio)
150-
return ClientProcessImpl(containerId: self.id, client: self.sandboxClient)
140+
let request = XPCMessage(route: .containerBootstrap)
141+
let client = Self.newClient()
142+
143+
for (i, h) in stdio.enumerated() {
144+
let key: XPCKeys = {
145+
switch i {
146+
case 0: .stdin
147+
case 1: .stdout
148+
case 2: .stderr
149+
default:
150+
fatalError("invalid fd \(i)")
151+
}
152+
}()
153+
154+
if let h {
155+
request.set(key: key, value: h)
156+
}
157+
}
158+
159+
do {
160+
request.set(key: .id, value: self.id)
161+
try await client.send(request)
162+
return ClientProcessImpl(containerId: self.id, client: client)
163+
} catch {
164+
throw ContainerizationError(
165+
.internalError,
166+
message: "failed to bootstrap container",
167+
cause: error
168+
)
169+
}
170+
}
171+
172+
public func kill(_ signal: Int32) async throws {
173+
do {
174+
let request = XPCMessage(route: .containerKill)
175+
request.set(key: .id, value: id)
176+
request.set(key: .processIdentifier, value: id)
177+
request.set(key: .signal, value: Int64(signal))
178+
179+
let client = Self.newClient()
180+
try await client.send(request)
181+
} catch {
182+
throw ContainerizationError(
183+
.internalError,
184+
message: "failed to kill container",
185+
cause: error
186+
)
187+
}
151188
}
152189

153190
/// Stop the container and all processes currently executing inside.
154191
public func stop(opts: ContainerStopOptions = ContainerStopOptions.default) async throws {
155192
do {
156-
let client = self.sandboxClient
157-
try await client.stop(options: opts)
193+
let client = Self.newClient()
194+
let request = XPCMessage(route: .containerStop)
195+
let data = try JSONEncoder().encode(opts)
196+
request.set(key: .id, value: self.id)
197+
request.set(key: .stopOptions, value: data)
198+
199+
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 1)))
200+
try await client.send(request, responseTimeout: responseTimeout)
158201
} catch {
159202
throw ContainerizationError(
160203
.internalError,
@@ -167,8 +210,8 @@ extension ClientContainer {
167210
/// Delete the container along with any resources.
168211
public func delete(force: Bool = false) async throws {
169212
do {
170-
let client = XPCClient(service: Self.serviceIdentifier)
171-
let request = XPCMessage(route: .deleteContainer)
213+
let client = Self.newClient()
214+
let request = XPCMessage(route: .containerDelete)
172215
request.set(key: .id, value: self.id)
173216
request.set(key: .forceDelete, value: force)
174217
try await client.send(request)
@@ -180,46 +223,53 @@ extension ClientContainer {
180223
)
181224
}
182225
}
183-
}
184226

185-
extension ClientContainer {
186-
/// Execute a new process inside a running container.
227+
/// Create a new process inside a running container. The process is in a
228+
/// created state and must still be started.
187229
public func createProcess(
188230
id: String,
189231
configuration: ProcessConfiguration,
190232
stdio: [FileHandle?]
191233
) async throws -> ClientProcess {
192234
do {
193-
let client = self.sandboxClient
194-
try await client.createProcess(id, config: configuration, stdio: stdio)
195-
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
196-
} catch {
197-
throw ContainerizationError(
198-
.internalError,
199-
message: "failed to exec in container",
200-
cause: error
201-
)
202-
}
203-
}
235+
let request = XPCMessage(route: .containerCreateProcess)
236+
request.set(key: .id, value: self.id)
237+
request.set(key: .processIdentifier, value: id)
204238

205-
/// Send or "kill" a signal to the initial process of the container.
206-
/// Kill does not wait for the process to exit, it only delivers the signal.
207-
public func kill(_ signal: Int32) async throws {
208-
do {
209-
let client = self.sandboxClient
210-
try await client.kill(self.id, signal: Int64(signal))
239+
let data = try JSONEncoder().encode(configuration)
240+
request.set(key: .processConfig, value: data)
241+
242+
for (i, h) in stdio.enumerated() {
243+
let key: XPCKeys = {
244+
switch i {
245+
case 0: .stdin
246+
case 1: .stdout
247+
case 2: .stderr
248+
default:
249+
fatalError("invalid fd \(i)")
250+
}
251+
}()
252+
253+
if let h {
254+
request.set(key: key, value: h)
255+
}
256+
}
257+
258+
let client = Self.newClient()
259+
try await client.send(request)
260+
return ClientProcessImpl(containerId: self.id, processId: id, client: client)
211261
} catch {
212262
throw ContainerizationError(
213263
.internalError,
214-
message: "failed to kill container \(self.id)",
264+
message: "failed to create process in container",
215265
cause: error
216266
)
217267
}
218268
}
219269

220270
public func logs() async throws -> [FileHandle] {
221271
do {
222-
let client = XPCClient(service: Self.serviceIdentifier)
272+
let client = Self.newClient()
223273
let request = XPCMessage(route: .containerLogs)
224274
request.set(key: .id, value: self.id)
225275

@@ -242,15 +292,27 @@ extension ClientContainer {
242292
}
243293

244294
public func dial(_ port: UInt32) async throws -> FileHandle {
295+
let request = XPCMessage(route: .containerDial)
296+
request.set(key: .id, value: self.id)
297+
request.set(key: .port, value: UInt64(port))
298+
299+
let client = Self.newClient()
300+
let response: XPCMessage
245301
do {
246-
let client = self.sandboxClient
247-
return try await client.dial(port)
302+
response = try await client.send(request)
248303
} catch {
249304
throw ContainerizationError(
250305
.internalError,
251-
message: "failed to dial \(port) in container \(self.id)",
306+
message: "failed to dial port \(port) on container",
252307
cause: error
253308
)
254309
}
310+
guard let fh = response.fileHandle(key: .fd) else {
311+
throw ContainerizationError(
312+
.internalError,
313+
message: "failed to get fd for vsock port \(port)"
314+
)
315+
}
316+
return fh
255317
}
256318
}

Sources/ContainerClient/Core/ClientProcess.swift

Lines changed: 38 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public protocol ClientProcess: Sendable {
3535
func start() async throws
3636
/// Send a terminal resize request to the process `id`.
3737
func resize(_ size: Terminal.Size) async throws
38-
/// Send or "kill" a signal to the process `id`.
38+
/// Send a signal to the process `id`.
3939
/// Kill does not wait for the process to exit, it only delivers the signal.
4040
func kill(_ signal: Int32) async throws
4141
/// Wait for the process `id` to complete and return its exit code.
@@ -45,79 +45,66 @@ public protocol ClientProcess: Sendable {
4545

4646
struct ClientProcessImpl: ClientProcess, Sendable {
4747
static let serviceIdentifier = "com.apple.container.apiserver"
48+
49+
/// ID of the process.
50+
public var id: String {
51+
processId ?? containerId
52+
}
53+
4854
/// Identifier of the container.
4955
public let containerId: String
5056

51-
private let client: SandboxClient
52-
5357
/// Identifier of a process. That is running inside of a container.
5458
/// This field is nil if the process this objects refers to is the
5559
/// init process of the container.
5660
public let processId: String?
5761

58-
public var id: String {
59-
processId ?? containerId
60-
}
62+
private let client: XPCClient
6163

62-
init(containerId: String, processId: String? = nil, client: SandboxClient) {
64+
init(containerId: String, processId: String? = nil, client: XPCClient) {
6365
self.containerId = containerId
6466
self.processId = processId
6567
self.client = client
6668
}
6769

68-
/// Start the container and return the initial process.
70+
/// Start the process.
6971
public func start() async throws {
70-
do {
71-
let client = self.client
72-
try await client.startProcess(self.id)
73-
} catch {
74-
throw ContainerizationError(
75-
.internalError,
76-
message: "failed to start container",
77-
cause: error
78-
)
79-
}
72+
let request = XPCMessage(route: .containerStartProcess)
73+
request.set(key: .id, value: containerId)
74+
request.set(key: .processIdentifier, value: id)
75+
76+
try await client.send(request)
8077
}
8178

79+
/// Send a signal to the process.
8280
public func kill(_ signal: Int32) async throws {
83-
do {
84-
85-
let client = self.client
86-
try await client.kill(self.id, signal: Int64(signal))
87-
} catch {
88-
throw ContainerizationError(
89-
.internalError,
90-
message: "failed to kill process",
91-
cause: error
92-
)
93-
}
94-
}
81+
let request = XPCMessage(route: .containerKill)
82+
request.set(key: .id, value: containerId)
83+
request.set(key: .processIdentifier, value: id)
84+
request.set(key: .signal, value: Int64(signal))
9585

96-
public func resize(_ size: ContainerizationOS.Terminal.Size) async throws {
97-
do {
86+
try await client.send(request)
87+
}
9888

99-
let client = self.client
100-
try await client.resize(self.id, size: size)
89+
/// Resize the processes PTY if it has one.
90+
public func resize(_ size: Terminal.Size) async throws {
91+
let request = XPCMessage(route: .containerResize)
92+
request.set(key: .id, value: containerId)
93+
request.set(key: .processIdentifier, value: id)
94+
request.set(key: .width, value: UInt64(size.width))
95+
request.set(key: .height, value: UInt64(size.height))
10196

102-
} catch {
103-
throw ContainerizationError(
104-
.internalError,
105-
message: "failed to resize process",
106-
cause: error
107-
)
108-
}
97+
try await client.send(request)
10998
}
11099

100+
/// Wait for the process to exit.
111101
public func wait() async throws -> Int32 {
112-
do {
113-
let client = self.client
114-
return try await client.wait(self.id)
115-
} catch {
116-
throw ContainerizationError(
117-
.internalError,
118-
message: "failed to wait on process",
119-
cause: error
120-
)
121-
}
102+
let request = XPCMessage(route: .containerWait)
103+
request.set(key: .id, value: containerId)
104+
request.set(key: .processIdentifier, value: id)
105+
106+
let response = try await client.send(request)
107+
let code = response.int64(key: .exitCode)
108+
return Int32(code)
122109
}
123110
}

0 commit comments

Comments
 (0)