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
13 changes: 13 additions & 0 deletions Documentation/GuestAppSupport.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# VirtualBuddyGuest support

| Guest | Supported features |
| --- | --- |
| macOS 14 or later with the latest VirtualBuddyGuest | Clipboard sharing and all current guest integration features |
| macOS 13 with a legacy VirtualBuddyGuest | Automatic mounting of shared folders only |
| macOS 12 or earlier | VirtualBuddyGuest is not supported |

Archived guest apps do not support clipboard sharing with the current host, even when selected on a newer guest OS. The host no longer creates a serial connection or handles the legacy protocol. Linux SPICE integration is unchanged.

`GuestAppSupport` applies the same policy to configuration UI, guest installer attachment, and guest communication startup. The latest app’s minimum OS comes from its embedded `LSMinimumSystemVersion`. Unsupported guests skip installer preparation and attachment even if an existing configuration has the guest app enabled.

As with the guest app version picker, OS detection uses restore-image metadata, which records the OS used to create the VM rather than inspecting the running guest. Imported VMs without this metadata use their explicit legacy app selection. Unknown VMs without an override use the latest app. Existing macOS 12 overrides remain recognizable so the UI can flag them as unsupported.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ If you’d just like to download the latest version, [go to GitHub releases](htt

#### VirtualBuddyGuest app

VirtualBuddy automatically mounts a disk image with the VirtualBuddyGuest app when you boot up a virtual machine running macOS. To install the VirtualBuddyGuest app, just select the “Guest” disk on Finder’s side bar then double-click the “VirtualBuddyGuest” app icon.
VirtualBuddy automatically mounts a disk image with the VirtualBuddyGuest app when you boot a supported macOS virtual machine with the guest app enabled. To install the VirtualBuddyGuest app, just select the “Guest” disk on Finder’s side bar then double-click the “VirtualBuddyGuest” app icon.

VirtualBuddyGuest enables clipboard sharing between host and guest and automatic mounting of the shared folders configured for the virtual machine.
The latest VirtualBuddyGuest app requires macOS 14 or later and supports clipboard sharing and automatic mounting of shared folders. On macOS 13, the legacy guest app only mounts shared folders automatically; clipboard sharing is unavailable. VirtualBuddyGuest is not supported on macOS 12 or earlier.

![](./assets/GuestApp.jpg)

Expand Down
50 changes: 50 additions & 0 deletions VirtualBuddy.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
/* End PBXAggregateTarget section */

/* Begin PBXBuildFile section */
35C9D84FABD14D5A9535F0EF /* VMBridge in Frameworks */ = {isa = PBXBuildFile; productRef = 91FC62A28F764E37BFFAA951 /* VMBridge */; };
781FD61DBFAD4B389CB9682E /* VMBridge in Frameworks */ = {isa = PBXBuildFile; productRef = 252AC97268424F96B307D189 /* VMBridge */; };
AE3D052420DE463B85CE31B2 /* VMBridge in Frameworks */ = {isa = PBXBuildFile; productRef = D7B6E9E58E03491A8DA72A9B /* VMBridge */; };
CD4422E92FE9B99300DD1670 /* BuddyFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CD4422E82FE9B99300DD1670 /* BuddyFoundation */; };
CD4422ED2FE9B9D900DD1670 /* BuddyFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = CD4422EC2FE9B9D900DD1670 /* BuddyFoundation */; };
F40F5DA53040AF22005C19FD /* USBIDKit in Frameworks */ = {isa = PBXBuildFile; productRef = F40F5DA43040AF22005C19FD /* USBIDKit */; };
Expand Down Expand Up @@ -62,6 +65,8 @@
F4EFB5BF2FDB2BD400AF2A63 /* VirtualInstallationService.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = F4EFB5B42FDB2BD300AF2A63 /* VirtualInstallationService.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F4EFB5CA2FDB2BEF00AF2A63 /* VirtualInstallation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4EFB5632FDB2A0D00AF2A63 /* VirtualInstallation.framework */; };
F4EFB5D12FDB2CD200AF2A63 /* VirtualInstallation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4EFB5632FDB2A0D00AF2A63 /* VirtualInstallation.framework */; };
F4F0F4973053041E00D25236 /* VMBridge in Frameworks */ = {isa = PBXBuildFile; productRef = F4F0F4963053041E00D25236 /* VMBridge */; };
F4F0F4993053041E00D25236 /* VMBridgeVirtualization in Frameworks */ = {isa = PBXBuildFile; productRef = F4F0F4983053041E00D25236 /* VMBridgeVirtualization */; };
VB02DISKTEST00003A0103 /* VirtualCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */; };
/* End PBXBuildFile section */

