Compare commits

..
10 Commits
Author SHA1 Message Date
Nikolay Edigaryev b52a857698 tart {clone,pull}: make deduplication opt-in (#924) 2024-10-25 17:56:38 +04:00
Nikolay Edigaryev 3bf0bb22f3 CI: populate CFBundleShortVersionString in Info.plist (#923) 2024-10-24 18:47:50 +00:00
Nikolay Edigaryev accbd0cb33 Registry: prevent double authorization when getting a new token (#922) 2024-10-23 23:51:55 +04:00
Nikolay Edigaryev c0b20932c7 Prevent pipe deadlock when spawning a Process() (#916) 2024-10-02 09:48:23 -04:00
Nikolay Edigaryev 3694af946c Document automatic pruning in FAQ (#913)
* Document automatic pruning in FAQ

* tart {pull,clone}: consistent automatic pruning documentation in --help
2024-09-30 21:56:55 +04:00
Nikolay Edigaryev dbf711a6c9 tart delete: return human-friendly error when local VM doesn't exist (#910) 2024-09-26 14:45:14 +04:00
Nikolay Edigaryev b9f24a40c1 Info.plist: set CFBundleName and CFBundleDisplayName to Tart (#909) 2024-09-24 17:24:50 +04:00
Nikolay Edigaryev 10c6ace671 Re-generate ANTLR files using ANTLR 4.13.2 (#907)
* Re-generate ANTLR files using ANTLR 4.13.2

* Package.swift: require exactly ANTLR of version 4.13.2
2024-09-20 17:50:14 +04:00
Nikolay Edigaryev b98e23956b Package.swift: bump Sentry SDK to 8.36.0 + upgrade other packages (#905)
* Package.swift: bump Sentry SDK to 8.36.0

* $ swift package update
2024-09-19 19:06:37 +00:00
Fedor Korotkov ce23f9c2a7 Completely disable audio devices in case of --no-audio (#904)
This way VM won't have empty audio device at all.

This should fix with an issue like that https://github.com/actions/runner-images/issues/9330
2024-09-17 09:40:37 +00:00
22 changed files with 143 additions and 90 deletions
+2
View File
@@ -3,3 +3,5 @@
TMPFILE=$(mktemp) TMPFILE=$(mktemp)
envsubst < Sources/tart/CI/CI.swift > $TMPFILE envsubst < Sources/tart/CI/CI.swift > $TMPFILE
mv $TMPFILE Sources/tart/CI/CI.swift mv $TMPFILE Sources/tart/CI/CI.swift
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string ${CIRRUS_TAG}" Info.plist
+18 -18
View File
@@ -1,13 +1,13 @@
{ {
"originHash" : "2c514a4a1d7e106713db744bee89edb40d75da63e6611990ec2f4b0da53c0455", "originHash" : "6a15657d8cb1d3e2b447f31aff5b47d6a9655d2262e48ca76476ba525435269b",
"pins" : [ "pins" : [
{ {
"identity" : "antlr4", "identity" : "antlr4",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/antlr/antlr4", "location" : "https://github.com/antlr/antlr4",
"state" : { "state" : {
"branch" : "dev", "revision" : "cc82115a4e7f53d71d9d905caa2c2dfa4da58899",
"revision" : "2703a8516c0fb7fe92db6b9c40e0113f577646d2" "version" : "4.13.2"
} }
}, },
{ {
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/groue/Semaphore", "location" : "https://github.com/groue/Semaphore",
"state" : { "state" : {
"revision" : "f1c4a0acabeb591068dea6cffdd39660b86dec28", "revision" : "2543679282aa6f6c8ecf2138acd613ed20790bc2",
"version" : "0.0.8" "version" : "0.1.0"
} }
}, },
{ {
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa", "location" : "https://github.com/getsentry/sentry-cocoa",
"state" : { "state" : {
"revision" : "ef4fec9dfb8dd5027b09a4a5c9362feafd118e1a", "revision" : "5575af93efb776414f243e93d6af9f6258dc539a",
"version" : "8.24.0" "version" : "8.36.0"
} }
}, },
{ {
@@ -51,8 +51,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser", "location" : "https://github.com/apple/swift-argument-parser",
"state" : { "state" : {
"revision" : "46989693916f56d1186bd59ac15124caef896560", "revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.3.1" "version" : "1.5.0"
} }
}, },
{ {
@@ -61,7 +61,7 @@
"location" : "https://github.com/apple/swift-async-algorithms", "location" : "https://github.com/apple/swift-async-algorithms",
"state" : { "state" : {
"branch" : "main", "branch" : "main",
"revision" : "f05e450f0b909c0e80670a47516c4b9700b9e5da" "revision" : "5c8bd186f48c16af0775972700626f0b74588278"
} }
}, },
{ {
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-collections.git", "location" : "https://github.com/apple/swift-collections.git",
"state" : { "state" : {
"revision" : "f504716c27d2e5d4144fa4794b12129301d17729", "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06",
"version" : "1.0.3" "version" : "1.1.3"
} }
}, },
{ {
@@ -87,8 +87,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-log.git", "location" : "https://github.com/apple/swift-log.git",
"state" : { "state" : {
"revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", "revision" : "9cb486020ebf03bfa5b5df985387a14a98744537",
"version" : "1.5.4" "version" : "1.6.1"
} }
}, },
{ {
@@ -105,8 +105,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/fumoboy007/swift-retry", "location" : "https://github.com/fumoboy007/swift-retry",
"state" : { "state" : {
"revision" : "9f133487ffc2ab4539688c29efe57bb1ba31d7b0", "revision" : "df9d7b185d2e433147ec0083a73c257e665eea0d",
"version" : "0.2.3" "version" : "0.2.4"
} }
}, },
{ {
@@ -141,8 +141,8 @@
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/nicklockwood/SwiftFormat", "location" : "https://github.com/nicklockwood/SwiftFormat",
"state" : { "state" : {
"revision" : "9df3b01f477163b33d5e63c5e2e5b9f946a49c56", "revision" : "ab6844edb79a7b88dc6320e6cee0a0db7674dac3",
"version" : "0.53.6" "version" : "0.54.5"
} }
}, },
{ {
+2 -2
View File
@@ -15,10 +15,10 @@ let package = Package(
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"), .package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
.package(url: "https://github.com/apple/swift-async-algorithms", branch: "main"), .package(url: "https://github.com/apple/swift-async-algorithms", branch: "main"),
.package(url: "https://github.com/malcommac/SwiftDate", from: "7.0.0"), .package(url: "https://github.com/malcommac/SwiftDate", from: "7.0.0"),
.package(url: "https://github.com/antlr/antlr4", branch: "dev"), .package(url: "https://github.com/antlr/antlr4", exact: "4.13.2"),
.package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")), .package(url: "https://github.com/apple/swift-atomics.git", .upToNextMajor(from: "1.2.0")),
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.6"), .package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.6"),
.package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.24.0"), .package(url: "https://github.com/getsentry/sentry-cocoa", from: "8.36.0"),
.package(url: "https://github.com/cfilipov/TextTable", branch: "master"), .package(url: "https://github.com/cfilipov/TextTable", branch: "master"),
.package(url: "https://github.com/sersoft-gmbh/swift-sysctl.git", from: "1.8.0"), .package(url: "https://github.com/sersoft-gmbh/swift-sysctl.git", from: "1.8.0"),
.package(url: "https://github.com/orchetect/SwiftRadix", from: "1.3.1"), .package(url: "https://github.com/orchetect/SwiftRadix", from: "1.3.1"),
+19 -17
View File
@@ -2,22 +2,24 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>tart</string> <string>Tart</string>
<key>CFBundleIdentifier</key> <key>CFBundleDisplayName</key>
<string>org.cirruslabs.tart</string> <string>Tart</string>
<key>CFBundleExecutable</key> <key>CFBundleIdentifier</key>
<string>tart</string> <string>org.cirruslabs.tart</string>
<key>LSBackgroundOnly</key> <key>CFBundleExecutable</key>
<string>1</string> <string>tart</string>
<key>CFBundleIconFiles</key> <key>LSBackgroundOnly</key>
<array> <string>1</string>
<string>AppIcon.png</string> <key>CFBundleIconFiles</key>
</array> <array>
<key>NSAppTransportSecurity</key> <string>AppIcon.png</string>
<dict> </array>
<key>NSAllowsArbitraryLoads</key> <key>NSAppTransportSecurity</key>
<true/> <dict>
</dict> <key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict> </dict>
</plist> </plist>
+7 -6
View File
@@ -9,12 +9,10 @@ struct Clone: AsyncParsableCommand {
Creates a local virtual machine by cloning either a remote or another local virtual machine. Creates a local virtual machine by cloning either a remote or another local virtual machine.
Due to copy-on-write magic in Apple File System, a cloned VM won't actually claim all the space right away. Due to copy-on-write magic in Apple File System, a cloned VM won't actually claim all the space right away.
Only changes to a cloned disk will be written and claim new space. By default, Tart checks available capacity Only changes to a cloned disk will be written and claim new space. This also speeds up clones enormously.
in Tart's home directory and checks if there is enough space for the worst possible scenario: when the whole disk
will be modified.
This behaviour can be disabled by setting TART_NO_AUTO_PRUNE environment variable. This might be helpful By default, Tart checks available capacity in Tart's home directory and tries to reclaim minimum possible storage for the cloned image
for use cases when the original image is very big and a workload is known to only modify a fraction of the cloned disk. to fit. This behaviour is called "automatic pruning" and can be disabled by setting TART_NO_AUTO_PRUNE environment variable.
""" """
) )
@@ -30,6 +28,9 @@ struct Clone: AsyncParsableCommand {
@Option(help: "network concurrency to use when pulling a remote VM from the OCI-compatible registry") @Option(help: "network concurrency to use when pulling a remote VM from the OCI-compatible registry")
var concurrency: UInt = 4 var concurrency: UInt = 4
@Flag(help: .hidden)
var deduplicate: Bool = false
func validate() throws { func validate() throws {
if newName.contains("/") { if newName.contains("/") {
throw ValidationError("<new-name> should be a local name") throw ValidationError("<new-name> should be a local name")
@@ -47,7 +48,7 @@ struct Clone: AsyncParsableCommand {
if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) { if let remoteName = try? RemoteName(sourceName), !ociStorage.exists(remoteName) {
// Pull the VM in case it's OCI-based and doesn't exist locally yet // Pull the VM in case it's OCI-based and doesn't exist locally yet
let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure) let registry = try Registry(host: remoteName.host, namespace: remoteName.namespace, insecure: insecure)
try await ociStorage.pull(remoteName, registry: registry, concurrency: concurrency) try await ociStorage.pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate)
} }
let sourceVM = try VMStorageHelper.open(sourceName) let sourceVM = try VMStorageHelper.open(sourceName)
+6 -3
View File
@@ -9,8 +9,8 @@ struct Pull: AsyncParsableCommand {
Pulls a virtual machine from a remote OCI-compatible registry. Supports authorization via Keychain (see "tart login --help"), Pulls a virtual machine from a remote OCI-compatible registry. Supports authorization via Keychain (see "tart login --help"),
Docker credential helpers defined in ~/.docker/config.json or via TART_REGISTRY_USERNAME/TART_REGISTRY_PASSWORD environment variables. Docker credential helpers defined in ~/.docker/config.json or via TART_REGISTRY_USERNAME/TART_REGISTRY_PASSWORD environment variables.
By default, Tart checks available capacity in Tart's home directory and tries to reclaim minimum possible storage for the remote image to fit via "tart prune". By default, Tart checks available capacity in Tart's home directory and tries to reclaim minimum possible storage for the remote image
This behaviour can be disabled by setting TART_NO_AUTO_PRUNE environment variable. to fit. This behaviour is called "automatic pruning" and can be disabled by setting TART_NO_AUTO_PRUNE environment variable.
""" """
) )
@@ -23,6 +23,9 @@ struct Pull: AsyncParsableCommand {
@Option(help: "network concurrency to use when pulling a remote VM from the OCI-compatible registry") @Option(help: "network concurrency to use when pulling a remote VM from the OCI-compatible registry")
var concurrency: UInt = 4 var concurrency: UInt = 4
@Flag(help: .hidden)
var deduplicate: Bool = false
func validate() throws { func validate() throws {
if concurrency < 1 { if concurrency < 1 {
throw ValidationError("network concurrency cannot be less than 1") throw ValidationError("network concurrency cannot be less than 1")
@@ -43,6 +46,6 @@ struct Pull: AsyncParsableCommand {
defaultLogger.appendNewLine("pulling \(remoteName)...") defaultLogger.appendNewLine("pulling \(remoteName)...")
try await VMStorageOCI().pull(remoteName, registry: registry, concurrency: concurrency) try await VMStorageOCI().pull(remoteName, registry: registry, concurrency: concurrency, deduplicate: deduplicate)
} }
} }
@@ -39,9 +39,10 @@ class DockerConfigCredentialsProvider: CredentialsProvider {
inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!) inPipe.fileHandleForWriting.write("\(host)\n".data(using: .utf8)!)
inPipe.fileHandleForWriting.closeFile() inPipe.fileHandleForWriting.closeFile()
let outputData = try outPipe.fileHandleForReading.readToEnd()
process.waitUntilExit() process.waitUntilExit()
let outputData = try outPipe.fileHandleForReading.readToEnd()
if !(process.terminationReason == .exit && process.terminationStatus == 0) { if !(process.terminationReason == .exit && process.terminationStatus == 0) {
if let outputData = outputData { if let outputData = outputData {
print(String(decoding: outputData, as: UTF8.self)) print(String(decoding: outputData, as: UTF8.self))
@@ -52,6 +52,11 @@ struct ARPCache {
process.standardInput = FileHandle.nullDevice process.standardInput = FileHandle.nullDevice
try process.run() try process.run()
guard let arpCommandOutput = try pipe.fileHandleForReading.readToEnd() else {
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
}
process.waitUntilExit() process.waitUntilExit()
if !(process.terminationReason == .exit && process.terminationStatus == 0) { if !(process.terminationReason == .exit && process.terminationStatus == 0) {
@@ -60,10 +65,6 @@ struct ARPCache {
terminationStatus: process.terminationStatus) terminationStatus: process.terminationStatus)
} }
guard let arpCommandOutput = try pipe.fileHandleForReading.readToEnd() else {
throw ARPCommandYieldedInvalidOutputError(explanation: "empty output")
}
self.arpCommandOutput = arpCommandOutput self.arpCommandOutput = arpCommandOutput
} }
+1 -1
View File
@@ -2,5 +2,5 @@ import Foundation
protocol Disk { protocol Disk {
static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer] static func push(diskURL: URL, registry: Registry, chunkSizeMb: Int, concurrency: UInt, progress: Progress) async throws -> [OCIManifestLayer]
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache?) async throws static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws
} }
+1 -1
View File
@@ -45,7 +45,7 @@ class DiskV1: Disk {
return pushedLayers return pushedLayers
} }
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil) async throws { static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false) async throws {
if !FileManager.default.createFile(atPath: diskURL.path, contents: nil) { if !FileManager.default.createFile(atPath: diskURL.path, contents: nil) {
throw OCIError.FailedToCreateVmFile throw OCIError.FailedToCreateVmFile
} }
+20 -11
View File
@@ -69,12 +69,12 @@ class DiskV2: Disk {
} }
} }
static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil) async throws { static func pull(registry: Registry, diskLayers: [OCIManifestLayer], diskURL: URL, concurrency: UInt, progress: Progress, localLayerCache: LocalLayerCache? = nil, deduplicate: Bool = false) async throws {
// Support resumable pulls // Support resumable pulls
let pullResumed = FileManager.default.fileExists(atPath: diskURL.path) let pullResumed = FileManager.default.fileExists(atPath: diskURL.path)
if !pullResumed { if !pullResumed {
if let localLayerCache = localLayerCache { if deduplicate, let localLayerCache = localLayerCache {
// Clone the local layer cache's disk and use it as a base, potentially // Clone the local layer cache's disk and use it as a base, potentially
// reducing the space usage since some blocks won't be written at all // reducing the space usage since some blocks won't be written at all
try FileManager.default.copyItem(at: localLayerCache.diskURL, to: diskURL) try FileManager.default.copyItem(at: localLayerCache.diskURL, to: diskURL)
@@ -151,26 +151,31 @@ class DiskV2: Disk {
// Also open the disk file for reading and verifying // Also open the disk file for reading and verifying
// its contents in case the local layer cache is used // its contents in case the local layer cache is used
let rdisk: FileHandle? = if localLayerCache != nil { let rdisk: FileHandle? = if deduplicate && localLayerCache != nil {
try FileHandle(forReadingFrom: diskURL) try FileHandle(forReadingFrom: diskURL)
} else { } else {
nil nil
} }
// Check if we already have this layer contents in the local layer cache // Check if we already have this layer contents in the local layer cache,
if let localLayerCache = localLayerCache, let localLayerInfo = localLayerCache.findInfo(digest: diskLayer.digest, offsetHint: diskWritingOffset) { // or perhaps even on the cloned disk (when the deduplication is enabled)
// indicates that the locally cloned disk image has the same content at the given offset if let localLayerCache = localLayerCache,
let localHit = localLayerInfo.uncompressedContentDigest == uncompressedLayerContentDigest let localLayerInfo = localLayerCache.findInfo(digest: diskLayer.digest, offsetHint: diskWritingOffset),
&& localLayerInfo.range.lowerBound == diskWritingOffset localLayerInfo.uncompressedContentDigest == uncompressedLayerContentDigest {
// doesn't seem that localHit can ever be false if the localLayerCache is not nil if deduplicate && localLayerInfo.range.lowerBound == diskWritingOffset {
// but let's just add extra safety here and check it // Do nothing, because the data is already on the disk that we've inherited from
if !localHit { } else {
// Fulfil the layer contents from the local blob cache // Fulfil the layer contents from the local blob cache
let data = localLayerCache.subdata(localLayerInfo.range) let data = localLayerCache.subdata(localLayerInfo.range)
_ = try zeroSkippingWrite(disk, rdisk, fsBlockSize, diskWritingOffset, data) _ = try zeroSkippingWrite(disk, rdisk, fsBlockSize, diskWritingOffset, data)
} }
try disk.close() try disk.close()
if let rdisk = rdisk {
try rdisk.close()
}
// Update the progress // Update the progress
progress.completedUnitCount += Int64(diskLayer.size) progress.completedUnitCount += Int64(diskLayer.size)
@@ -198,6 +203,10 @@ class DiskV2: Disk {
try filter.finalize() try filter.finalize()
try disk.close() try disk.close()
if let rdisk = rdisk {
try rdisk.close()
}
} }
globalDiskWritingOffset += uncompressedLayerSize globalDiskWritingOffset += uncompressedLayerSize
@@ -1,4 +1,4 @@
// Generated from java-escape by ANTLR 4.11.1 // Generated from Reference.g4 by ANTLR 4.13.2
import Antlr4 import Antlr4
@@ -1,4 +1,4 @@
// Generated from java-escape by ANTLR 4.11.1 // Generated from Reference.g4 by ANTLR 4.13.2
import Antlr4 import Antlr4
open class ReferenceLexer: Lexer { open class ReferenceLexer: Lexer {
@@ -49,7 +49,7 @@ open class ReferenceLexer: Lexer {
public public
required init(_ input: CharStream) { required init(_ input: CharStream) {
RuntimeMetaData.checkVersion("4.11.1", RuntimeMetaData.VERSION) RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION)
super.init(input) super.init(input)
_interp = LexerATNSimulator(self, ReferenceLexer._ATN, ReferenceLexer._decisionToDFA, ReferenceLexer._sharedContextCache) _interp = LexerATNSimulator(self, ReferenceLexer._ATN, ReferenceLexer._decisionToDFA, ReferenceLexer._sharedContextCache)
} }
@@ -1,4 +1,4 @@
// Generated from java-escape by ANTLR 4.11.1 // Generated from Reference.g4 by ANTLR 4.13.2
import Antlr4 import Antlr4
/** /**
@@ -1,4 +1,4 @@
// Generated from java-escape by ANTLR 4.11.1 // Generated from Reference.g4 by ANTLR 4.13.2
import Antlr4 import Antlr4
open class ReferenceParser: Parser { open class ReferenceParser: Parser {
@@ -41,7 +41,7 @@ open class ReferenceParser: Parser {
static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES) static let VOCABULARY = Vocabulary(_LITERAL_NAMES, _SYMBOLIC_NAMES)
override open override open
func getGrammarFileName() -> String { return "java-escape" } func getGrammarFileName() -> String { return "Reference.g4" }
override open override open
func getRuleNames() -> [String] { return ReferenceParser.ruleNames } func getRuleNames() -> [String] { return ReferenceParser.ruleNames }
@@ -60,7 +60,7 @@ open class ReferenceParser: Parser {
override public override public
init(_ input:TokenStream) throws { init(_ input:TokenStream) throws {
RuntimeMetaData.checkVersion("4.11.1", RuntimeMetaData.VERSION) RuntimeMetaData.checkVersion("4.13.2", RuntimeMetaData.VERSION)
try super.init(input) try super.init(input)
_interp = ParserATNSimulator(self,ReferenceParser._ATN,ReferenceParser._decisionToDFA, ReferenceParser._sharedContextCache) _interp = ParserATNSimulator(self,ReferenceParser._ATN,ReferenceParser._decisionToDFA, ReferenceParser._sharedContextCache)
} }
@@ -460,7 +460,7 @@ open class ReferenceParser: Parser {
setState(63) setState(63)
try _errHandler.sync(self) try _errHandler.sync(self)
_la = try _input.LA(1) _la = try _input.LA(1)
if ((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0) { if (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0)) {
setState(62) setState(62)
try separator() try separator()
@@ -611,7 +611,7 @@ open class ReferenceParser: Parser {
setState(84) setState(84)
try _errHandler.sync(self) try _errHandler.sync(self)
_la = try _input.LA(1) _la = try _input.LA(1)
while ((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0) { while (((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0)) {
setState(79) setState(79)
try separator() try separator()
setState(80) setState(80)
@@ -664,7 +664,7 @@ open class ReferenceParser: Parser {
try enterOuterAlt(_localctx, 1) try enterOuterAlt(_localctx, 1)
setState(87) setState(87)
_la = try _input.LA(1) _la = try _input.LA(1)
if (!((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0)) { if (!(((Int64(_la) & ~0x3f) == 0 && ((Int64(1) << _la) & 88) != 0))) {
try _errHandler.recoverInline(self) try _errHandler.recoverInline(self)
} }
else { else {
+7 -5
View File
@@ -327,12 +327,12 @@ class Registry {
request.httpBody = body request.httpBody = body
} }
var (channel, response) = try await authAwareRequest(request: request, viaFile: viaFile) var (channel, response) = try await authAwareRequest(request: request, viaFile: viaFile, doAuth: doAuth)
if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue { if doAuth && response.statusCode == HTTPCode.Unauthorized.rawValue {
_ = try await channel.asData() _ = try await channel.asData()
try await auth(response: response) try await auth(response: response)
(channel, response) = try await authAwareRequest(request: request, viaFile: viaFile) (channel, response) = try await authAwareRequest(request: request, viaFile: viaFile, doAuth: doAuth)
} }
return (channel, response) return (channel, response)
@@ -413,11 +413,13 @@ class Registry {
return nil return nil
} }
private func authAwareRequest(request: URLRequest, viaFile: Bool = false) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) { private func authAwareRequest(request: URLRequest, viaFile: Bool = false, doAuth: Bool) async throws -> (AsyncThrowingChannel<Data, Error>, HTTPURLResponse) {
var request = request var request = request
if let (name, value) = await authenticationKeeper.header() { if doAuth {
request.addValue(value, forHTTPHeaderField: name) if let (name, value) = await authenticationKeeper.header() {
request.addValue(value, forHTTPHeaderField: name)
}
} }
request.setValue("Tart/\(CI.version) (\(DeviceInfo.os); \(DeviceInfo.model))", request.setValue("Tart/\(CI.version) (\(DeviceInfo.os); \(DeviceInfo.model))",
+1 -1
View File
@@ -11,7 +11,7 @@ class PIDLock {
if fd == -1 { if fd == -1 {
let details = Errno(rawValue: CInt(errno)) let details = Errno(rawValue: CInt(errno))
throw RuntimeError.PIDLockFailed("failed to open lock file \(url): \(details)") throw RuntimeError.PIDLockMissing("failed to open lock file \(url): \(details)")
} }
} }
+8 -4
View File
@@ -317,15 +317,19 @@ class VM: NSObject, VZVirtualMachineDelegate, ObservableObject {
// Audio // Audio
let soundDeviceConfiguration = VZVirtioSoundDeviceConfiguration() let soundDeviceConfiguration = VZVirtioSoundDeviceConfiguration()
let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration()
let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration()
if audio && !suspendable { if audio && !suspendable {
let inputAudioStreamConfiguration = VZVirtioSoundDeviceInputStreamConfiguration()
let outputAudioStreamConfiguration = VZVirtioSoundDeviceOutputStreamConfiguration()
inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource() inputAudioStreamConfiguration.source = VZHostAudioInputStreamSource()
outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink() outputAudioStreamConfiguration.sink = VZHostAudioOutputStreamSink()
soundDeviceConfiguration.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration]
} else {
// just a null speaker
soundDeviceConfiguration.streams = [VZVirtioSoundDeviceOutputStreamConfiguration()]
} }
soundDeviceConfiguration.streams = [inputAudioStreamConfiguration, outputAudioStreamConfiguration]
configuration.audioDevices = [soundDeviceConfiguration] configuration.audioDevices = [soundDeviceConfiguration]
// Keyboard and mouse // Keyboard and mouse
+4 -3
View File
@@ -11,7 +11,7 @@ enum OCIError: Error {
} }
extension VMDirectory { extension VMDirectory {
func pullFromRegistry(registry: Registry, manifest: OCIManifest, concurrency: UInt, localLayerCache: LocalLayerCache?) async throws { func pullFromRegistry(registry: Registry, manifest: OCIManifest, concurrency: UInt, localLayerCache: LocalLayerCache?, deduplicate: Bool) async throws {
// Pull VM's config file layer and re-serialize it into a config file // Pull VM's config file layer and re-serialize it into a config file
let configLayers = manifest.layers.filter { let configLayers = manifest.layers.filter {
$0.mediaType == configMediaType $0.mediaType == configMediaType
@@ -54,12 +54,13 @@ extension VMDirectory {
do { do {
try await diskImplType.pull(registry: registry, diskLayers: layers, diskURL: diskURL, try await diskImplType.pull(registry: registry, diskLayers: layers, diskURL: diskURL,
concurrency: concurrency, progress: progress, concurrency: concurrency, progress: progress,
localLayerCache: localLayerCache) localLayerCache: localLayerCache,
deduplicate: deduplicate)
} catch let error where error is FilterError { } catch let error where error is FilterError {
throw RuntimeError.PullFailed("failed to decompress disk: \(error.localizedDescription)") throw RuntimeError.PullFailed("failed to decompress disk: \(error.localizedDescription)")
} }
if let llc = localLayerCache { if deduplicate, let llc = localLayerCache {
// set custom attribute to remember deduplicated bytes // set custom attribute to remember deduplicated bytes
diskURL.setDeduplicatedBytes(llc.deduplicatedBytes) diskURL.setDeduplicatedBytes(llc.deduplicatedBytes)
} }
+5
View File
@@ -24,6 +24,8 @@ class VMStorageHelper {
private static func missingVMWrap<R: Any>(_ name: String, closure: () throws -> R) throws -> R { private static func missingVMWrap<R: Any>(_ name: String, closure: () throws -> R) throws -> R {
do { do {
return try closure() return try closure()
} catch RuntimeError.PIDLockMissing {
throw RuntimeError.VMDoesNotExist(name: name)
} catch { } catch {
if error.isFileNotFound() { if error.isFileNotFound() {
throw RuntimeError.VMDoesNotExist(name: name) throw RuntimeError.VMDoesNotExist(name: name)
@@ -59,6 +61,7 @@ enum RuntimeError : Error {
case InvalidDiskSize(_ message: String) case InvalidDiskSize(_ message: String)
case FailedToUpdateAccessDate(_ message: String) case FailedToUpdateAccessDate(_ message: String)
case PIDLockFailed(_ message: String) case PIDLockFailed(_ message: String)
case PIDLockMissing(_ message: String)
case FailedToParseRemoteName(_ message: String) case FailedToParseRemoteName(_ message: String)
case VMTerminationFailed(_ message: String) case VMTerminationFailed(_ message: String)
case ImproperlyFormattedHost(_ host: String, _ hint: String) case ImproperlyFormattedHost(_ host: String, _ hint: String)
@@ -105,6 +108,8 @@ extension RuntimeError : CustomStringConvertible {
return message return message
case .PIDLockFailed(let message): case .PIDLockFailed(let message):
return message return message
case .PIDLockMissing(let message):
return message
case .FailedToParseRemoteName(let cause): case .FailedToParseRemoteName(let cause):
return "failed to parse remote name: \(cause)" return "failed to parse remote name: \(cause)"
case .VMTerminationFailed(let message): case .VMTerminationFailed(let message):
+7 -3
View File
@@ -140,7 +140,7 @@ class VMStorageOCI: PrunableStorage {
try list().filter { (_, _, isSymlink) in !isSymlink }.map { (_, vmDir, _) in vmDir } try list().filter { (_, _, isSymlink) in !isSymlink }.map { (_, vmDir, _) in vmDir }
} }
func pull(_ name: RemoteName, registry: Registry, concurrency: UInt) async throws { func pull(_ name: RemoteName, registry: Registry, concurrency: UInt, deduplicate: Bool) async throws {
SentrySDK.configureScope { scope in SentrySDK.configureScope { scope in
scope.setContext(value: ["imageName": name.description], key: "OCI") scope.setContext(value: ["imageName": name.description], key: "OCI")
} }
@@ -203,10 +203,14 @@ class VMStorageOCI: PrunableStorage {
if let llc = localLayerCache { if let llc = localLayerCache {
let deduplicatedHuman = ByteCountFormatter.string(fromByteCount: Int64(llc.deduplicatedBytes), countStyle: .file) let deduplicatedHuman = ByteCountFormatter.string(fromByteCount: Int64(llc.deduplicatedBytes), countStyle: .file)
defaultLogger.appendNewLine("found an image \(llc.name) that will allow us to deduplicate \(deduplicatedHuman), using it as a base...") if deduplicate {
defaultLogger.appendNewLine("found an image \(llc.name) that will allow us to deduplicate \(deduplicatedHuman), using it as a base...")
} else {
defaultLogger.appendNewLine("found an image \(llc.name) that will allow us to avoid fetching \(deduplicatedHuman), will try use it...")
}
} }
try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache) try await tmpVMDir.pullFromRegistry(registry: registry, manifest: manifest, concurrency: concurrency, localLayerCache: localLayerCache, deduplicate: deduplicate)
} recoverFromFailure: { error in } recoverFromFailure: { error in
if error is Retryable { if error is Retryable {
print("Error: \(error.localizedDescription)") print("Error: \(error.localizedDescription)")
+18
View File
@@ -90,3 +90,21 @@ Instead of Anka Registry, Tart can work with any OCI-compatible container regist
and scalable experience for distributing virtual machines. and scalable experience for distributing virtual machines.
Tart doesn't yet have an analogue of Anka Controller for managing long living VMs but [soon will be](https://github.com/cirruslabs/tart/issues/372). Tart doesn't yet have an analogue of Anka Controller for managing long living VMs but [soon will be](https://github.com/cirruslabs/tart/issues/372).
## Automatic pruning
`tart pull` and `tart clone` commands check the remaining space available on the volume associated with `TART_HOME` directory (defaults to `~/.tart`) before pulling or cloning anything.
In case there's not enough space to fit the newly pulled or cloned VM image, Tart will remove the least recently accessed VMs from OCI cache and `.ipsw` files from IPSW cache until enough free space is available.
To disable this functionality, set the `TART_NO_AUTO_PRUNE` environment variable either globally:
```shell
export TART_NO_AUTO_PRUNE=
```
...or per `tart pull` and `tart clone` invocation as follows:
```shell
TART_NO_AUTO_PRUNE= tart pull ...
```