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
31 changes: 31 additions & 0 deletions VirtualBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,11 @@
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 */; };
VB01DISKRESIZ00004A0104 /* VBVirtualMachine+DiskResize.swift in Sources */ = {isa = PBXBuildFile; fileRef = VB01DISKRESIZ00003A0103 /* VBVirtualMachine+DiskResize.swift */; };
VB02DISKTEST00001A0101 /* DiskResizeSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = VB02DISKTEST00002A0102 /* DiskResizeSupportTests.swift */; };
VB02DISKTEST00003A0103 /* VirtualCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; };
VB03DISKTEST00001A0101 /* VBDiskResizerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = VB03DISKTEST00002A0102 /* VBDiskResizerTests.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 @@ -439,6 +444,13 @@
remoteGlobalIDString = F4C189DF2848F59F00335EC7;
remoteInfo = VirtualWormhole;
};
VB02DISKTEST00004A0104 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F4BE9C4627FF052100B648F8 /* Project object */;
proxyType = 1;
remoteGlobalIDString = F4BE9C6427FF053A00B648F8;
remoteInfo = VirtualCore;
};
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
Expand Down Expand Up @@ -828,6 +840,8 @@
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>"; };
VB01DISKRESIZ00003A0103 /* VBVirtualMachine+DiskResize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VBVirtualMachine+DiskResize.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 All @@ -846,6 +860,8 @@
F4D3059629B8D9D30006E748 /* WormholePacket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholePacket.swift; sourceTree = "<group>"; };
F4D3059C29B8DB700006E748 /* VirtualWormholeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VirtualWormholeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F4D3059E29B8DB700006E748 /* WormholePacketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WormholePacketTests.swift; sourceTree = "<group>"; };
VB02DISKTEST00002A0102 /* DiskResizeSupportTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskResizeSupportTests.swift; sourceTree = "<group>"; };
VB03DISKTEST00002A0102 /* VBDiskResizerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VBDiskResizerTests.swift; sourceTree = "<group>"; };
F4D305A929B8E7120006E748 /* TestStream.bin */ = {isa = PBXFileReference; lastKnownFileType = archive.macbinary; path = TestStream.bin; sourceTree = "<group>"; };
F4D305AB29B8FEE90006E748 /* WHDarwinNotificationsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WHDarwinNotificationsService.swift; sourceTree = "<group>"; };
F4D305AF29B900860006E748 /* SystemNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemNotification.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -950,6 +966,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
VB02DISKTEST00003A0103 /* VirtualCore.framework in Frameworks */,
F4D305A029B8DB700006E748 /* VirtualWormhole.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -1629,6 +1646,7 @@
F4DE1C102D6F642E00603527 /* VBStorageDeviceContainer.swift */,
F46FFBA72804F07400D61023 /* VBNVRAMVariable.swift */,
F4D725FD286677B8001818F7 /* VBVirtualMachine+Metadata.swift */,
VB01DISKRESIZ00003A0103 /* VBVirtualMachine+DiskResize.swift */,
F4D0F71428667984004D5782 /* VBVirtualMachine+Screenshot.swift */,
);
path = Models;
Expand Down Expand Up @@ -1922,6 +1940,7 @@
F485B91E2BB2F4AC004B3C2B /* Bundle+Version.swift */,
F444D1332BB478AD00AB786F /* VBMemoryLeakDebugAssertions.swift */,
F453C4BA2DF231B7007EAD5F /* PreventTerminationAssertion.swift */,
VB01DISKRESIZ00001A0101 /* VBDiskResizer.swift */,
);
path = Utilities;
sourceTree = "<group>";
Expand Down Expand Up @@ -1952,6 +1971,8 @@
isa = PBXGroup;
children = (
F4D305A829B8E70A0006E748 /* Resources */,
VB02DISKTEST00002A0102 /* DiskResizeSupportTests.swift */,
VB03DISKTEST00002A0102 /* VBDiskResizerTests.swift */,
F4D3059E29B8DB700006E748 /* WormholePacketTests.swift */,
);
path = VirtualWormholeTests;
Expand Down Expand Up @@ -2272,6 +2293,7 @@
buildRules = (
);
dependencies = (
VB02DISKTEST00005A0105 /* PBXTargetDependency */,
F4D305A229B8DB700006E748 /* PBXTargetDependency */,
);
name = VirtualWormholeTests;
Expand Down Expand Up @@ -2701,6 +2723,7 @@
F453C4A22DF1D7F6007EAD5F /* SimulatedRestoreBackend.swift in Sources */,
F4DE1C0B2D6F54E700603527 /* VBSavedStateMetadata+Clone.swift in Sources */,
F4D725FE286677B8001818F7 /* VBVirtualMachine+Metadata.swift in Sources */,
VB01DISKRESIZ00004A0104 /* VBVirtualMachine+DiskResize.swift in Sources */,
F4A21BF428033102001072B8 /* VBError.swift in Sources */,
F4D0F71F2867517A004D5782 /* AppUpdateChannel.swift in Sources */,
F49FD8842DFB727B0019D638 /* VMImporter+Helpers.swift in Sources */,
Expand All @@ -2725,6 +2748,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 Expand Up @@ -2801,6 +2825,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
VB02DISKTEST00001A0101 /* DiskResizeSupportTests.swift in Sources */,
VB03DISKTEST00001A0101 /* VBDiskResizerTests.swift in Sources */,
F4D3059F29B8DB700006E748 /* WormholePacketTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down Expand Up @@ -2872,6 +2898,11 @@
target = F4C189DF2848F59F00335EC7 /* VirtualWormhole */;
targetProxy = F4D305A129B8DB700006E748 /* PBXContainerItemProxy */;
};
VB02DISKTEST00005A0105 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F4BE9C6427FF053A00B648F8 /* VirtualCore */;
targetProxy = VB02DISKTEST00004A0104 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */

