mirror of https://github.com/cirruslabs/tart.git
Introduce "tart stop" (#316)
This commit is contained in:
parent
f37372da28
commit
5e77968989
|
|
@ -108,6 +108,24 @@ struct Run: AsyncParsableCommand {
|
|||
}
|
||||
}()
|
||||
|
||||
// Lock the VM
|
||||
//
|
||||
// More specifically, lock the "config.json", because we can't lock
|
||||
// directories with fcntl(2)-based locking and we better not interfere
|
||||
// with the VM's disk and NVRAM, because they are opened (and even seem
|
||||
// to be locked) directly by the Virtualization.Framework's process.
|
||||
//
|
||||
// Note that due to "completely stupid semantics"[1] of the fcntl-based
|
||||
// file locking, we need to acquire the lock after we read the VM's
|
||||
// configuration file, otherwise we will loose the lock.
|
||||
//
|
||||
// [1]: https://man.openbsd.org/fcntl
|
||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
||||
if try !lock.trylock() {
|
||||
print("Virtual machine \"\(name)\" is already running!")
|
||||
Foundation.exit(2)
|
||||
}
|
||||
|
||||
let task = Task {
|
||||
do {
|
||||
if let vncImpl = vncImpl {
|
||||
|
|
@ -129,11 +147,6 @@ struct Run: AsyncParsableCommand {
|
|||
|
||||
Foundation.exit(0)
|
||||
} catch {
|
||||
if error.localizedDescription.contains("Failed to lock auxiliary storage.") {
|
||||
print("Virtual machine \"\(name)\" is already running!")
|
||||
Foundation.exit(2)
|
||||
}
|
||||
|
||||
print(error)
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import System
|
||||
import SwiftDate
|
||||
|
||||
struct Stop: AsyncParsableCommand {
|
||||
static var configuration = CommandConfiguration(commandName: "stop", abstract: "Stop a VM")
|
||||
|
||||
@Argument(help: "VM name")
|
||||
var name: String
|
||||
|
||||
@Option(name: [.short, .long], help: "Seconds to wait for graceful termination before forcefully terminating the VM")
|
||||
var timeout: UInt64 = 30
|
||||
|
||||
func run() async throws {
|
||||
do {
|
||||
let vmDir = try VMStorageLocal().open(name)
|
||||
let lock = try PIDLock(lockURL: vmDir.configURL)
|
||||
|
||||
// Find the VM's PID
|
||||
var pid = try lock.pid()
|
||||
if pid == 0 {
|
||||
print("VM \(name) is not running")
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
||||
// Try to gracefully terminate the VM
|
||||
//
|
||||
// Note that we don't check the return code here
|
||||
// to provide a clean exit from "tart stop" in cases
|
||||
// when the VM is already shutting down and we hit
|
||||
// a race condition.
|
||||
//
|
||||
// We check the return code in the kill(2) below, though,
|
||||
// because it's a less common scenario and it would be
|
||||
// nice to know for the user that we've tried all methods
|
||||
// and failed to shutdown the VM.
|
||||
kill(pid, SIGINT)
|
||||
|
||||
// Ensure that the VM has terminated
|
||||
var gracefulWaitDuration = Measurement(value: Double(timeout), unit: UnitDuration.seconds)
|
||||
let gracefulTickDuration = Measurement(value: Double(100), unit: UnitDuration.milliseconds)
|
||||
|
||||
while gracefulWaitDuration.value > 0 {
|
||||
pid = try lock.pid()
|
||||
if pid == 0 {
|
||||
Foundation.exit(0)
|
||||
}
|
||||
|
||||
try await Task.sleep(nanoseconds: UInt64(gracefulTickDuration.converted(to: .nanoseconds).value))
|
||||
gracefulWaitDuration = gracefulWaitDuration - gracefulTickDuration
|
||||
}
|
||||
|
||||
// Seems that VM is still running, proceed with forceful termination
|
||||
let ret = kill(pid, SIGKILL)
|
||||
if ret != 0 {
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
print("failed to forcefully terminate the VM \(name): \(details)")
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
|
||||
Foundation.exit(0)
|
||||
} catch {
|
||||
print(error)
|
||||
|
||||
Foundation.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import Foundation
|
||||
import System
|
||||
|
||||
class PIDLock {
|
||||
let url: URL
|
||||
let fd: Int32
|
||||
|
||||
init(lockURL: URL) throws {
|
||||
url = lockURL
|
||||
fd = open(lockURL.path, O_RDWR)
|
||||
}
|
||||
|
||||
deinit {
|
||||
close(fd)
|
||||
}
|
||||
|
||||
func trylock() throws -> Bool {
|
||||
let (locked, _) = try lockWrapper(F_SETLK, F_WRLCK, "failed to lock \(url)")
|
||||
return locked
|
||||
}
|
||||
|
||||
func lock() throws {
|
||||
_ = try lockWrapper(F_SETLKW, F_WRLCK, "failed to lock \(url)")
|
||||
}
|
||||
|
||||
func unlock() throws {
|
||||
_ = try lockWrapper(F_SETLK, F_UNLCK, "failed to unlock \(url)")
|
||||
}
|
||||
|
||||
func pid() throws -> pid_t {
|
||||
let (_, result) = try lockWrapper(F_GETLK, F_RDLCK, "failed to get lock \(url) status")
|
||||
|
||||
return result.l_pid
|
||||
}
|
||||
|
||||
func lockWrapper(_ operation: Int32, _ type: Int32, _ message: String) throws -> (Bool, flock) {
|
||||
var result = flock(l_start: 0, l_len: 0, l_pid: 0, l_type: Int16(type), l_whence: Int16(SEEK_SET))
|
||||
|
||||
let ret = fcntl(fd, operation, &result)
|
||||
if ret != 0 {
|
||||
if operation == F_SETLK && errno == EAGAIN {
|
||||
return (false, result)
|
||||
}
|
||||
|
||||
let details = Errno(rawValue: CInt(errno))
|
||||
|
||||
throw RuntimeError("\(message): \(details)")
|
||||
}
|
||||
|
||||
return (true, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ struct Root: AsyncParsableCommand {
|
|||
Push.self,
|
||||
Prune.self,
|
||||
Rename.self,
|
||||
Stop.self,
|
||||
Delete.self,
|
||||
])
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue