@@ -235,17 +235,23 @@ public actor ContainersService {
235235 let systemPlatform = kernel. platform
236236 let initFs = try await self . getInitBlock ( for: systemPlatform. ociPlatform ( ) )
237237
238- let bundle = try ContainerResource . Bundle. create (
239- path: path,
240- initialFilesystem: initFs,
241- kernel: kernel,
242- containerConfiguration: configuration
243- )
238+ let metadataPath = FileManager . default. temporaryDirectory
239+ . appendingPathComponent ( " bundle-metadata- \( configuration. id) .json " )
240+
244241 do {
245242 let containerImage = ClientImage ( description: configuration. image)
246243 let imageFs = try await containerImage. getCreateSnapshot ( platform: configuration. platform)
247- try bundle. setContainerRootFs ( cloning: imageFs, readonly: configuration. readOnly)
248- try bundle. write ( filename: " options.json " , value: options)
244+
245+ let completeMetadata = BundleMetadata (
246+ path: path,
247+ initialFilesystem: initFs,
248+ kernel: kernel,
249+ containerConfiguration: configuration,
250+ containerRootFilesystem: imageFs,
251+ options: options
252+ )
253+
254+ try ContainerResource . Bundle. writeMetadata ( completeMetadata, to: metadataPath)
249255
250256 let snapshot = ContainerSnapshot (
251257 configuration: configuration,
@@ -255,11 +261,8 @@ public actor ContainersService {
255261 )
256262 await self . setContainerState ( configuration. id, ContainerState ( snapshot: snapshot) , context: context)
257263 } catch {
258- do {
259- try bundle. delete ( )
260- } catch {
261- self . log. error ( " failed to delete bundle for container \( configuration. id) : \( error) " )
262- }
264+ try ? FileManager . default. removeItem ( at: metadataPath)
265+ self . log. error ( " failed to cleanup metadata file for container \( configuration. id) " )
263266 throw error
264267 }
265268 }
@@ -279,8 +282,7 @@ public actor ContainersService {
279282 }
280283
281284 let path = self . containerRoot. appendingPathComponent ( id)
282- let bundle = ContainerResource . Bundle ( path: path)
283- let config = try bundle. configuration
285+ let config = try await self . getContainerConfiguration ( for: id, at: path)
284286
285287 do {
286288 try Self . registerService (
@@ -295,6 +297,13 @@ public actor ContainersService {
295297 id: id,
296298 runtime: runtime
297299 )
300+
301+ // Create bundle if it doesn't exist
302+ if !( await self . bundleExists ( at: path) ) {
303+ let metadataPath = Self . getMetadataPath ( for: id)
304+ try await sandboxClient. createBundle ( metadataPath: metadataPath)
305+ }
306+
298307 try await sandboxClient. bootstrap ( stdio: stdio)
299308
300309 try await self . exitMonitor. registerProcess (
@@ -596,15 +605,39 @@ public actor ContainersService {
596605 // the OCI runtime.
597606 await self . exitMonitor. stopTracking ( id: id)
598607 let path = self . containerRoot. appendingPathComponent ( id)
608+
609+ // Try to get config for service deregistration, but don't fail if bundle is incomplete
610+ var config : ContainerConfiguration ?
599611 let bundle = ContainerResource . Bundle ( path: path)
600- let config = try bundle. configuration
612+ do {
613+ config = try bundle. configuration
614+ } catch {
615+ self . log. warning ( " Unable to read bundle configuration during cleanup for container \( id) : \( error) " )
616+ }
617+
618+ // Only try to deregister service if we have a valid config
619+ if let config = config {
620+ let label = Self . fullLaunchdServiceLabel (
621+ runtimeName: config. runtimeHandler,
622+ instanceId: id
623+ )
624+ try ? ServiceManager . deregister ( fullServiceLabel: label)
625+ }
626+
627+ // Always try to delete the bundle directory, even if it's incomplete
628+ do {
629+ try bundle. delete ( )
630+ } catch {
631+ self . log. warning ( " Failed to delete bundle for container \( id) : \( error) " )
632+ // Still try to remove the directory manually if bundle.delete() fails
633+ try ? FileManager . default. removeItem ( at: path)
634+ }
635+
636+ // If a container is created and immediately removed (without starting),
637+ // the temp bundle metadata file never gets removed (so do that now)
638+ let metadataPath = Self . getMetadataPath ( for: id)
639+ try ? FileManager . default. removeItem ( at: metadataPath)
601640
602- let label = Self . fullLaunchdServiceLabel (
603- runtimeName: config. runtimeHandler,
604- instanceId: id
605- )
606- try ServiceManager . deregister ( fullServiceLabel: label)
607- try bundle. delete ( )
608641 self . containers. removeValue ( forKey: id)
609642 }
610643
@@ -668,6 +701,43 @@ public actor ContainersService {
668701 private static func isInitProcess( id: String , processID: String ) -> Bool {
669702 id == processID
670703 }
704+
705+ /// Check if a bundle exists at the given path
706+ private func bundleExists( at path: URL ) async -> Bool {
707+ guard FileManager . default. fileExists ( atPath: path. path) else {
708+ return false
709+ }
710+
711+ let bundle = ContainerResource . Bundle ( path: path)
712+ do {
713+ _ = try bundle. configuration
714+ return true
715+ } catch {
716+ return false
717+ }
718+ }
719+
720+ /// Get metadata file path for the given container ID
721+ private static func getMetadataPath( for containerID: String ) -> URL {
722+ FileManager . default. temporaryDirectory
723+ . appendingPathComponent ( " bundle-metadata- \( containerID) .json " )
724+ }
725+
726+ /// Get container configuration, either from existing bundle or from metadata
727+ private func getContainerConfiguration( for id: String , at path: URL ) async throws -> ContainerConfiguration {
728+ guard await bundleExists ( at: path) else {
729+ // Bundle doesn't exist, get config from metadata
730+ let metadataPath = Self . getMetadataPath ( for: id)
731+ let metadata = try ContainerResource . Bundle. readMetadata ( from: metadataPath)
732+ guard let config = metadata. containerConfiguration else {
733+ throw ContainerizationError ( . internalError, message: " Metadata missing container configuration " )
734+ }
735+ return config
736+ }
737+ // Bundle exists, read config normally
738+ let bundle = ContainerResource . Bundle ( path: path)
739+ return try bundle. configuration
740+ }
671741}
672742
673743extension XPCMessage {
0 commit comments