Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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)"
}
}
}
Comment thread
balcsida marked this conversation as resolved.

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
}
}
}
Comment thread
balcsida marked this conversation as resolved.

/// 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
}
}

Comment thread
balcsida marked this conversation as resolved.
Outdated
// 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