Skip to content

Commit ab3cf6a

Browse files
authored
Merge branch 'main' into fix/467-validate-nameserver-ip-addresses
2 parents caaab25 + d3ff56e commit ab3cf6a

17 files changed

Lines changed: 1831 additions & 960 deletions

Package.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ let package = Package(
6060
.product(name: "GRPC", package: "grpc-swift"),
6161
.product(name: "SystemPackage", package: "swift-system"),
6262
.product(name: "_NIOFileSystem", package: "swift-nio"),
63+
"ContainerizationArchive",
6364
"ContainerizationOCI",
6465
"ContainerizationOS",
6566
"ContainerizationIO",

Sources/Containerization/LinuxContainer.swift

Lines changed: 194 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//===----------------------------------------------------------------------===//
1616

1717
#if os(macOS)
18+
import ContainerizationArchive
1819
import ContainerizationError
1920
import ContainerizationExtras
2021
import ContainerizationOCI
@@ -123,6 +124,9 @@ public final class LinuxContainer: Container, Sendable {
123124
// the host.
124125
private let guestVsockPorts: Atomic<UInt32>
125126

127+
// Queue for copy IO.
128+
private let copyQueue = DispatchQueue(label: "com.apple.containerization.copy")
129+
126130
private enum State: Sendable {
127131
/// The container class has been created but no live resources are running.
128132
case initialized
@@ -1043,52 +1047,219 @@ extension LinuxContainer {
10431047
/// Default chunk size for file transfers (1MiB).
10441048
public static let defaultCopyChunkSize = 1024 * 1024
10451049

1046-
/// Copy a file from the host into the container.
1050+
/// Copy a file or directory from the host into the container.
1051+
///
1052+
/// Data transfer happens over a dedicated vsock connection. For directories,
1053+
/// the source is archived as tar+gzip and streamed directly through vsock
1054+
/// without intermediate temp files.
10471055
public func copyIn(
10481056
from source: URL,
10491057
to destination: URL,
10501058
mode: UInt32 = 0o644,
10511059
createParents: Bool = true,
1052-
chunkSize: Int = defaultCopyChunkSize,
1053-
progress: ProgressHandler? = nil
1060+
chunkSize: Int = defaultCopyChunkSize
10541061
) async throws {
10551062
try await self.state.withLock {
10561063
let state = try $0.startedState("copyIn")
10571064

1065+
var isDirectory: ObjCBool = false
1066+
guard FileManager.default.fileExists(atPath: source.path, isDirectory: &isDirectory) else {
1067+
throw ContainerizationError(.notFound, message: "copyIn: source not found '\(source.path)'")
1068+
}
1069+
let isArchive = isDirectory.boolValue
1070+
10581071
let guestPath = URL(filePath: self.root).appending(path: destination.path)
1059-
try await state.vm.withAgent { agent in
1060-
try await agent.copyIn(
1061-
from: source,
1062-
to: guestPath,
1063-
mode: mode,
1064-
createParents: createParents,
1065-
chunkSize: chunkSize,
1066-
progress: progress
1067-
)
1072+
let port = self.hostVsockPorts.wrappingAdd(1, ordering: .relaxed).oldValue
1073+
let listener = try state.vm.listen(port)
1074+
1075+
try await withThrowingTaskGroup(of: Void.self) { group in
1076+
group.addTask {
1077+
try await state.vm.withAgent { agent in
1078+
guard let vminitd = agent as? Vminitd else {
1079+
throw ContainerizationError(.unsupported, message: "copyIn requires Vminitd agent")
1080+
}
1081+
try await vminitd.copy(
1082+
direction: .copyIn,
1083+
guestPath: guestPath,
1084+
vsockPort: port,
1085+
mode: mode,
1086+
createParents: createParents,
1087+
isArchive: isArchive
1088+
)
1089+
}
1090+
}
1091+
1092+
group.addTask {
1093+
guard let conn = await listener.first(where: { _ in true }) else {
1094+
throw ContainerizationError(.internalError, message: "copyIn: vsock connection not established")
1095+
}
1096+
try listener.finish()
1097+
1098+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
1099+
self.copyQueue.async {
1100+
do {
1101+
defer { conn.closeFile() }
1102+
1103+
if isArchive {
1104+
let writer = try ArchiveWriter(configuration: .init(format: .pax, filter: .gzip))
1105+
try writer.open(fileDescriptor: conn.fileDescriptor)
1106+
try writer.archiveDirectory(source)
1107+
try writer.finishEncoding()
1108+
} else {
1109+
let srcFd = open(source.path, O_RDONLY)
1110+
guard srcFd != -1 else {
1111+
throw ContainerizationError(
1112+
.internalError,
1113+
message: "copyIn: failed to open '\(source.path)': \(String(cString: strerror(errno)))"
1114+
)
1115+
}
1116+
defer { close(srcFd) }
1117+
1118+
var buf = [UInt8](repeating: 0, count: chunkSize)
1119+
while true {
1120+
let n = read(srcFd, &buf, buf.count)
1121+
if n == 0 { break }
1122+
guard n > 0 else {
1123+
throw ContainerizationError(
1124+
.internalError,
1125+
message: "copyIn: read error: \(String(cString: strerror(errno)))"
1126+
)
1127+
}
1128+
var written = 0
1129+
while written < n {
1130+
let w = buf.withUnsafeBytes { ptr in
1131+
write(conn.fileDescriptor, ptr.baseAddress! + written, n - written)
1132+
}
1133+
guard w > 0 else {
1134+
throw ContainerizationError(
1135+
.internalError,
1136+
message: "copyIn: vsock write error: \(String(cString: strerror(errno)))"
1137+
)
1138+
}
1139+
written += w
1140+
}
1141+
}
1142+
}
1143+
continuation.resume()
1144+
} catch {
1145+
continuation.resume(throwing: error)
1146+
}
1147+
}
1148+
}
1149+
}
1150+
1151+
try await group.waitForAll()
10681152
}
10691153
}
10701154
}
10711155

1072-
/// Copy a file from the container to the host.
1156+
/// Copy a file or directory from the container to the host.
1157+
///
1158+
/// Data transfer happens over a dedicated vsock connection. For directories,
1159+
/// the guest archives the source as tar+gzip and streams it directly through
1160+
/// vsock. The host extracts the archive without intermediate temp files.
10731161
public func copyOut(
10741162
from source: URL,
10751163
to destination: URL,
10761164
createParents: Bool = true,
1077-
chunkSize: Int = defaultCopyChunkSize,
1078-
progress: ProgressHandler? = nil
1165+
chunkSize: Int = defaultCopyChunkSize
10791166
) async throws {
10801167
try await self.state.withLock {
10811168
let state = try $0.startedState("copyOut")
10821169

1170+
if createParents {
1171+
let parentDir = destination.deletingLastPathComponent()
1172+
try FileManager.default.createDirectory(at: parentDir, withIntermediateDirectories: true)
1173+
}
1174+
10831175
let guestPath = URL(filePath: self.root).appending(path: source.path)
1084-
try await state.vm.withAgent { agent in
1085-
try await agent.copyOut(
1086-
from: guestPath,
1087-
to: destination,
1088-
createParents: createParents,
1089-
chunkSize: chunkSize,
1090-
progress: progress
1091-
)
1176+
let port = self.hostVsockPorts.wrappingAdd(1, ordering: .relaxed).oldValue
1177+
let listener = try state.vm.listen(port)
1178+
1179+
let (metadataStream, metadataCont) = AsyncStream.makeStream(of: Vminitd.CopyMetadata.self)
1180+
1181+
try await withThrowingTaskGroup(of: Void.self) { group in
1182+
group.addTask {
1183+
try await state.vm.withAgent { agent in
1184+
guard let vminitd = agent as? Vminitd else {
1185+
throw ContainerizationError(.unsupported, message: "copyOut requires Vminitd agent")
1186+
}
1187+
try await vminitd.copy(
1188+
direction: .copyOut,
1189+
guestPath: guestPath,
1190+
vsockPort: port,
1191+
onMetadata: { meta in
1192+
metadataCont.yield(meta)
1193+
metadataCont.finish()
1194+
}
1195+
)
1196+
}
1197+
}
1198+
1199+
group.addTask {
1200+
guard let metadata = await metadataStream.first(where: { _ in true }) else {
1201+
throw ContainerizationError(.internalError, message: "copyOut: no metadata received")
1202+
}
1203+
1204+
guard let conn = await listener.first(where: { _ in true }) else {
1205+
throw ContainerizationError(.internalError, message: "copyOut: vsock connection not established")
1206+
}
1207+
try listener.finish()
1208+
1209+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
1210+
self.copyQueue.async {
1211+
do {
1212+
defer { conn.closeFile() }
1213+
1214+
if metadata.isArchive {
1215+
try FileManager.default.createDirectory(at: destination, withIntermediateDirectories: true)
1216+
let fh = FileHandle(fileDescriptor: dup(conn.fileDescriptor), closeOnDealloc: true)
1217+
let reader = try ArchiveReader(format: .pax, filter: .gzip, fileHandle: fh)
1218+
_ = try reader.extractContents(to: destination)
1219+
} else {
1220+
let destFd = open(destination.path, O_WRONLY | O_CREAT | O_TRUNC, 0o644)
1221+
guard destFd != -1 else {
1222+
throw ContainerizationError(
1223+
.internalError,
1224+
message: "copyOut: failed to open '\(destination.path)': \(String(cString: strerror(errno)))"
1225+
)
1226+
}
1227+
defer { close(destFd) }
1228+
1229+
var buf = [UInt8](repeating: 0, count: chunkSize)
1230+
while true {
1231+
let n = read(conn.fileDescriptor, &buf, buf.count)
1232+
if n == 0 { break }
1233+
guard n > 0 else {
1234+
throw ContainerizationError(
1235+
.internalError,
1236+
message: "copyOut: vsock read error: \(String(cString: strerror(errno)))"
1237+
)
1238+
}
1239+
var written = 0
1240+
while written < n {
1241+
let w = buf.withUnsafeBytes { ptr in
1242+
write(destFd, ptr.baseAddress! + written, n - written)
1243+
}
1244+
guard w > 0 else {
1245+
throw ContainerizationError(
1246+
.internalError,
1247+
message: "copyOut: write error: \(String(cString: strerror(errno)))"
1248+
)
1249+
}
1250+
written += w
1251+
}
1252+
}
1253+
}
1254+
continuation.resume()
1255+
} catch {
1256+
continuation.resume(throwing: error)
1257+
}
1258+
}
1259+
}
1260+
}
1261+
1262+
try await group.waitForAll()
10921263
}
10931264
}
10941265
}

0 commit comments

Comments
 (0)