Skip to content

Commit 444064d

Browse files
authored
Swap to APIServer for all communications (#628)
1 parent 07f1d60 commit 444064d

File tree

21 files changed

+1254
-568
lines changed

21 files changed

+1254
-568
lines changed

Sources/ContainerClient/ContainerEvents.swift

Lines changed: 0 additions & 20 deletions
This file was deleted.

Sources/ContainerClient/Core/ClientContainer.swift

Lines changed: 112 additions & 45 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,14 +54,10 @@ 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 {
68-
private static func newClient() -> XPCClient {
60+
private static func newXPCClient() -> XPCClient {
6961
XPCClient(service: serviceIdentifier)
7062
}
7163

@@ -84,8 +76,8 @@ extension ClientContainer {
8476
kernel: Kernel
8577
) async throws -> ClientContainer {
8678
do {
87-
let client = Self.newClient()
88-
let request = XPCMessage(route: .createContainer)
79+
let client = Self.newXPCClient()
80+
let request = XPCMessage(route: .containerCreate)
8981

9082
let data = try JSONEncoder().encode(configuration)
9183
let kdata = try JSONEncoder().encode(kernel)
@@ -107,8 +99,8 @@ extension ClientContainer {
10799

108100
public static func list() async throws -> [ClientContainer] {
109101
do {
110-
let client = Self.newClient()
111-
let request = XPCMessage(route: .listContainer)
102+
let client = Self.newXPCClient()
103+
let request = XPCMessage(route: .containerList)
112104

113105
let response = try await xpcSend(
114106
client: client,
@@ -145,16 +137,72 @@ 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.newXPCClient()
142+
143+
for (i, h) in stdio.enumerated() {
144+
let key: XPCKeys = try {
145+
switch i {
146+
case 0: .stdin
147+
case 1: .stdout
148+
case 2: .stderr
149+
default:
150+
throw ContainerizationError(.invalidArgument, message: "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, xpcClient: 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: self.id)
176+
request.set(key: .processIdentifier, value: self.id)
177+
request.set(key: .signal, value: Int64(signal))
178+
179+
let client = Self.newXPCClient()
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.newXPCClient()
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+
// Stop is somewhat more prone to hanging than other commands given it
200+
// has quite a bit of `wait()`'s down the chain to make sure the container actually
201+
// exited. To combat a potential hang, lets timeout if we don't return in a small
202+
// time period after the actual stop timeout sent via .stopOptions (the time
203+
// until we send SIGKILL after SIGTERM if the container still hasn't exited).
204+
let responseTimeout = Duration(.seconds(Int64(opts.timeoutInSeconds + 3)))
205+
try await client.send(request, responseTimeout: responseTimeout)
158206
} catch {
159207
throw ContainerizationError(
160208
.internalError,
@@ -167,8 +215,8 @@ extension ClientContainer {
167215
/// Delete the container along with any resources.
168216
public func delete(force: Bool = false) async throws {
169217
do {
170-
let client = XPCClient(service: Self.serviceIdentifier)
171-
let request = XPCMessage(route: .deleteContainer)
218+
let client = Self.newXPCClient()
219+
let request = XPCMessage(route: .containerDelete)
172220
request.set(key: .id, value: self.id)
173221
request.set(key: .forceDelete, value: force)
174222
try await client.send(request)
@@ -180,46 +228,53 @@ extension ClientContainer {
180228
)
181229
}
182230
}
183-
}
184231

185-
extension ClientContainer {
186-
/// Execute a new process inside a running container.
232+
/// Create a new process inside a running container. The process is in a
233+
/// created state and must still be started.
187234
public func createProcess(
188235
id: String,
189236
configuration: ProcessConfiguration,
190237
stdio: [FileHandle?]
191238
) async throws -> ClientProcess {
192239
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-
}
240+
let request = XPCMessage(route: .containerCreateProcess)
241+
request.set(key: .id, value: self.id)
242+
request.set(key: .processIdentifier, value: id)
204243

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))
244+
let data = try JSONEncoder().encode(configuration)
245+
request.set(key: .processConfig, value: data)
246+
247+
for (i, h) in stdio.enumerated() {
248+
let key: XPCKeys = try {
249+
switch i {
250+
case 0: .stdin
251+
case 1: .stdout
252+
case 2: .stderr
253+
default:
254+
throw ContainerizationError(.invalidArgument, message: "invalid fd \(i)")
255+
}
256+
}()
257+
258+
if let h {
259+
request.set(key: key, value: h)
260+
}
261+
}
262+
263+
let client = Self.newXPCClient()
264+
try await client.send(request)
265+
return ClientProcessImpl(containerId: self.id, processId: id, xpcClient: client)
211266
} catch {
212267
throw ContainerizationError(
213268
.internalError,
214-
message: "failed to kill container \(self.id)",
269+
message: "failed to create process in container",
215270
cause: error
216271
)
217272
}
218273
}
219274

220275
public func logs() async throws -> [FileHandle] {
221276
do {
222-
let client = XPCClient(service: Self.serviceIdentifier)
277+
let client = Self.newXPCClient()
223278
let request = XPCMessage(route: .containerLogs)
224279
request.set(key: .id, value: self.id)
225280

@@ -242,15 +297,27 @@ extension ClientContainer {
242297
}
243298

244299
public func dial(_ port: UInt32) async throws -> FileHandle {
300+
let request = XPCMessage(route: .containerDial)
301+
request.set(key: .id, value: self.id)
302+
request.set(key: .port, value: UInt64(port))
303+
304+
let client = Self.newXPCClient()
305+
let response: XPCMessage
245306
do {
246-
let client = self.sandboxClient
247-
return try await client.dial(port)
307+
response = try await client.send(request)
248308
} catch {
249309
throw ContainerizationError(
250310
.internalError,
251-
message: "failed to dial \(port) in container \(self.id)",
311+
message: "failed to dial port \(port) on container",
252312
cause: error
253313
)
254314
}
315+
guard let fh = response.fileHandle(key: .fd) else {
316+
throw ContainerizationError(
317+
.internalError,
318+
message: "failed to get fd for vsock port \(port)"
319+
)
320+
}
321+
return fh
255322
}
256323
}

Sources/ContainerClient/Core/ClientProcess.swift

Lines changed: 39 additions & 52 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 xpcClient: XPCClient
6163

62-
init(containerId: String, processId: String? = nil, client: SandboxClient) {
64+
init(containerId: String, processId: String? = nil, xpcClient: XPCClient) {
6365
self.containerId = containerId
6466
self.processId = processId
65-
self.client = client
67+
self.xpcClient = xpcClient
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 xpcClient.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 xpcClient.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 xpcClient.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 xpcClient.send(request)
107+
let code = response.int64(key: .exitCode)
108+
return Int32(code)
122109
}
123110
}

0 commit comments

Comments
 (0)