/* Begin PBXVariantGroup section */
Expand Down
23 changes: 23 additions & 0 deletions VirtualCore/Source/Models/Configuration/ConfigurationModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,23 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
}
}
}

public var displayName: String {
switch self {
case .raw: "Raw Image"
case .dmg: "Disk Image (DMG)"
case .sparse: "Sparse Image"
case .asif: "Apple Sparse Image Format (ASIF)"
}
}
}
Comment thread
balcsida marked this conversation as resolved.

public var id: String = UUID().uuidString
public var filename: String
public var size: UInt64
public var format: Format = .sparse
@DecodableDefault.False
public var resizePending = false

// Not a stored property because Format.defaultBootDisk can change based on user preferences.
public static var managedBootImage: VBManagedDiskImage {
Expand All @@ -135,6 +146,13 @@ public struct VBManagedDiskImage: Identifiable, Hashable, Codable {
format: .raw
)
}

public var canBeResized: Bool {
switch format {
case .raw, .sparse: true
case .dmg, .asif: false
}
}
}
Comment thread
balcsida marked this conversation as resolved.

/// Configures a storage device.
Expand Down Expand Up @@ -202,6 +220,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
73 changes: 73 additions & 0 deletions VirtualCore/Source/Models/VBVirtualMachine+DiskResize.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// VBVirtualMachine+DiskResize.swift
// VirtualCore
//
// Created by VirtualBuddy on 25/05/26.
//

import Foundation
import OSLog

private let diskResizeLogger = Logger(for: VBVirtualMachine.self, label: "DiskResize")

public extension VBVirtualMachine {

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

/// Expands managed disk images whose configured size is larger than the image on disk.
/// Returns `true` if any disk image was expanded.
@discardableResult
mutating func checkAndResizeDiskImages(progressHandler: DiskResizeProgressHandler? = nil) async throws -> Bool {
let resizableImages = configuration.hardware.storageDevices.indices.compactMap { index -> (index: Int, name: String, image: VBManagedDiskImage)? in
let device = configuration.hardware.storageDevices[index]
guard case .managedImage(let image) = device.backing, image.resizePending else { return nil }
return (index, device.displayName, image)
}

let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useGB, .useMB, .useTB]
formatter.countStyle = .binary

var didResize = false

for (index, name, image) in resizableImages {
let imageURL = diskImageURL(for: image)

guard FileManager.default.fileExists(atPath: imageURL.path) else {
throw VBDiskResizeError.diskImageNotFound(imageURL)
}

let actualSize = try await VBDiskResizer.currentImageSize(at: imageURL, format: image.format)
guard image.size >= actualSize else {
var image = image
image.resizePending = false
configuration.hardware.storageDevices[index].backing = .managedImage(image)
continue
}

if image.size > actualSize {
let targetDescription = formatter.string(fromByteCount: Int64(image.size))
if let progressHandler {
await progressHandler("Expanding \(name) to \(targetDescription)...")
}
}

diskResizeLogger.debug("Resizing disk image at \(imageURL.path, privacy: .public) to \(image.size, privacy: .public) bytes")
try await VBDiskResizer.resizeDiskImage(at: imageURL, format: image.format, newSize: image.size)

var image = image
image.resizePending = false
configuration.hardware.storageDevices[index].backing = .managedImage(image)
didResize = didResize || image.size > actualSize
}

return didResize
}

var hasPendingDiskImageResizes: Bool {
configuration.hardware.storageDevices.contains {
guard case .managedImage(let image) = $0.backing else { return false }
return image.resizePending
}
}
}
Loading