Skip to content

Commit 18b447a

Browse files
committed
remove api server work + add runtime api
1 parent ef9a515 commit 18b447a

10 files changed

Lines changed: 52 additions & 186 deletions

File tree

Sources/APIServer/APIServer+Start.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -297,8 +297,6 @@ extension APIServer {
297297
routes[XPCRoute.containerBootstrap] = XPCServer.route(harness.bootstrap)
298298
routes[XPCRoute.containerDial] = XPCServer.route(harness.dial)
299299
routes[XPCRoute.containerStop] = XPCServer.route(harness.stop)
300-
routes[XPCRoute.containerFreeze] = XPCServer.route(harness.freeze)
301-
routes[XPCRoute.containerThaw] = XPCServer.route(harness.thaw)
302300
routes[XPCRoute.containerStartProcess] = XPCServer.route(harness.startProcess)
303301
routes[XPCRoute.containerCreateProcess] = XPCServer.route(harness.createProcess)
304302
routes[XPCRoute.containerResize] = XPCServer.route(harness.resize)

Sources/Plugins/RuntimeLinux/RuntimeLinuxHelper+Start.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,9 @@ extension RuntimeLinuxHelper {
104104
RuntimeRoutes.dial.rawValue: XPCServer.route(server.dial),
105105
RuntimeRoutes.shutdown.rawValue: XPCServer.route(server.shutdown),
106106
RuntimeRoutes.statistics.rawValue: XPCServer.route(server.statistics),
107-
RuntimeRoutes.filesystemOperation.rawValue: XPCServer.route(server.filesystemOperation),
108107
RuntimeRoutes.copyIn.rawValue: XPCServer.route(server.copyIn),
109108
RuntimeRoutes.copyOut.rawValue: XPCServer.route(server.copyOut),
109+
RuntimeRoutes.snapshotDisk.rawValue: XPCServer.route(server.snapshotDisk),
110110
],
111111
log: log
112112
)

Sources/Services/ContainerAPIService/Client/ContainerClient.swift

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -193,38 +193,6 @@ public struct ContainerClient: Sendable {
193193
}
194194
}
195195