Expand Down Expand Up @@ -496,6 +501,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
781FD61DBFAD4B389CB9682E /* VMBridge in Frameworks */,
F453C44C2DF0B835007EAD5F /* libcurl.tbd in Frameworks */,
F43B01472AD85A7D00164CD1 /* URLQueryItemCoder in Frameworks */,
F498AD042884BF13006F1C00 /* VirtualUI.framework in Frameworks */,
Expand All @@ -517,20 +523,23 @@
F4EFB5D12FDB2CD200AF2A63 /* VirtualInstallation.framework in Frameworks */,
CD4422E92FE9B99300DD1670 /* BuddyFoundation in Frameworks */,
F4C189F22848F5F500335EC7 /* VirtualWormhole.framework in Frameworks */,
F4F0F4993053041E00D25236 /* VMBridgeVirtualization in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4C189DD2848F59F00335EC7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F4F0F4973053041E00D25236 /* VMBridge in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F4C18A3F28491B8500335EC7 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
AE3D052420DE463B85CE31B2 /* VMBridge in Frameworks */,
F453C4242DF0B602007EAD5F /* VirtualUI.framework in Frameworks */,
F4C18A5228491B9D00335EC7 /* VirtualWormhole.framework in Frameworks */,
F428622E2AE87D7E0052F029 /* DeepLinkSecurity.framework in Frameworks */,
Expand All @@ -543,6 +552,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
35C9D84FABD14D5A9535F0EF /* VMBridge in Frameworks */,
VB02DISKTEST00003A0103 /* VirtualCore.framework in Frameworks */,
F4D305A029B8DB700006E748 /* VirtualWormhole.framework in Frameworks */,
);
Expand Down Expand Up @@ -747,6 +757,7 @@
);
name = VirtualBuddy;
packageProductDependencies = (
252AC97268424F96B307D189 /* VMBridge */,
F4D0F71728674E4B004D5782 /* Sparkle */,
F43B01462AD85A7D00164CD1 /* URLQueryItemCoder */,
F453C42A2DF0B792007EAD5F /* ArgumentParser */,
Expand Down Expand Up @@ -780,6 +791,9 @@
F4FFF064301D13FD00620720 /* VirtualCore */,
);
name = VirtualCore;
packageProductDependencies = (
F4F0F4983053041E00D25236 /* VMBridgeVirtualization */,
);
productName = VirtualCore;
productReference = F4BE9C6527FF053A00B648F8 /* VirtualCore.framework */;
productType = "com.apple.product-type.framework";
Expand All @@ -802,6 +816,7 @@
);
name = VirtualWormhole;
packageProductDependencies = (
F4F0F4963053041E00D25236 /* VMBridge */,
);
productName = VirtualWormhole;
productReference = F4C189E02848F59F00335EC7 /* VirtualWormhole.framework */;
Expand Down Expand Up @@ -831,6 +846,9 @@
F4FFEFA4301D13C500620720 /* VirtualBuddyGuest */,
);
name = VirtualBuddyGuest;
packageProductDependencies = (
D7B6E9E58E03491A8DA72A9B /* VMBridge */,
);
productName = VirtualBuddyGuest;
productReference = F4C18A4228491B8500335EC7 /* VirtualBuddyGuest.app */;
productType = "com.apple.product-type.application";
Expand All @@ -852,6 +870,9 @@
F4FFEF87301D13BE00620720 /* VirtualWormholeTests */,
);
name = VirtualWormholeTests;
packageProductDependencies = (
91FC62A28F764E37BFFAA951 /* VMBridge */,
);
productName = VirtualWormholeTests;
productReference = F4D3059C29B8DB700006E748 /* VirtualWormholeTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
Expand Down Expand Up @@ -970,6 +991,7 @@
F453C4012DF0AEF5007EAD5F /* XCRemoteSwiftPackageReference "swift-argument-parser" */,
F453C4482DF0B7F6007EAD5F /* XCRemoteSwiftPackageReference "libfragmentzip" */,
F40F5DA33040AF22005C19FD /* XCRemoteSwiftPackageReference "USBIDKit" */,
F4F0F4953053041E00D25236 /* XCRemoteSwiftPackageReference "VMBridge" */,
);
preferredProjectObjectVersion = 55;
productRefGroup = F4BE9C4F27FF052100B648F8 /* Products */;
Expand Down Expand Up @@ -4011,9 +4033,25 @@
minimumVersion = 2.9.4;
};
};
F4F0F4953053041E00D25236 /* XCRemoteSwiftPackageReference "VMBridge" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/insidegui/VMBridge.git";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */

/* Begin XCSwiftPackageProductDependency section */
252AC97268424F96B307D189 /* VMBridge */ = {
isa = XCSwiftPackageProductDependency;
productName = VMBridge;
};
91FC62A28F764E37BFFAA951 /* VMBridge */ = {
isa = XCSwiftPackageProductDependency;
productName = VMBridge;
};
CD4422E82FE9B99300DD1670 /* BuddyFoundation */ = {
isa = XCSwiftPackageProductDependency;
package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */;
Expand All @@ -4024,6 +4062,10 @@
package = F453C3D82DF0A426007EAD5F /* XCRemoteSwiftPackageReference "BuddyKit" */;
productName = BuddyFoundation;
};
D7B6E9E58E03491A8DA72A9B /* VMBridge */ = {
isa = XCSwiftPackageProductDependency;
productName = VMBridge;
};
F40F5DA43040AF22005C19FD /* USBIDKit */ = {
isa = XCSwiftPackageProductDependency;
package = F40F5DA33040AF22005C19FD /* XCRemoteSwiftPackageReference "USBIDKit" */;
Expand Down Expand Up @@ -4064,6 +4106,14 @@
package = F4D0F71628674E4B004D5782 /* XCRemoteSwiftPackageReference "Sparkle" */;
productName = Sparkle;
};
F4F0F4963053041E00D25236 /* VMBridge */ = {
isa = XCSwiftPackageProductDependency;
productName = VMBridge;
};
F4F0F4983053041E00D25236 /* VMBridgeVirtualization */ = {
isa = XCSwiftPackageProductDependency;
productName = VMBridgeVirtualization;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = F4BE9C4627FF052100B648F8 /* Project object */;
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion VirtualBuddy/Bootstrap/VirtualBuddyAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import SwiftUI
logger.info("User cancelled termination request. Good.")
case .terminateNow:
logger.info("User decided to terminate now despite assertions :(")
return finishTerminationAfterGuestTeardown()
case .terminateLater:
logger.info("User wants app to terminate when assertions preventing termination are invalidated.")

Expand All @@ -115,10 +116,22 @@ import SwiftUI

return reply
} else {
return .terminateNow
return finishTerminationAfterGuestTeardown()
}
}

private var terminationTask: Task<Void, Never>?

private func finishTerminationAfterGuestTeardown() -> NSApplication.TerminateReply {
if terminationTask == nil {
terminationTask = Task {
await library.stopGuestCommunication()
NSApp.reply(toApplicationShouldTerminate: true)
}
}
return .terminateLater
}

private var settingsWindow: NSWindow?

