mirror of
https://github.com/cirruslabs/tart.git
synced 2026-10-18 21:47:08 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea6fd814f5 | ||
|
|
e72fcd9b19 | ||
|
|
33a51b7344 | ||
|
|
5da1a085d3 | ||
|
|
f62949b6f4 | ||
|
|
d7561cab0b | ||
|
|
066f585c53 | ||
|
|
d8ac36b3bd | ||
|
|
4a454a3115 | ||
|
|
94e9425db7 | ||
|
|
0ab0fe23c5 | ||
|
|
0a970462e8 | ||
|
|
5b8d1d1168 | ||
|
|
97c63bcc5b | ||
|
|
6409d53814 |
@@ -80,8 +80,6 @@ task:
|
|||||||
always:
|
always:
|
||||||
dist_artifacts:
|
dist_artifacts:
|
||||||
path: "dist/*"
|
path: "dist/*"
|
||||||
pkg_artifacts:
|
|
||||||
path: ".ci/*.pkg"
|
|
||||||
|
|
||||||
task:
|
task:
|
||||||
name: Release
|
name: Release
|
||||||
|
|||||||
+6
-11
@@ -4,7 +4,6 @@ before:
|
|||||||
hooks:
|
hooks:
|
||||||
- .ci/set-version.sh
|
- .ci/set-version.sh
|
||||||
- swift build -c debug --product tart
|
- swift build -c debug --product tart
|
||||||
- gon gon.hcl
|
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- builder: prebuilt
|
- builder: prebuilt
|
||||||
@@ -16,22 +15,20 @@ builds:
|
|||||||
path: .build/arm64-apple-macosx/debug/tart
|
path: .build/arm64-apple-macosx/debug/tart
|
||||||
hooks:
|
hooks:
|
||||||
post:
|
post:
|
||||||
- ./.ci/create-pkg.sh
|
- gon gon.hcl
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- name_template: "{{ .ProjectName }}"
|
- name_template: "{{ .ProjectName }}"
|
||||||
format: zip
|
|
||||||
files:
|
files:
|
||||||
- src: Resources/embedded.provisionprofile
|
- src: Resources/embedded.provisionprofile
|
||||||
|
dst: tart.app/Contents
|
||||||
|
strip_parent: true
|
||||||
|
- src: ".build/arm64-apple-macosx/debug/tart"
|
||||||
|
dst: tart.app/Contents/MacOS
|
||||||
strip_parent: true
|
strip_parent: true
|
||||||
hooks:
|
|
||||||
after:
|
|
||||||
- xcrun notarytool submit "dist/tart.zip" --keychain-profile "notarytool" --wait
|
|
||||||
|
|
||||||
release:
|
release:
|
||||||
prerelease: auto
|
prerelease: auto
|
||||||
extra_files:
|
|
||||||
- glob: ./.ci/Tart-{{ .Tag }}.pkg
|
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- name: tart
|
- name: tart
|
||||||
@@ -46,9 +43,7 @@ brews:
|
|||||||
dependencies:
|
dependencies:
|
||||||
- "cirruslabs/cli/softnet"
|
- "cirruslabs/cli/softnet"
|
||||||
install: |
|
install: |
|
||||||
mkdir_p libexec/"tart.app/Contents/MacOS/"
|
libexec.install Dir["*"]
|
||||||
libexec.install "tart" => "tart.app/Contents/MacOS/tart"
|
|
||||||
libexec.install "embedded.provisionprofile" => "tart.app/Contents/embedded.provisionprofile"
|
|
||||||
bin.write_exec_script "#{libexec}/tart.app/Contents/MacOS/tart"
|
bin.write_exec_script "#{libexec}/tart.app/Contents/MacOS/tart"
|
||||||
custom_block: |
|
custom_block: |
|
||||||
depends_on :macos => :monterey
|
depends_on :macos => :monterey
|
||||||
|
|||||||
+2
-2
@@ -23,8 +23,8 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/getsentry/sentry-cocoa",
|
"location" : "https://github.com/getsentry/sentry-cocoa",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "c3c19e29f775ee95b77aa3168f3e2fd6c20deba6",
|
"revision" : "ac224c437a3070ffe34460137ac8761eddaf2852",
|
||||||
"version" : "7.31.3"
|
"version" : "8.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
|||||||
.package(url: "https://github.com/antlr/antlr4", branch: "dev"),
|
.package(url: "https://github.com/antlr/antlr4", branch: "dev"),
|
||||||
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
|
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.0.0")),
|
||||||
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.6"),
|
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.50.6"),
|
||||||
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "7.31.3"),
|
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.3.3"),
|
||||||
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ Many more companies are using Tart in their internal setups. Here are a few of t
|
|||||||
<a href="https://symflower.com/" target=_blank>
|
<a href="https://symflower.com/" target=_blank>
|
||||||
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Symflower.png" height="65"/>
|
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Symflower.png" height="65"/>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://transloadit.com/" target=_blank>
|
||||||
|
<img src="https://github.com/cirruslabs/tart/raw/main/Resources/Users/Transloadit.png" height="65"/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
|
**Note:** If your company or project is using Tart please consider [adding yourself to the list above](/Resources/Users/HowToAddYourself.md).
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -1,16 +1,44 @@
|
|||||||
import ArgumentParser
|
import ArgumentParser
|
||||||
|
import Foundation
|
||||||
|
|
||||||
struct Export: AsyncParsableCommand {
|
struct Export: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Export VM to a file")
|
static var configuration = CommandConfiguration(abstract: "Export VM to a compressed .tvm file")
|
||||||
|
|
||||||
@Argument(help: "Source VM name.")
|
@Argument(help: "Source VM name.")
|
||||||
var name: String
|
var name: String
|
||||||
|
|
||||||
@Argument(help: "Path to the destination file.")
|
@Argument(help: "Path to the destination file.")
|
||||||
var path: String
|
var path: String?
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
|
let correctedPath: String
|
||||||
|
|
||||||
|
if let path = path {
|
||||||
|
correctedPath = path
|
||||||
|
} else {
|
||||||
|
correctedPath = "\(name).tvm"
|
||||||
|
|
||||||
|
if FileManager.default.fileExists(atPath: correctedPath) {
|
||||||
|
while true {
|
||||||
|
if userWantsOverwrite(correctedPath) {
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
print("exporting...")
|
print("exporting...")
|
||||||
try VMStorageHelper.open(name).exportToArchive(path: path)
|
|
||||||
|
try VMStorageHelper.open(name).exportToArchive(path: correctedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func userWantsOverwrite(_ filename: String) -> Bool {
|
||||||
|
print("file \(filename) already exists, are you sure you want to overwrite it? (yes, [no])? ", terminator: "")
|
||||||
|
|
||||||
|
let answer = readLine()!
|
||||||
|
|
||||||
|
return answer == "yes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ struct Get: AsyncParsableCommand {
|
|||||||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||||
let diskSizeInGb = try vmDir.sizeGB()
|
let diskSizeInGb = try vmDir.sizeGB()
|
||||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||||
let running = try PIDLock(lockURL: vmDir.configURL).pid() > 0
|
|
||||||
|
|
||||||
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb, Display: vmConfig.display.description, Running: running)
|
let info = VMInfo(CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: diskSizeInGb,
|
||||||
|
Display: vmConfig.display.description, Running: try vmDir.running())
|
||||||
print(format.renderSingle(info))
|
print(format.renderSingle(info))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ import Network
|
|||||||
import SystemConfiguration
|
import SystemConfiguration
|
||||||
import Sentry
|
import Sentry
|
||||||
|
|
||||||
|
enum IPResolutionStrategy: String, ExpressibleByArgument, CaseIterable {
|
||||||
|
case dhcp, arp
|
||||||
|
|
||||||
|
private(set) static var allValueStrings: [String] = Format.allCases.map { "\($0)"}
|
||||||
|
}
|
||||||
|
|
||||||
struct IP: AsyncParsableCommand {
|
struct IP: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
|
static var configuration = CommandConfiguration(abstract: "Get VM's IP address")
|
||||||
|
|
||||||
@@ -13,46 +19,40 @@ struct IP: AsyncParsableCommand {
|
|||||||
@Option(help: "Number of seconds to wait for a potential VM booting")
|
@Option(help: "Number of seconds to wait for a potential VM booting")
|
||||||
var wait: UInt16 = 0
|
var wait: UInt16 = 0
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp("Strategy for resolving IP address: dhcp or arp",
|
||||||
|
discussion: """
|
||||||
|
By default, Tart is looking up and parsing DHCP lease file to determine the IP of the VM.\n
|
||||||
|
This method is fast and the most reliable but only returns local IP adresses.\n
|
||||||
|
Alternatively, Tart can call external `arp` executable and parse it's output.\n
|
||||||
|
In case of enabled Bridged Networking this method will return VM's IP address on the network interface used for Bridged Networking.\n
|
||||||
|
Note that `arp` strategy won't work for VMs using `--net-softnet`.
|
||||||
|
"""))
|
||||||
|
var resolver: IPResolutionStrategy = .dhcp
|
||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
let vmDir = try VMStorageLocal().open(name)
|
let vmDir = try VMStorageLocal().open(name)
|
||||||
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
let vmConfig = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
|
let vmMACAddress = MACAddress(fromString: vmConfig.macAddress.string)!
|
||||||
|
|
||||||
guard let ipViaDHCP = try await IP.resolveIP(vmMACAddress, secondsToWait: wait) else {
|
guard let ip = try await IP.resolveIP(vmMACAddress, resolutionStrategy: resolver, secondsToWait: wait) else {
|
||||||
throw RuntimeError.NoIPAddressFound("no IP address found, is your VM running?")
|
throw RuntimeError.NoIPAddressFound("no IP address found, is your VM running?")
|
||||||
}
|
}
|
||||||
|
print(ip)
|
||||||
let arpCache = try ARPCache()
|
|
||||||
|
|
||||||
if let ipViaARP = try arpCache.ResolveMACAddress(macAddress: vmMACAddress), ipViaARP != ipViaDHCP {
|
|
||||||
// Capture the warning into Sentry
|
|
||||||
SentrySDK.capture(message: "DHCP lease and ARP cache entries for a single MAC address differ") { scope in
|
|
||||||
scope.setLevel(.warning)
|
|
||||||
|
|
||||||
scope.setContext(value: [
|
|
||||||
"MAC address": vmMACAddress,
|
|
||||||
"IP via ARP": ipViaARP,
|
|
||||||
"IP via DHCP": ipViaDHCP,
|
|
||||||
], key: "Address conflict details")
|
|
||||||
|
|
||||||
scope.add(Attachment(path: "/var/db/dhcpd_leases", filename: "dhcpd_leases.txt", contentType: "text/plain"))
|
|
||||||
scope.add(Attachment(data: arpCache.arpCommandOutput, filename: "arp-an-output.txt", contentType: "text/plain"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fputs("WARNING: DHCP lease and ARP cache entries for MAC address \(vmMACAddress) differ: "
|
|
||||||
+ "got \(ipViaDHCP) and \(ipViaARP) respectively, consider reporting this case to"
|
|
||||||
+ " https://github.com/cirruslabs/tart/issues/172\n", stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
print(ipViaDHCP)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static public func resolveIP(_ vmMACAddress: MACAddress, secondsToWait: UInt16) async throws -> IPv4Address? {
|
static public func resolveIP(_ vmMACAddress: MACAddress, resolutionStrategy: IPResolutionStrategy = .dhcp, secondsToWait: UInt16 = 0) async throws -> IPv4Address? {
|
||||||
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
|
let waitUntil = Calendar.current.date(byAdding: .second, value: Int(secondsToWait), to: Date.now)!
|
||||||
|
|
||||||
repeat {
|
repeat {
|
||||||
if let leases = try Leases(), let ip = try leases.resolveMACAddress(macAddress: vmMACAddress) {
|
switch resolutionStrategy {
|
||||||
return ip
|
case .arp:
|
||||||
|
if let ip = try ARPCache().ResolveMACAddress(macAddress: vmMACAddress) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
case .dhcp:
|
||||||
|
if let leases = try Leases(), let ip = try leases.ResolveMACAddress(macAddress: vmMACAddress) {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wait a second
|
// wait a second
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ArgumentParser
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct Import: AsyncParsableCommand {
|
struct Import: AsyncParsableCommand {
|
||||||
static var configuration = CommandConfiguration(abstract: "Import VM from a file")
|
static var configuration = CommandConfiguration(abstract: "Import VM from a compressed .tvm file")
|
||||||
|
|
||||||
@Argument(help: "Path to a file created with \"tart export\".")
|
@Argument(help: "Path to a file created with \"tart export\".")
|
||||||
var path: String
|
var path: String
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ fileprivate struct VMInfo: Encodable {
|
|||||||
let Source: String
|
let Source: String
|
||||||
let Name: String
|
let Name: String
|
||||||
let Size: Int
|
let Size: Int
|
||||||
|
let Running: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct List: AsyncParsableCommand {
|
struct List: AsyncParsableCommand {
|
||||||
@@ -32,17 +33,19 @@ struct List: AsyncParsableCommand {
|
|||||||
|
|
||||||
func run() async throws {
|
func run() async throws {
|
||||||
var infos: [VMInfo] = []
|
var infos: [VMInfo] = []
|
||||||
|
|
||||||
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())
|
try VMInfo(Source: "local", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
try VMInfo(Source: "oci", Name: name, Size: vmDir.sizeGB(), Running: vmDir.running())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (quiet) {
|
if (quiet) {
|
||||||
for info in infos {
|
for info in infos {
|
||||||
print(info.Name)
|
print(info.Name)
|
||||||
|
|||||||
@@ -20,6 +20,17 @@ struct Run: AsyncParsableCommand {
|
|||||||
discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM."))
|
discussion: "Useful for integrating Tart VMs into other tools.\nUse `tart ip` in order to get an IP for SSHing or VNCing into the VM."))
|
||||||
var noGraphics: Bool = false
|
var noGraphics: Bool = false
|
||||||
|
|
||||||
|
@Flag(help: ArgumentHelp(
|
||||||
|
"Open serial console in /dev/ttySXX",
|
||||||
|
discussion: "Useful for debugging Linux Kernel."))
|
||||||
|
var serial: Bool = false
|
||||||
|
|
||||||
|
@Option(help: ArgumentHelp(
|
||||||
|
"Attach an externally created serial console",
|
||||||
|
discussion: "Alternative to `--serial` flag for programmatic integrations."
|
||||||
|
))
|
||||||
|
var serialPath: String?
|
||||||
|
|
||||||
@Flag(help: "Force open a UI window, even when VNC is enabled.")
|
@Flag(help: "Force open a UI window, even when VNC is enabled.")
|
||||||
var graphics: Bool = false
|
var graphics: Bool = false
|
||||||
|
|
||||||
@@ -118,11 +129,30 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var serialPorts: [VZSerialPortConfiguration] = []
|
||||||
|
if serial {
|
||||||
|
let tty_fd = createPTY()
|
||||||
|
if (tty_fd < 0) {
|
||||||
|
throw RuntimeError.VMConfigurationError("Failed to create PTY")
|
||||||
|
}
|
||||||
|
let tty_read = FileHandle.init(fileDescriptor: tty_fd)
|
||||||
|
let tty_write = FileHandle.init(fileDescriptor: tty_fd)
|
||||||
|
serialPorts.append(createSerialPortConfiguration(tty_read, tty_write))
|
||||||
|
} else if serialPath != nil {
|
||||||
|
let tty_read = FileHandle.init(forReadingAtPath: serialPath!)
|
||||||
|
let tty_write = FileHandle.init(forWritingAtPath: serialPath!)
|
||||||
|
if (tty_read == nil || tty_write == nil) {
|
||||||
|
throw RuntimeError.VMConfigurationError("Failed to open PTY")
|
||||||
|
}
|
||||||
|
serialPorts.append(createSerialPortConfiguration(tty_read!, tty_write!))
|
||||||
|
}
|
||||||
|
|
||||||
vm = try VM(
|
vm = try VM(
|
||||||
vmDir: vmDir,
|
vmDir: vmDir,
|
||||||
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
|
network: userSpecifiedNetwork(vmDir: vmDir) ?? NetworkShared(),
|
||||||
additionalDiskAttachments: additionalDiskAttachments,
|
additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directoryShares() + rosettaDirectoryShare()
|
directorySharingDevices: directoryShares() + rosettaDirectoryShare(),
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
|
|
||||||
let vncImpl: VNC? = try {
|
let vncImpl: VNC? = try {
|
||||||
@@ -198,6 +228,16 @@ struct Run: AsyncParsableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func createSerialPortConfiguration(_ tty_read: FileHandle, _ tty_write: FileHandle) -> VZVirtioConsoleDeviceSerialPortConfiguration {
|
||||||
|
let serialPortConfiguration = VZVirtioConsoleDeviceSerialPortConfiguration()
|
||||||
|
let serialPortAttachment = VZFileHandleSerialPortAttachment(
|
||||||
|
fileHandleForReading: tty_read,
|
||||||
|
fileHandleForWriting: tty_write)
|
||||||
|
|
||||||
|
serialPortConfiguration.attachment = serialPortAttachment
|
||||||
|
return serialPortConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
func isInteractiveSession() -> Bool {
|
func isInteractiveSession() -> Bool {
|
||||||
isatty(STDOUT_FILENO) == 1
|
isatty(STDOUT_FILENO) == 1
|
||||||
}
|
}
|
||||||
@@ -357,7 +397,13 @@ struct Run: AsyncParsableCommand {
|
|||||||
VMView(vm: vm!).onAppear {
|
VMView(vm: vm!).onAppear {
|
||||||
NSWindow.allowsAutomaticWindowTabbing = false
|
NSWindow.allowsAutomaticWindowTabbing = false
|
||||||
}.onDisappear {
|
}.onDisappear {
|
||||||
NSApplication.shared.terminate(self)
|
let ret = kill(getpid(), SIGINT)
|
||||||
|
if ret != 0 {
|
||||||
|
// Fallback to the old termination method that doesn't
|
||||||
|
// propagate the cancellation to Task's in case graceful
|
||||||
|
// termination via kill(2) is not successful
|
||||||
|
NSApplication.shared.terminate(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
|
}.frame(width: CGFloat(vm!.config.display.width), height: CGFloat(vm!.config.display.height))
|
||||||
}.commands {
|
}.commands {
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ struct ARPCache {
|
|||||||
self.arpCommandOutput = arpCommandOutput
|
self.arpCommandOutput = arpCommandOutput
|
||||||
}
|
}
|
||||||
|
|
||||||
func ResolveMACAddress(macAddress: MACAddress, bridgeOnly: Bool = true) throws -> IPv4Address? {
|
func ResolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||||
let lines = String(decoding: arpCommandOutput, as: UTF8.self)
|
let lines = String(decoding: arpCommandOutput, as: UTF8.self)
|
||||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
.components(separatedBy: "\n")
|
.components(separatedBy: "\n")
|
||||||
@@ -95,11 +95,6 @@ struct ARPCache {
|
|||||||
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
|
throw ARPCommandYieldedInvalidOutputError(explanation: "failed to parse MAC address \(rawMAC)")
|
||||||
}
|
}
|
||||||
|
|
||||||
let interface = try match.getCaptureGroup(name: "interface", for: line)
|
|
||||||
if bridgeOnly && !interface.starts(with: "bridge") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if macAddress == mac {
|
if macAddress == mac {
|
||||||
return ip
|
return ip
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ class Leases {
|
|||||||
return rawLeases
|
return rawLeases
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
func ResolveMACAddress(macAddress: MACAddress) throws -> IPv4Address? {
|
||||||
leases[macAddress]?.ip
|
leases[macAddress]?.ip
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
func createPTY() -> Int32 {
|
||||||
|
var tty_fd: Int32 = -1
|
||||||
|
var sfd: Int32 = -1
|
||||||
|
var termios_ = termios()
|
||||||
|
let tty_path = UnsafeMutablePointer<CChar>.allocate(capacity: 1024)
|
||||||
|
|
||||||
|
var res = openpty(&tty_fd, &sfd, tty_path, nil, nil);
|
||||||
|
if (res < 0) {
|
||||||
|
perror("openpty error")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// close slave file descriptor
|
||||||
|
close(sfd)
|
||||||
|
|
||||||
|
res = fcntl(tty_fd, F_GETFL)
|
||||||
|
if (res < 0) {
|
||||||
|
perror("fcntl F_GETFL error")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// set serial nonblocking
|
||||||
|
res = fcntl(tty_fd, F_SETFL, res | O_NONBLOCK)
|
||||||
|
if (res < 0) {
|
||||||
|
perror("fcntl F_SETFL O_NONBLOCK error")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// set baudrate to 115200
|
||||||
|
tcgetattr(tty_fd, &termios_)
|
||||||
|
cfsetispeed(&termios_, speed_t(B115200))
|
||||||
|
cfsetospeed(&termios_, speed_t(B115200))
|
||||||
|
if (tcsetattr(tty_fd, TCSANOW, &termios_) != 0) {
|
||||||
|
perror("tcsetattr error")
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
print("Successfully open pty \(String(cString: tty_path))")
|
||||||
|
|
||||||
|
tty_path.deallocate()
|
||||||
|
return tty_fd
|
||||||
|
}
|
||||||
+13
-5
@@ -40,7 +40,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
init(vmDir: VMDirectory,
|
init(vmDir: VMDirectory,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
|
||||||
|
serialPorts: [VZSerialPortConfiguration] = []
|
||||||
) throws {
|
) throws {
|
||||||
name = vmDir.name
|
name = vmDir.name
|
||||||
config = try VMConfig.init(fromURL: vmDir.configURL)
|
config = try VMConfig.init(fromURL: vmDir.configURL)
|
||||||
@@ -54,7 +55,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
|
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL,
|
||||||
nvramURL: vmDir.nvramURL, vmConfig: config,
|
nvramURL: vmDir.nvramURL, vmConfig: config,
|
||||||
network: network, additionalDiskAttachments: additionalDiskAttachments,
|
network: network, additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directorySharingDevices
|
directorySharingDevices: directorySharingDevices,
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
@@ -132,7 +134,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
diskSizeGB: UInt16,
|
diskSizeGB: UInt16,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment] = [],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = []
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration] = [],
|
||||||
|
serialPorts: [VZSerialPortConfiguration] = []
|
||||||
) async throws {
|
) async throws {
|
||||||
var ipswURL = ipswURL
|
var ipswURL = ipswURL
|
||||||
|
|
||||||
@@ -179,7 +182,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
|
let configuration = try Self.craftConfiguration(diskURL: vmDir.diskURL, nvramURL: vmDir.nvramURL,
|
||||||
vmConfig: config, network: network,
|
vmConfig: config, network: network,
|
||||||
additionalDiskAttachments: additionalDiskAttachments,
|
additionalDiskAttachments: additionalDiskAttachments,
|
||||||
directorySharingDevices: directorySharingDevices
|
directorySharingDevices: directorySharingDevices,
|
||||||
|
serialPorts: serialPorts
|
||||||
)
|
)
|
||||||
virtualMachine = VZVirtualMachine(configuration: configuration)
|
virtualMachine = VZVirtualMachine(configuration: configuration)
|
||||||
|
|
||||||
@@ -261,7 +265,8 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
vmConfig: VMConfig,
|
vmConfig: VMConfig,
|
||||||
network: Network = NetworkShared(),
|
network: Network = NetworkShared(),
|
||||||
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
additionalDiskAttachments: [VZDiskImageStorageDeviceAttachment],
|
||||||
directorySharingDevices: [VZDirectorySharingDeviceConfiguration]
|
directorySharingDevices: [VZDirectorySharingDeviceConfiguration],
|
||||||
|
serialPorts: [VZSerialPortConfiguration]
|
||||||
) throws -> VZVirtualMachineConfiguration {
|
) throws -> VZVirtualMachineConfiguration {
|
||||||
let configuration = VZVirtualMachineConfiguration()
|
let configuration = VZVirtualMachineConfiguration()
|
||||||
|
|
||||||
@@ -308,6 +313,9 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
|
|||||||
// Directory sharing devices
|
// Directory sharing devices
|
||||||
configuration.directorySharingDevices = directorySharingDevices
|
configuration.directorySharingDevices = directorySharingDevices
|
||||||
|
|
||||||
|
// Serial Port
|
||||||
|
configuration.serialPorts = serialPorts
|
||||||
|
|
||||||
try configuration.validate()
|
try configuration.validate()
|
||||||
|
|
||||||
return configuration
|
return configuration
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ struct VMDirectory: Prunable {
|
|||||||
baseURL
|
baseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func running() throws -> Bool {
|
||||||
|
try PIDLock(lockURL: configURL).pid() != 0
|
||||||
|
}
|
||||||
|
|
||||||
static func temporary() throws -> VMDirectory {
|
static func temporary() throws -> VMDirectory {
|
||||||
let tmpDir = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString)
|
let tmpDir = try Config().tartTmpDir.appendingPathComponent(UUID().uuidString)
|
||||||
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false)
|
try FileManager.default.createDirectory(at: tmpDir, withIntermediateDirectories: false)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ extension Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RuntimeError : Error {
|
enum RuntimeError : Error {
|
||||||
|
case VMConfigurationError(_ message: String)
|
||||||
case VMDoesNotExist(name: String)
|
case VMDoesNotExist(name: String)
|
||||||
case VMMissingFiles(_ message: String)
|
case VMMissingFiles(_ message: String)
|
||||||
case VMNotRunning(_ message: String)
|
case VMNotRunning(_ message: String)
|
||||||
@@ -65,6 +66,8 @@ protocol HasExitCode {
|
|||||||
extension RuntimeError : CustomStringConvertible {
|
extension RuntimeError : CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
|
case .VMConfigurationError(let message):
|
||||||
|
return message
|
||||||
case .VMDoesNotExist(let name):
|
case .VMDoesNotExist(let name):
|
||||||
return "the specified VM \"\(name)\" does not exist"
|
return "the specified VM \"\(name)\" does not exist"
|
||||||
case .VMMissingFiles(let message):
|
case .VMMissingFiles(let message):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ final class MACAddressResolverTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMultipleEntries() throws {
|
func testMultipleEntries() throws {
|
||||||
@@ -28,8 +28,8 @@ final class MACAddressResolverTests: XCTestCase {
|
|||||||
""")
|
""")
|
||||||
|
|
||||||
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
XCTAssertEqual(IPv4Address("1.2.3.4"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "00:11:22:33:44:55")!))
|
||||||
XCTAssertEqual(IPv4Address("5.6.7.8"),
|
XCTAssertEqual(IPv4Address("5.6.7.8"),
|
||||||
try leases.resolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!))
|
try leases.ResolveMACAddress(macAddress: MACAddress(fromString: "AA:BB:CC:DD:EE:FF")!))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ Tart already powers several CI services mentioned above including our own [Cirru
|
|||||||
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
|
For services that haven't leveraged Tart yet, we offer fully managed runners via a monthly subscription.
|
||||||
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
|
*Cirrus Runners* is the fastest way to get your current CI workflows to benefit from Apple Silicon hardware. No need to manage infrastructure or migrate to another CI provider.
|
||||||
|
|
||||||
|
## Testimonials from customers
|
||||||
|
|
||||||
|
Sebastian Jachec, Mobile Engineer at [Daybridge](https://www.daybridge.com/).
|
||||||
|
|
||||||
|
> It’s been plain-sailing with the Cirrus Runners — they’ve been great! They’re consistently 60+% faster on workflows that we previously used Github Actions’ macOS runners for.
|
||||||
|
|
||||||
|
Max Lapides, Senior Mobile Engineer at [Tonal](https://www.tonal.com/).
|
||||||
|
|
||||||
|
> Previously, we were using the GitHub‑hosted macOS runners and our iOS build took ~30 minutes. Now with Cirrus Runners, the iOS build only takes ~12 minutes. That’s a huge boost to our productivity, and for only $150/month per runner it is much less expensive too.
|
||||||
|
|
||||||
|
|
||||||
## Configuring Cirrus Runners
|
## Configuring Cirrus Runners
|
||||||
|
|
||||||
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
|
Configuring Cirrus Runners for GitHub Actions is as simple as installing [Cirrus Runners App](https://github.com/apps/cirrus-runners).
|
||||||
|
|||||||
Vendored
+3
-3
@@ -169,7 +169,7 @@
|
|||||||
<figcaption class="md-typeset">
|
<figcaption class="md-typeset">
|
||||||
<p>
|
<p>
|
||||||
Tart powers several continuous integration systems including
|
Tart powers several continuous integration systems including
|
||||||
<a href="/github-actions"
|
<a href="/integrations/github-actions"
|
||||||
>on‑demand GitHub Actions Runners</a
|
>on‑demand GitHub Actions Runners</a
|
||||||
>
|
>
|
||||||
and
|
and
|
||||||
@@ -261,7 +261,7 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<cite>
|
<cite>
|
||||||
It’s been plain-sailing with the
|
It’s been plain-sailing with the
|
||||||
<a href="/github-actions">Cirrus Runners</a> —
|
<a href="/integrations/github-actions">Cirrus Runners</a> —
|
||||||
they’ve been great! They’re consistently 60+%
|
they’ve been great! They’re consistently 60+%
|
||||||
faster on workflows that we previously used Github
|
faster on workflows that we previously used Github
|
||||||
Actions’ macOS runners for.
|
Actions’ macOS runners for.
|
||||||
@@ -310,7 +310,7 @@
|
|||||||
<cite>
|
<cite>
|
||||||
Previously, we were using the GitHub‑hosted macOS runners
|
Previously, we were using the GitHub‑hosted macOS runners
|
||||||
and our iOS build took ~30 minutes. Now with
|
and our iOS build took ~30 minutes. Now with
|
||||||
<a href="/github-actions">Cirrus Runners</a>, the iOS build only
|
<a href="/integrations/github-actions">Cirrus Runners</a>, the iOS build only
|
||||||
takes ~12 minutes. That’s a huge boost to our productivity,
|
takes ~12 minutes. That’s a huge boost to our productivity,
|
||||||
and for only $150/month per runner it is much less expensive too.
|
and for only $150/month per runner it is much less expensive too.
|
||||||
</cite>
|
</cite>
|
||||||
|
|||||||
Reference in New Issue
Block a user