mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-17 19:36:10 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e2a19015 | ||
|
|
89ff5f6b65 | ||
|
|
6bf39e73d0 | ||
|
|
0b693f6bc9 | ||
|
|
99bbd838a1 | ||
|
|
5c7743b7cd | ||
|
|
a40e104c03 | ||
|
|
e2d6c13ed0 |
@@ -7,6 +7,8 @@ export VERSION="${CIRRUS_TAG:-0}"
|
|||||||
mkdir -p .ci/pkg/
|
mkdir -p .ci/pkg/
|
||||||
cp .build/arm64-apple-macosx/release/tart .ci/pkg/tart
|
cp .build/arm64-apple-macosx/release/tart .ci/pkg/tart
|
||||||
cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile
|
cp Resources/embedded.provisionprofile .ci/pkg/embedded.provisionprofile
|
||||||
|
cp Resources/AppIcon.png .ci/pkg/AppIcon.png
|
||||||
|
cp Resources/Info.plist .ci/pkg/Info.plist
|
||||||
pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \
|
pkgbuild --root .ci/pkg/ --identifier com.github.cirruslabs.tart --version $VERSION \
|
||||||
--scripts .ci/pkg/scripts \
|
--scripts .ci/pkg/scripts \
|
||||||
--install-location "/Library/Application Support/Tart" \
|
--install-location "/Library/Application Support/Tart" \
|
||||||
|
|||||||
@@ -3,9 +3,11 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
# fix structure
|
# fix structure
|
||||||
mkdir -p "$2/tart.app/Contents/MacOS"
|
mkdir -p "$2/tart.app/Contents/MacOS" "$2/tart.app/Resources"
|
||||||
mv "$2/tart" "$2/tart.app/Contents/MacOS/tart"
|
mv "$2/tart" "$2/tart.app/Contents/MacOS/tart"
|
||||||
mv "$2/embedded.provisionprofile" "$2/tart.app/Contents/embedded.provisionprofile"
|
mv "$2/embedded.provisionprofile" "$2/tart.app/Contents/embedded.provisionprofile"
|
||||||
|
mv "$2/AppIcon.png" "$2/tart.app/Resources/AppIcon.png"
|
||||||
|
mv "$2/Info.plist" "$2/tart.app/Contents/Info.plist"
|
||||||
|
|
||||||
echo "#!/bin/sh" > /usr/local/bin/tart
|
echo "#!/bin/sh" > /usr/local/bin/tart
|
||||||
echo "exec '$2/tart.app/Contents/MacOS/tart' \"\$@\"" >> /usr/local/bin/tart
|
echo "exec '$2/tart.app/Contents/MacOS/tart' \"\$@\"" >> /usr/local/bin/tart
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ task:
|
|||||||
install_script:
|
install_script:
|
||||||
- brew install go goreleaser/tap/goreleaser-pro
|
- brew install go goreleaser/tap/goreleaser-pro
|
||||||
- brew install mitchellh/gon/gon
|
- brew install mitchellh/gon/gon
|
||||||
- curl -sL https://sentry.io/get-cli/ | sh
|
|
||||||
info_script:
|
info_script:
|
||||||
- security find-identity -v
|
- security find-identity -v
|
||||||
- xcodebuild -version
|
- xcodebuild -version
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>tart</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>org.cirruslabs.tart</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>tart</string>
|
||||||
|
<key>LSBackgroundOnly</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>CFBundleIconFiles</key>
|
||||||
|
<array>
|
||||||
|
<string>AppIcon.png</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -73,7 +73,7 @@ struct Clone: AsyncParsableCommand {
|
|||||||
// APFS is doing copy-on-write so the above cloning operation (just copying files on disk)
|
// APFS is doing copy-on-write so the above cloning operation (just copying files on disk)
|
||||||
// is not actually claiming new space until the VM is started and it writes something to disk.
|
// is not actually claiming new space until the VM is started and it writes something to disk.
|
||||||
// So once we clone the VM let's try to claim a little bit of space for the VM to run.
|
// So once we clone the VM let's try to claim a little bit of space for the VM to run.
|
||||||
try Prune.reclaimIfNeeded(UInt64(sourceVM.sizeBytes()), sourceVM)
|
try Prune.reclaimIfNeeded(UInt64(sourceVM.allocatedSizeBytes()), sourceVM)
|
||||||
}, onCancel: {
|
}, onCancel: {
|
||||||
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
|
try? FileManager.default.removeItem(at: tmpVMDir.baseURL)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ fileprivate struct VMInfo: Encodable {
|
|||||||
let CPU: Int
|
let CPU: Int
|
||||||
let Memory: UInt64
|
let Memory: UInt64
|
||||||
let Disk: Int
|
let Disk: Int
|
||||||
|
let Size: String
|
||||||
let Display: String
|
let Display: String
|
||||||
let Running: Bool
|
let Running: Bool
|
||||||
let State: String
|
let State: String
|
||||||
@@ -22,10 +23,9 @@ struct Get: AsyncParsableCommand {
|
|||||||
func run() async throws {
|
func run() async throws {
|
||||||
let vmDir = try VMStorageLocal().open(name)
|
let vmDir = try VMStorageLocal().open(name)
|
||||||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||||
let diskSizeInGb = try vmDir.sizeGB()
|
|
||||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||||
|
|
||||||
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb,
|
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000),
|
||||||
Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state())
|
Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state())
|
||||||
print(format.renderSingle(info))
|
print(format.renderSingle(info))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import SwiftUI
|
|||||||
fileprivate struct VMInfo: Encodable {
|
fileprivate struct VMInfo: Encodable {
|
||||||
let Source: String
|
let Source: String
|
||||||
let Name: String
|
let Name: String
|
||||||
|
let Disk: Int
|
||||||
let Size: Int
|
let Size: Int
|
||||||
let Running: Bool
|
let Running: Bool
|
||||||
let State: String
|
let State: String
|
||||||
@@ -37,13 +38,13 @@ struct List: AsyncParsableCommand {
|
|||||||
|
|
||||||
if source == nil || source == "local" {
|
if source == nil || source == "local" {
|
||||||
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
infos += sortedInfos(try VMStorageLocal().list().map { (name, vmDir) in
|
||||||
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running(), State: vmDir.state())
|
try VMInfo(Source: "local", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), Running: vmDir.running(), State: vmDir.state())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if source == nil || source == "oci" {
|
if source == nil || source == "oci" {
|
||||||
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
infos += sortedInfos(try VMStorageOCI().list().map { (name, vmDir, _) in
|
||||||
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running(), State: vmDir.state())
|
try VMInfo(Source: "oci", Name: name, Disk: vmDir.sizeGB(), Size: vmDir.allocatedSizeGB(), Running: vmDir.running(), State: vmDir.state())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ struct Prune: AsyncParsableCommand {
|
|||||||
var prunablesToDelete: [Prunable] = []
|
var prunablesToDelete: [Prunable] = []
|
||||||
|
|
||||||
for prunable in prunables {
|
for prunable in prunables {
|
||||||
let prunableSizeBytes = UInt64(try prunable.sizeBytes())
|
let prunableSizeBytes = UInt64(try prunable.allocatedSizeBytes())
|
||||||
|
|
||||||
if prunableSizeBytes <= spaceBudgetBytes {
|
if prunableSizeBytes <= spaceBudgetBytes {
|
||||||
// Don't mark for deletion as
|
// Don't mark for deletion as
|
||||||
@@ -158,7 +158,7 @@ struct Prune: AsyncParsableCommand {
|
|||||||
.sorted { try $0.accessDate() < $1.accessDate() }
|
.sorted { try $0.accessDate() < $1.accessDate() }
|
||||||
|
|
||||||
// Does it even make sense to start?
|
// Does it even make sense to start?
|
||||||
let cacheUsedBytes = try prunables.map { try $0.sizeBytes() }.reduce(0, +)
|
let cacheUsedBytes = try prunables.map { try $0.allocatedSizeBytes() }.reduce(0, +)
|
||||||
if cacheUsedBytes < reclaimBytes {
|
if cacheUsedBytes < reclaimBytes {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -177,9 +177,9 @@ struct Prune: AsyncParsableCommand {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
try SentrySDK.span?.setData(value: prunable.sizeBytes(), key: prunable.url.path)
|
try SentrySDK.span?.setData(value: prunable.allocatedSizeBytes(), key: prunable.url.path)
|
||||||
|
|
||||||
cacheReclaimedBytes += try prunable.sizeBytes()
|
cacheReclaimedBytes += try prunable.allocatedSizeBytes()
|
||||||
|
|
||||||
try prunable.delete()
|
try prunable.delete()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ struct Run: AsyncParsableCommand {
|
|||||||
discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet"))
|
discussion: "Learn how to configure Softnet for use with Tart here: https://github.com/cirruslabs/softnet"))
|
||||||
var netSoftnet: Bool = false
|
var netSoftnet: Bool = false
|
||||||
|
|
||||||
|
@Flag(help: ArgumentHelp("Restrict network access to the host-only network"))
|
||||||
|
var netHost: Bool = false
|
||||||
|
|
||||||
#if arch(arm64)
|
#if arch(arm64)
|
||||||
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
|
@Flag(help: ArgumentHelp("Disables audio and entropy devices and switches to only Mac-specific input devices.", discussion: "Useful for running a VM that can be suspended via \"tart suspend\"."))
|
||||||
#endif
|
#endif
|
||||||
@@ -133,8 +136,14 @@ struct Run: AsyncParsableCommand {
|
|||||||
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
throw ValidationError("--vnc and --vnc-experimental are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
if netBridged.count > 0 && netSoftnet {
|
// check that not more than one network option is specified
|
||||||
throw ValidationError("--net-bridged and --net-softnet are mutually exclusive")
|
var netFlags = 0
|
||||||
|
if netBridged.count > 0 { netFlags += 1 }
|
||||||
|
if netSoftnet { netFlags += 1 }
|
||||||
|
if netHost { netFlags += 1 }
|
||||||
|
|
||||||
|
if netFlags > 1 {
|
||||||
|
throw ValidationError("--net-bridged, --net-softnet and --net-host are mutually exclusive")
|
||||||
}
|
}
|
||||||
|
|
||||||
if graphics {
|
if graphics {
|
||||||
@@ -184,7 +193,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if netSoftnet && isInteractiveSession() {
|
if (netSoftnet || netHost) && isInteractiveSession() {
|
||||||
try Softnet.configureSUIDBitIfNeeded()
|
try Softnet.configureSUIDBitIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,9 +349,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
if noGraphics {
|
if noGraphics {
|
||||||
// enter the main even loop, without bringing up any UI,
|
// enter the main even loop, without bringing up any UI,
|
||||||
// and just wait for the VM to exit.
|
// and just wait for the VM to exit.
|
||||||
let nsApp = NSApplication.shared
|
NSApp.run()
|
||||||
nsApp.setActivationPolicy(.prohibited)
|
|
||||||
nsApp.run()
|
|
||||||
} else {
|
} else {
|
||||||
runUI(suspendable, captureSystemKeys)
|
runUI(suspendable, captureSystemKeys)
|
||||||
}
|
}
|
||||||
@@ -365,10 +372,14 @@ struct Run: AsyncParsableCommand {
|
|||||||
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
|
func userSpecifiedNetwork(vmDir: VMDirectory) throws -> Network? {
|
||||||
if netSoftnet {
|
if netSoftnet {
|
||||||
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
|
|
||||||
return try Softnet(vmMACAddress: config.macAddress.string)
|
return try Softnet(vmMACAddress: config.macAddress.string)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if netHost {
|
||||||
|
let config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
|
return try Softnet(vmMACAddress: config.macAddress.string, extraArguments: ["--vm-net-type", "host"])
|
||||||
|
}
|
||||||
|
|
||||||
if netBridged.count > 0 {
|
if netBridged.count > 0 {
|
||||||
func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface {
|
func findBridgedInterface(_ name: String) throws -> VZBridgedNetworkInterface {
|
||||||
let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in
|
let interface = VZBridgedNetworkInterface.networkInterfaces.first { interface in
|
||||||
@@ -466,7 +477,7 @@ struct Run: AsyncParsableCommand {
|
|||||||
allNamedShares = false
|
allNamedShares = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if directoryShares.count == 1 {
|
if directoryShares.count == 1 && directoryShares.first!.name == nil {
|
||||||
let directoryShare = directoryShares.first!
|
let directoryShare = directoryShares.first!
|
||||||
let singleDirectoryShare = VZSingleDirectoryShare(directory: try directoryShare.createConfiguration())
|
let singleDirectoryShare = VZSingleDirectoryShare(directory: try directoryShare.createConfiguration())
|
||||||
sharingDevice.share = singleDirectoryShare
|
sharingDevice.share = singleDirectoryShare
|
||||||
@@ -516,8 +527,6 @@ struct Run: AsyncParsableCommand {
|
|||||||
nsApp.setActivationPolicy(.regular)
|
nsApp.setActivationPolicy(.regular)
|
||||||
nsApp.activate(ignoringOtherApps: true)
|
nsApp.activate(ignoringOtherApps: true)
|
||||||
|
|
||||||
nsApp.applicationIconImage = NSImage(data: AppIconData)
|
|
||||||
|
|
||||||
struct MainApp: App {
|
struct MainApp: App {
|
||||||
static var disappearSignal: Int32 = SIGINT
|
static var disappearSignal: Int32 = SIGINT
|
||||||
static var capturesSystemKeys: Bool = false
|
static var capturesSystemKeys: Bool = false
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -45,7 +45,10 @@ class Leases {
|
|||||||
(lease.mac, lease)
|
(lease.mac, lease)
|
||||||
})
|
})
|
||||||
|
|
||||||
self.leases = Dictionary(uniqueKeysWithValues: leases)
|
self.leases = Dictionary(leases) { (left, right) in
|
||||||
|
// When duplicate lease is found, prefer a newer lease over the older one
|
||||||
|
(left.expiresAt > right.expiresAt) ? left : right
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse leases from the host cache similarly to the PLCache_read() function found in Apple's Open Source releases.
|
/// Parse leases from the host cache similarly to the PLCache_read() function found in Apple's Open Source releases.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Softnet: Network {
|
|||||||
|
|
||||||
let vmFD: Int32
|
let vmFD: Int32
|
||||||
|
|
||||||
init(vmMACAddress: String) throws {
|
init(vmMACAddress: String, extraArguments: [String] = []) throws {
|
||||||
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
|
let fds = UnsafeMutablePointer<Int32>.allocate(capacity: MemoryLayout<Int>.stride * 2)
|
||||||
|
|
||||||
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
|
let ret = socketpair(AF_UNIX, SOCK_DGRAM, 0, fds)
|
||||||
@@ -30,7 +30,7 @@ class Softnet: Network {
|
|||||||
try setSocketBuffers(softnetFD, 1 * 1024 * 1024);
|
try setSocketBuffers(softnetFD, 1 * 1024 * 1024);
|
||||||
|
|
||||||
process.executableURL = try Self.softnetExecutableURL()
|
process.executableURL = try Self.softnetExecutableURL()
|
||||||
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress]
|
process.arguments = ["--vm-fd", String(STDIN_FILENO), "--vm-mac-address", vmMACAddress] + extraArguments
|
||||||
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
|
process.standardInput = FileHandle(fileDescriptor: softnetFD, closeOnDealloc: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
|
|||||||
func keyboards() -> [VZKeyboardConfiguration] {
|
func keyboards() -> [VZKeyboardConfiguration] {
|
||||||
if #available(macOS 14, *) {
|
if #available(macOS 14, *) {
|
||||||
// Mac keyboard is only supported by guests starting with macOS Ventura
|
// Mac keyboard is only supported by guests starting with macOS Ventura
|
||||||
return [VZMacKeyboardConfiguration(), VZUSBKeyboardConfiguration()]
|
return [VZUSBKeyboardConfiguration(), VZMacKeyboardConfiguration()]
|
||||||
} else {
|
} else {
|
||||||
return [VZUSBKeyboardConfiguration()]
|
return [VZUSBKeyboardConfiguration()]
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ struct UnsupportedHostOSError: Error, CustomStringConvertible {
|
|||||||
|
|
||||||
func pointingDevices() -> [VZPointingDeviceConfiguration] {
|
func pointingDevices() -> [VZPointingDeviceConfiguration] {
|
||||||
// Trackpad is only supported by guests starting with macOS Ventura
|
// Trackpad is only supported by guests starting with macOS Ventura
|
||||||
[VZMacTrackpadConfiguration(), VZUSBScreenCoordinatePointingDeviceConfiguration()]
|
[VZUSBScreenCoordinatePointingDeviceConfiguration(), VZMacTrackpadConfiguration()]
|
||||||
}
|
}
|
||||||
|
|
||||||
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
|
func pointingDevicesSuspendable() -> [VZPointingDeviceConfiguration] {
|
||||||
|
|||||||
@@ -8,5 +8,8 @@ protocol Prunable {
|
|||||||
var url: URL { get }
|
var url: URL { get }
|
||||||
func delete() throws
|
func delete() throws
|
||||||
func accessDate() throws -> Date
|
func accessDate() throws -> Date
|
||||||
|
// size on disk as seen in Finder including empty blocks
|
||||||
func sizeBytes() throws -> Int
|
func sizeBytes() throws -> Int
|
||||||
|
// actual size on disk without empty blocks
|
||||||
|
func allocatedSizeBytes() throws -> Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ extension URL: Prunable {
|
|||||||
try FileManager.default.removeItem(at: self)
|
try FileManager.default.removeItem(at: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sizeBytes() throws -> Int {
|
func allocatedSizeBytes() throws -> Int {
|
||||||
try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize!
|
try resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sizeBytes() throws -> Int {
|
||||||
|
try resourceValues(forKeys: [.totalFileSizeKey]).totalFileSize!
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,14 @@ struct VMDirectory: Prunable {
|
|||||||
try baseURL.accessDate()
|
try baseURL.accessDate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func allocatedSizeBytes() throws -> Int {
|
||||||
|
try configURL.allocatedSizeBytes() + diskURL.allocatedSizeBytes() + nvramURL.allocatedSizeBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func allocatedSizeGB() throws -> Int {
|
||||||
|
try allocatedSizeBytes() / 1000 / 1000 / 1000
|
||||||
|
}
|
||||||
|
|
||||||
func sizeBytes() throws -> Int {
|
func sizeBytes() throws -> Int {
|
||||||
try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes()
|
try configURL.sizeBytes() + diskURL.sizeBytes() + nvramURL.sizeBytes()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,4 +35,27 @@ final class LeasesTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(IPv4Address("1.2.3.4"), leases.ResolveMACAddress(macAddress: macAddress))
|
XCTAssertEqual(IPv4Address("1.2.3.4"), leases.ResolveMACAddress(macAddress: macAddress))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDuplicateYetNotExpiredLeases() throws {
|
||||||
|
let macAddress = MACAddress(fromString: "11:22:33:44:55:66")!
|
||||||
|
|
||||||
|
let leases = try Leases("""
|
||||||
|
{
|
||||||
|
name=debian
|
||||||
|
ip_address=192.168.64.1
|
||||||
|
hw_address=1,\(macAddress)
|
||||||
|
identifier=1,\(macAddress)
|
||||||
|
lease=\(Int((Date() + 10.minutes).timeIntervalSince1970).hex)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name=debian
|
||||||
|
ip_address=192.168.64.2
|
||||||
|
hw_address=1,\(macAddress)
|
||||||
|
identifier=1,\(macAddress)
|
||||||
|
lease=\(Int((Date() + 5.minutes).timeIntervalSince1970).hex)
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
XCTAssertEqual(IPv4Address("192.168.64.1"), leases.ResolveMACAddress(macAddress: macAddress))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ and before reaching the 100 CPU cores limit is royalty-free and does not have th
|
|||||||
|
|
||||||
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold Tier License](/licensing#license-tiers),
|
When an organization surpasses the 100 CPU cores limit, they will be required to obtain a [Gold Tier License](/licensing#license-tiers),
|
||||||
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](/licensing#license-tiers)
|
which costs \$1000 per month. Upon reaching a limit of 500 CPU cores, a [Platinum Tier License](/licensing#license-tiers)
|
||||||
(\$5000 per month) will be required, and for organizations that exceed 5000 CPU cores, a custom [Diamond Tier License](/licensing#license-tiers)
|
(\$3000 per month) will be required, and for organizations that exceed 3000 CPU cores, a custom [Diamond Tier License](/licensing#license-tiers)
|
||||||
(\$1 per core per month) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.**
|
(\$1 per core per month) will be necessary. **All paid license tiers will include priority feature development and SLAs on support with urgent issues.**
|
||||||
|
|
||||||
## Have we considered alternatives?
|
## Have we considered alternatives?
|
||||||
|
|||||||
+2
-2
@@ -40,9 +40,9 @@ Gold Tier license has a 500 CPU core limit for Tart and 20 Orchard Workers limit
|
|||||||
|
|
||||||
### Platinum Tier
|
### Platinum Tier
|
||||||
|
|
||||||
If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$5000 per month.
|
If an organization wishes to exceed the limits of the Gold Tier license, a purchase of the [Platinum Tier License](#get-the-license) is required, which costs \$3000 per month.
|
||||||
|
|
||||||
Platinum Tier license has a 5,000 CPU core limit for Tart and 200 Orchard Workers limit for Orchard.
|
Platinum Tier license has a 3,000 CPU core limit for Tart and 200 Orchard Workers limit for Orchard.
|
||||||
|
|
||||||
### Diamond Tier
|
### Diamond Tier
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from testcontainers.core.waiting_utils import wait_container_is_ready
|
from testcontainers.core.waiting_utils import wait_container_is_ready
|
||||||
from testcontainers.general import DockerContainer
|
from testcontainers.core.container import DockerContainer
|
||||||
|
|
||||||
|
|
||||||
class DockerRegistry(DockerContainer):
|
class DockerRegistry(DockerContainer):
|
||||||
|
|||||||
@@ -8,8 +8,11 @@ set -e
|
|||||||
swift build --product tart
|
swift build --product tart
|
||||||
codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
|
codesign --sign - --entitlements Resources/tart-dev.entitlements --force .build/debug/tart
|
||||||
|
|
||||||
mkdir -p .build/tart.app/Contents/MacOS
|
rm -Rf .build/tart.app/
|
||||||
|
mkdir -p .build/tart.app/Contents/MacOS .build/tart.app/Contents/Resources
|
||||||
cp -c .build/debug/tart .build/tart.app/Contents/MacOS/tart
|
cp -c .build/debug/tart .build/tart.app/Contents/MacOS/tart
|
||||||
cp -c Resources/embedded.provisionprofile .build/tart.app/Contents/embedded.provisionprofile
|
cp -c Resources/embedded.provisionprofile .build/tart.app/Contents/embedded.provisionprofile
|
||||||
|
cp -c Resources/Info.plist .build/tart.app/Contents/Info.plist
|
||||||
|
cp -c Resources/AppIcon.png .build/tart.app/Contents/Resources
|
||||||
|
|
||||||
.build/tart.app/Contents/MacOS/tart "$@"
|
.build/tart.app/Contents/MacOS/tart "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user