private(set) lazy var openSettingsAction = OpenVirtualBuddySettingsAction { [weak self] in
Expand Down
9 changes: 3 additions & 6 deletions VirtualBuddyGuest/Dashboard/GuestDashboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import VirtualWormhole

struct GuestDashboard: View {
@EnvironmentObject private var launchAtLoginManager: GuestLaunchAtLoginManager
@EnvironmentObject private var hostConnection: WormholeManager
@Environment(GuestHostSession.self) private var hostConnection
@EnvironmentObject private var sharedFolders: GuestSharedFoldersManager

@State var activated = false
Expand All @@ -35,7 +35,7 @@ struct GuestDashboard: View {
showingDefaultsPopover.toggle()
}
.popover(isPresented: $showingDefaultsPopover) {
GuestDefaultsImportView()
GuestDefaultsImportView(connection: hostConnection)
}
#endif
}
Expand Down Expand Up @@ -104,12 +104,9 @@ struct GuestDashboard_Previews: PreviewProvider {
static var previews: some View {
GuestDashboard()
.environmentObject(GuestLaunchAtLoginManager())
.environmentObject(WormholeManager.sharedGuest)
.environment(GuestHostSession())
.environmentObject(GuestSharedFoldersManager())
}
}

final class MockHostConnectionStateProvider: HostConnectionStateProvider {
var isConnected: Bool = false
}
#endif
30 changes: 10 additions & 20 deletions VirtualBuddyGuest/Dashboard/GuestDefaultsImportView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,32 @@ import VirtualUI
import VirtualWormhole
import Combine

@MainActor
final class DefaultsImportViewModel: ObservableObject {

let connection: WormholeManager
let connection: GuestHostSession
let controller = DefaultsImportController()

@Published private(set) var domains = [DefaultsDomainDescriptor]()

init(connection: WormholeManager = .sharedGuest) {
init(connection: GuestHostSession) {
self.connection = connection

controller.$sortedDomains.assign(to: &$domains)
}

private var _client: WHDefaultsImportClient?
private var client: WHDefaultsImportClient {
get throws {
if let _client { return _client }

let newClient = try connection.makeClient(WHDefaultsImportClient.self)

_client = newClient

return newClient
}
}

func importDomain(with id: DefaultsDomainDescriptor.ID) async throws {
let defaultsClient = try client

try await defaultsClient.importDomain(with: id)
try await connection.importDomain(with: id)
}

}

struct GuestDefaultsImportView: View {
@StateObject var viewModel = DefaultsImportViewModel()
@StateObject private var viewModel: DefaultsImportViewModel

init(connection: GuestHostSession) {
_viewModel = StateObject(wrappedValue: DefaultsImportViewModel(connection: connection))
}

var body: some View {
List {
Expand Down Expand Up @@ -101,7 +91,7 @@ struct DefaultsItemView: View {
#if DEBUG
struct GuestDefaultsImportView_Previews: PreviewProvider {
static var previews: some View {
GuestDefaultsImportView()
GuestDefaultsImportView(connection: GuestHostSession())
}
}
#endif
Expand Down
8 changes: 0 additions & 8 deletions VirtualBuddyGuest/Dashboard/HostConnectionStateProvider.swift

This file was deleted.

18 changes: 9 additions & 9 deletions VirtualBuddyGuest/GuestAppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import VirtualWormhole
import OSLog

@NSApplicationMain
@MainActor
final class GuestAppDelegate: NSObject, NSApplicationDelegate {

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Guest", category: "GuestAppDelegate")

private lazy var launchAtLoginManager = GuestLaunchAtLoginManager()

private let hostConnection = GuestHostSession()
private var connectionTask: Task<Void, Never>?

deinit { connectionTask?.cancel() }

private lazy var sharedFolders = GuestSharedFoldersManager()

private lazy var dashboardItem: StatusItemManager = {
Expand All @@ -19,7 +25,7 @@ final class GuestAppDelegate: NSObject, NSApplicationDelegate {
statusItem: .button(label: { Image("StatusItem") }),
content: GuestDashboard()
.environmentObject(self.launchAtLoginManager)
.environmentObject(WormholeManager.sharedGuest)
.environment(self.hostConnection)
.environmentObject(self.sharedFolders)
)
}()
Expand Down Expand Up @@ -49,7 +55,7 @@ final class GuestAppDelegate: NSObject, NSApplicationDelegate {

launchAtLoginManager.autoEnableIfNeeded()

WormholeManager.sharedGuest.activate()
connectionTask = hostConnection.start()

Task {
try? await sharedFolders.mount()
Expand Down Expand Up @@ -92,13 +98,7 @@ final class GuestAppDelegate: NSObject, NSApplicationDelegate {
startedDesktopPictureSendBeforeTermination = true

Task {
logger.notice("Requesting WH send desktop picture")

try? await Task.sleep(for: .milliseconds(500))

await WormholeManager.sharedGuest.sendDesktopPicture()

try? await Task.sleep(for: .seconds(500))
await hostConnection.prepareForTermination()

NSApp.reply(toApplicationShouldTerminate: true)
}
Expand Down
Loading