Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions VirtualBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@
F4C18A5328491B9D00335EC7 /* VirtualWormhole.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
F4C2374D2888A462001FF286 /* VolumeUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C2374C2888A462001FF286 /* VolumeUtils.swift */; };
F4C237502888AF67001FF286 /* LogStreamer.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C2374F2888AF67001FF286 /* LogStreamer.swift */; };
VB01DISKRESIZ00002A0102 /* VBDiskResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = VB01DISKRESIZ00001A0101 /* VBDiskResizer.swift */; };
F4C947BF2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947BE2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift */; };
F4C947D62E0B12D0001ACC91 /* String+AppleOSBuild.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947D52E0B12D0001ACC91 /* String+AppleOSBuild.swift */; };
F4C947DA2E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4C947D92E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift */; };
Expand Down Expand Up @@ -828,6 +829,7 @@
F4C18A4D28491B8500335EC7 /* VirtualBuddyGuest.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VirtualBuddyGuest.entitlements; sourceTree = "<group>"; };
F4C2374C2888A462001FF286 /* VolumeUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VolumeUtils.swift; sourceTree = "<group>"; };
F4C2374F2888AF67001FF286 /* LogStreamer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LogStreamer.swift; sourceTree = "<group>"; };
VB01DISKRESIZ00001A0101 /* VBDiskResizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBDiskResizer.swift; sourceTree = "<group>"; };
F4C947BE2E0B0F71001ACC91 /* URL+ExtendedAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ExtendedAttributes.swift"; sourceTree = "<group>"; };
F4C947D52E0B12D0001ACC91 /* String+AppleOSBuild.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+AppleOSBuild.swift"; sourceTree = "<group>"; };
F4C947D92E0B1E5D001ACC91 /* SoftwareCatalog+DownloadMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SoftwareCatalog+DownloadMatching.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1922,6 +1924,7 @@
F485B91E2BB2F4AC004B3C2B /* Bundle+Version.swift */,
F444D1332BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift */,
F453C4BA2DF231B7007EAD5F /* PreventTerminationAssertion.swift */,
VB01DISKRESIZ00001A0101 /* VBDiskResizer.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -2725,6 +2728,7 @@
F485B91D2BB2F0D9004B3C2B /* ProcessInfo+ECID.swift in Sources */,
F444D1342BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift in Sources */,
F4C237502888AF67001FF286 /* LogStreamer.swift in Sources */,
VB01DISKRESIZ00002A0102 /* VBDiskResizer.swift in Sources */,
F4F9B41A284CE37C00F21737 /* Logging.swift in Sources */,
F4B5C5D728870619005AA632 /* ConfigurationModels+Validation.swift in Sources */,
F4A7FB3B2BB5E79100E4C12A /* DirectoryObserver.swift in Sources */,
Expand Down
27 changes: 27 additions & 0 deletions VirtualCore/Source/Models/Configuration/ConfigurationModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
}
}
}

public var displayName: String {
switch self {
case .raw:
return "Raw Image"
case .dmg:
return "Disk Image (DMG)"
case .sparse:
return "Sparse Image"
case .asif:
return "Apple Sparse Image Format (ASIF)"
}
}
}

public var id: String = UUID().uuidString
Expand All @@ -135,6 +148,15 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
format: .raw
)
}

public var canBeResized: Bool {
switch format {
case .raw, .sparse:
return true
case .dmg, .asif:
return false
}
}
}

/// Configures a storage device.
Expand Down Expand Up @@ -202,6 +224,11 @@ public struct VBStorageDevice: Identifiable, Hashable, Codable {
)
}

public var canBeResized: Bool {
guard case .managedImage(let image) = backing else { return false }
return image.canBeResized
}

public var displayName: String {
guard !isBootVolume else { return "Boot" }

Expand Down
95 changes: 95 additions & 0 deletions VirtualCore/Source/Models/VBVirtualMachine+Metadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,98 @@ extension URL {
return current
}
}

// MARK: - Disk Resize Support

public extension VBVirtualMachine {

typealias DiskResizeProgressHandler = @MainActor (_ message: String) -> Void

/// Checks if any disk images need resizing based on configuration vs actual size
func checkAndResizeDiskImages(progressHandler: DiskResizeProgressHandler? = nil) async throws {
let config = configuration

func report(_ message: String) async {
guard let progressHandler else { return }
await MainActor.run {
progressHandler(message)
}
}

let resizableDevices = config.hardware.storageDevices.compactMap { device -> (VBStorageDevice, VBManagedDiskImage)? in
guard case .managedImage(let image) = device.backing else { return nil }
guard image.canBeResized else { return nil }
return (device, image)
}

guard !resizableDevices.isEmpty else {
await report("Disk images already match their configured sizes.")
return
}

let formatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB, .useTB]
formatter.countStyle = .binary
formatter.includesUnit = true
return formatter
}()

for (index, entry) in resizableDevices.enumerated() {
let (device, image) = entry
let position = index + 1
let total = resizableDevices.count
let deviceName = device.displayName

await report("Checking \(deviceName) (\(position)/\(total))...")

let imageURL = diskImageURL(for: image)

guard FileManager.default.fileExists(atPath: imageURL.path) else {
await report("Skipping \(deviceName): disk image not found.")
continue
}

let actualSize = try await VBDiskResizer.currentImageSize(at: imageURL, format: image.format)

if image.size > actualSize {
let targetDescription = formatter.string(fromByteCount: Int64(image.size))
await report("Expanding \(deviceName) to \(targetDescription) (\(position)/\(total))...")

try await resizeDiskImage(image, to: image.size)

await report("\(deviceName) expanded successfully.")
} else if image.size < actualSize {
let actualDescription = formatter.string(fromByteCount: Int64(actualSize))
await report("\(deviceName) exceeds the configured size (\(actualDescription)); no changes made.")
} else {
let currentDescription = formatter.string(fromByteCount: Int64(actualSize))
await report("\(deviceName) already uses \(currentDescription).")
}
}

await report("Disk image checks complete.")
}

/// Resizes a managed disk image to the specified size
private func resizeDiskImage(_ image: VBManagedDiskImage, to newSize: UInt64) async throws {
let imageURL = diskImageURL(for: image)
NSLog("Resizing disk image at \(imageURL.path) from current size to \(newSize) bytes")

try await VBDiskResizer.resizeDiskImage(
at: imageURL,
format: image.format,
newSize: newSize
)

NSLog("Successfully resized disk image at \(imageURL.path) to \(newSize) bytes")
}

/// Checks if a managed disk image has FileVault (locked volumes) enabled.
/// - Parameter image: The managed disk image to check.
/// - Returns: `true` if the disk image has FileVault-protected (locked) volumes, `false` otherwise.
func checkFileVaultForDiskImage(_ image: VBManagedDiskImage) async -> Bool {
let imageURL = diskImageURL(for: image)
return await VBDiskResizer.checkFileVaultStatus(at: imageURL, format: image.format)
}
}
Loading