196-
/// Freeze writes on the container root filesystem.
197-
public func freeze(id: String) async throws {
198-
do {
199-
let request = XPCMessage(route: .containerFreeze)
200-
request.set(key: .id, value: id)
201-
202-
try await xpcClient.send(request)
203-
} catch {
204-
throw ContainerizationError(
205-
.internalError,
206-
message: "failed to freeze container",
207-
cause: error
208-
)
209-
}
210-
}
211-
212-
/// Thaw writes on the container root filesystem.
213-
public func thaw(id: String) async throws {
214-
do {
215-
let request = XPCMessage(route: .containerThaw)
216-
request.set(key: .id, value: id)
217-
218-
try await xpcClient.send(request)
219-
} catch {
220-
throw ContainerizationError(
221-
.internalError,
222-
message: "failed to thaw container",
223-
cause: error
224-
)
225-
}
226-
}
227-
228196
/// Delete the container along with any resources.
229197
public func delete(id: String, force: Bool = false) async throws {
230198
do {

Sources/Services/ContainerAPIService/Client/XPC+.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,6 @@ public enum XPCRoute: String {
156156
case containerWait
157157
case containerDelete
158158
case containerStop
159-
case containerFreeze
160-
case containerThaw
161159
case containerDial
162160
case containerResize
163161
case containerKill

Sources/Services/ContainerAPIService/Server/Containers/ContainersHarness.swift

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -77,32 +77,6 @@ public struct ContainersHarness: Sendable {
7777
return message.reply()
7878
}
7979

80-
@Sendable
81-
public func freeze(_ message: XPCMessage) async throws -> XPCMessage {
82-
let id = message.string(key: .id)
83-
guard let id else {
84-
throw ContainerizationError(
85-
.invalidArgument,
86-
message: "id cannot be empty"
87-
)
88-
}
89-
try await service.freeze(id: id)
90-
return message.reply()
91-
}
92-
93-
@Sendable
94-
public func thaw(_ message: XPCMessage) async throws -> XPCMessage {
95-
let id = message.string(key: .id)
96-
guard let id else {
97-
throw ContainerizationError(
98-
.invalidArgument,
99-
message: "id cannot be empty"
100-
)
101-
}
102-
try await service.thaw(id: id)
103-
return message.reply()
104-
}
105-
10680
@Sendable
10781
public func dial(_ message: XPCMessage) async throws -> XPCMessage {
10882
let id = message.string(key: .id)

Sources/Services/ContainerAPIService/Server/Containers/ContainersService.swift

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -645,68 +645,6 @@ public actor ContainersService {
645645
try await handleContainerExit(id: id)
646646
}
647647

648-
/// Freeze writes on the root filesystem of a running container.
649-
public func freeze(id: String) async throws {
650-
log.debug(
651-
"ContainersService: enter",
652-
metadata: [
653-
"func": "\(#function)",
654-
"id": "\(id)",
655-
]
656-
)
657-
defer {
658-
log.debug(
659-
"ContainersService: exit",
660-
metadata: [
661-
"func": "\(#function)",
662-
"id": "\(id)",
663-
]
664-
)
665-
}
666-
667-
let state = try self._getContainerState(id: id)
668-
guard state.snapshot.status == .running else {
669-
throw ContainerizationError(
670-
.invalidState,
671-
message: "container \(id) is \(state.snapshot.status) and cannot be frozen"
672-
)
673-
}
674-
675-
let client = try state.getClient()
676-
try await client.filesystemOperation(operation: .freeze, path: "/")
677-
}
678-
679-
/// Thaw writes on the root filesystem of a running container.
680-
public func thaw(id: String) async throws {
681-
log.debug(
682-
"ContainersService: enter",
683-
metadata: [
684-
"func": "\(#function)",
685-
"id": "\(id)",
686-
]
687-
)
688-
defer {
689-
log.debug(
690-
"ContainersService: exit",
691-
metadata: [
692-
"func": "\(#function)",
693-
"id": "\(id)",
694-
]
695-
)
696-
}
697-
698-
let state = try self._getContainerState(id: id)
699-
guard state.snapshot.status == .running else {
700-
throw ContainerizationError(
701-
.invalidState,
702-
message: "container \(id) is \(state.snapshot.status) and cannot be thawed"
703-
)
704-
}
705-
706-
let client = try state.getClient()
707-
try await client.filesystemOperation(operation: .thaw, path: "/")
708-
}
709-
710648
public func dial(id: String, port: UInt32) async throws -> FileHandle {
711649
log.debug(
712650
"ContainersService: enter",
@@ -973,23 +911,10 @@ public actor ContainersService {
973911

974912
if live {
975913
let client = try state.getClient()
976-
try await client.filesystemOperation(operation: .freeze, path: "/")
977-
do {
978-
try EXT4.EXT4Reader(blockDevice: FilePath(rootfs)).export(archive: FilePath(archive))
979-
} catch {
980-
do {
981-
try await client.filesystemOperation(operation: .thaw, path: "/")
982-
} catch {
983-
self.log.error(
984-
"failed to thaw filesystem after live export error",
985-
metadata: [
986-
"id": "\(id)",
987-
"error": "\(error)",
988-
])
989-
}
990-
throw error
991-
}
992-
try await client.filesystemOperation(operation: .thaw, path: "/")
914+
let snapshot = rootfs.appendingPathExtension("snapshot")
915+
defer { try? FileManager.default.removeItem(at: snapshot) }
916+
try await client.snapshotDisk(imagePath: rootfs.path, destinationPath: snapshot.path)
917+
try EXT4.EXT4Reader(blockDevice: FilePath(snapshot)).export(archive: FilePath(archive))
993918
return
994919
}
995920

Sources/Services/Runtime/RuntimeClient/RuntimeClient.swift

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -319,25 +319,17 @@ extension RuntimeClient {
319319
}
320320
}
321321

322-
public func filesystemOperation(operation: FilesystemOperation, path: String) async throws {
323-
let request = XPCMessage(route: RuntimeRoutes.filesystemOperation.rawValue)
324-
request.set(
325-
key: RuntimeKeys.filesystemOperation.rawValue,
326-
value: {
327-
switch operation {
328-
case .freeze: "freeze"
329-
case .thaw: "thaw"
330-
case .trim: "trim"
331-
}
332-
}())
333-
request.set(key: RuntimeKeys.filesystemPath.rawValue, value: path)
322+
public func snapshotDisk(imagePath: String, destinationPath: String) async throws {
323+
let request = XPCMessage(route: RuntimeRoutes.snapshotDisk.rawValue)
324+
request.set(key: RuntimeKeys.imagePath.rawValue, value: imagePath)
325+
request.set(key: RuntimeKeys.destinationPath.rawValue, value: destinationPath)
334326

335327
do {
336328
try await self.client.send(request, responseTimeout: .seconds(300))
337329
} catch {
338330
throw ContainerizationError(
339331
.internalError,
340-
message: "failed to perform filesystem operation in container \(self.id)",
332+
message: "failed to snapshot disk in container \(self.id)",
341333
cause: error
342334
)
343335
}

Sources/Services/Runtime/RuntimeClient/RuntimeKeys.swift

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,12 @@ public enum RuntimeKeys: String {
4848
case destinationPath
4949
case fileMode
5050
case createParents
51+
/// Image path for snapshot operations
52+
case imagePath
5153

5254
/// Special-case environment variables recomputed on each container start
5355
case dynamicEnv
5456

55-
/// Filesystem operation to perform inside the guest.
56-
case filesystemOperation
57-
/// Target path for a guest filesystem operation.
58-
case filesystemPath
59-
6057
/// Per-network connection info passed to the runtime so it can allocate directly.
6158
case networkBootstrapInfos
6259
}

Sources/Services/Runtime/RuntimeClient/RuntimeRoutes.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,10 @@ public enum RuntimeRoutes: String {
5252
case exec = "com.apple.container.runtime/exec"
5353

5454
// MARK: - File Management
55-
/// Perform a filesystem operation inside the container.
56-
case filesystemOperation = "com.apple.container.runtime/filesystemOperation"
5755
/// Copy a file or directory into the container.
5856
case copyIn = "com.apple.container.runtime/copyIn"
5957
/// Copy a file or directory out of the container.
6058
case copyOut = "com.apple.container.runtime/copyOut"
59+
/// Snapshot the container's root filesystem to an image file.
60+
case snapshotDisk = "com.apple.container.runtime/snapshotDisk"
6161
}

Sources/Services/RuntimeLinux/Server/RuntimeService.swift

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -772,35 +772,63 @@ public actor RuntimeService {
772772
}
773773
}
774774

775-
/// Perform a filesystem operation inside the container.
775+
/// Snapshot the container's root filesystem by freezing it, cloning it to a destination image,
776+
/// and then thawing it. This ensures the filesystem is frozen for the minimal duration.
776777
///
777778
/// - Parameters:
778779
/// - message: An XPC message with the following parameters:
779-
/// - filesystemOperation: The operation to perform.
780-
/// - filesystemPath: The target path inside the container.
780+
/// - imagePath: The path to the source filesystem image.
781+
/// - destinationPath: The path where the snapshot will be written.
781782
///
782783
/// - Returns: An XPC message with no parameters.
783784
@Sendable
784-
public func filesystemOperation(_ message: XPCMessage) async throws -> XPCMessage {
785-
self.log.info("`filesystemOperation` xpc handler")
785+
public func snapshotDisk(_ message: XPCMessage) async throws -> XPCMessage {
786+
self.log.info("`snapshotDisk` xpc handler")
786787
switch self.state {
787788
case .running, .booted:
788-
let operation = try message.filesystemOperation()
789-
guard let path = message.string(key: RuntimeKeys.filesystemPath.rawValue) else {
789+
guard let imagePath = message.string(key: RuntimeKeys.imagePath.rawValue) else {
790790
throw ContainerizationError(
791791
.invalidArgument,
792-
message: "no filesystem path supplied for filesystemOperation"
792+
message: "no image path supplied for snapshotDisk"
793+
)
794+
}
795+
guard let destinationPath = message.string(key: RuntimeKeys.destinationPath.rawValue) else {
796+
throw ContainerizationError(
797+
.invalidArgument,
798+
message: "no destination path supplied for snapshotDisk"
793799
)
794800
}
795801

796802
let ctr = try getContainer()
797-
try await ctr.container.filesystemOperation(operation: operation, path: path)
803+
804+
// Freeze the filesystem
805+
try await ctr.container.filesystemOperation(operation: .freeze, path: "/")
806+
807+
do {
808+
// Clone the filesystem image atomically while frozen
809+
try FileManager.default.copyItem(atPath: imagePath, toPath: destinationPath)
810+
} catch {
811+
// Ensure we thaw even on error
812+
do {
813+
try await ctr.container.filesystemOperation(operation: .thaw, path: "/")
814+
} catch {
815+
self.log.error(
816+
"failed to thaw filesystem after snapshotDisk error",
817+
metadata: [
818+
"error": "\(error)"
819+
])
820+
}
821+
throw error
822+
}
823+
824+
// Thaw the filesystem
825+
try await ctr.container.filesystemOperation(operation: .thaw, path: "/")
798826

799827
return message.reply()
800828
default:
801829
throw ContainerizationError(
802830
.invalidState,
803-
message: "cannot perform filesystem operation: container is not running"
831+
message: "cannot snapshot disk: container is not running"
804832
)
805833
}
806834
}
@@ -1357,20 +1385,6 @@ extension XPCMessage {
13571385
return dynamicEnv
13581386
}
13591387

1360-
fileprivate func filesystemOperation() throws -> FilesystemOperation {
1361-
guard let operation = self.string(key: RuntimeKeys.filesystemOperation.rawValue) else {
1362-
throw ContainerizationError(.invalidArgument, message: "empty filesystem operation")
1363-
}
1364-
switch operation {
1365-
case "freeze":
1366-
return .freeze
1367-
case "thaw":
1368-
return .thaw
1369-
default:
1370-
throw ContainerizationError(.invalidArgument, message: "invalid filesystem operation \(operation)")
1371-
}
1372-
}
1373-
13741388
}
13751389

13761390
extension ContainerResource.Bundle {

0 commit comments

Comments
 (0)