mirror of https://github.com/cirruslabs/tart.git
Add NoGraphics field to tart get command
This commit is contained in:
parent
e3ee2da2fd
commit
db8daa7b3d
|
|
@ -22,3 +22,6 @@ dist/
|
|||
|
||||
# mkdocs-material
|
||||
site
|
||||
|
||||
# Python cache files
|
||||
*.pyc
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
import CoreGraphics
|
||||
import Darwin
|
||||
|
||||
fileprivate struct VMInfo: Encodable {
|
||||
let OS: OS
|
||||
|
|
@ -11,6 +13,29 @@ fileprivate struct VMInfo: Encodable {
|
|||
let Display: String
|
||||
let Running: Bool
|
||||
let State: String
|
||||
let NoGraphics: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case OS, CPU, Memory, Disk, DiskFormat, Size, Display, Running, State, NoGraphics
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(OS, forKey: .OS)
|
||||
try container.encode(CPU, forKey: .CPU)
|
||||
try container.encode(Memory, forKey: .Memory)
|
||||
try container.encode(Disk, forKey: .Disk)
|
||||
try container.encode(DiskFormat, forKey: .DiskFormat)
|
||||
try container.encode(Size, forKey: .Size)
|
||||
try container.encode(Display, forKey: .Display)
|
||||
try container.encode(Running, forKey: .Running)
|
||||
try container.encode(State, forKey: .State)
|
||||
if let noGraphics = NoGraphics {
|
||||
try container.encode(noGraphics, forKey: .NoGraphics)
|
||||
} else {
|
||||
try container.encodeNil(forKey: .NoGraphics)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Get: AsyncParsableCommand {
|
||||
|
|
@ -27,7 +52,83 @@ struct Get: AsyncParsableCommand {
|
|||
let vmConfig = try VMConfig(fromURL: vmDir.configURL)
|
||||
let memorySizeInMb = vmConfig.memorySize / 1024 / 1024
|
||||
|
||||
let info = VMInfo(OS: vmConfig.os, CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), DiskFormat: vmConfig.diskFormat.rawValue, Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000), Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state().rawValue)
|
||||
// Check if VM is running without graphics (no windows)
|
||||
var noGraphics: Bool? = nil
|
||||
if try vmDir.running() {
|
||||
let lock = try vmDir.lock()
|
||||
let pid = try lock.pid()
|
||||
if pid > 0 {
|
||||
noGraphics = try hasNoWindows(pid: pid)
|
||||
}
|
||||
}
|
||||
|
||||
let info = VMInfo(OS: vmConfig.os, CPU: vmConfig.cpuCount, Memory: memorySizeInMb, Disk: try vmDir.sizeGB(), DiskFormat: vmConfig.diskFormat.rawValue, Size: String(format: "%.3f", Float(try vmDir.allocatedSizeBytes()) / 1000 / 1000 / 1000), Display: vmConfig.display.description, Running: try vmDir.running(), State: try vmDir.state().rawValue, NoGraphics: noGraphics)
|
||||
print(format.renderSingle(info))
|
||||
}
|
||||
|
||||
private func hasNoWindows(pid: pid_t) throws -> Bool {
|
||||
// Check if the process and its children have any windows using Core Graphics Window Server
|
||||
// This is more reliable than checking command-line arguments since there are
|
||||
// multiple ways a VM might run without graphics (--no-graphics flag, CI environment, etc.)
|
||||
|
||||
// Get all PIDs to check (parent + children)
|
||||
var pidsToCheck = [pid]
|
||||
pidsToCheck.append(contentsOf: try getChildProcesses(of: pid))
|
||||
|
||||
// Get all window information from the window server
|
||||
guard let windowList = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID) as? [[String: Any]] else {
|
||||
// If we can't get window info, assume no graphics
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any window belongs to our process or its children
|
||||
for windowInfo in windowList {
|
||||
if let windowPID = windowInfo[kCGWindowOwnerPID as String] as? Int32,
|
||||
pidsToCheck.contains(windowPID) {
|
||||
// Found a window for this process tree, so it has graphics
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// No windows found for this process tree
|
||||
return true
|
||||
}
|
||||
|
||||
private func getChildProcesses(of parentPID: pid_t) throws -> [pid_t] {
|
||||
var children: [pid_t] = []
|
||||
|
||||
// Use sysctl to get process information
|
||||
var mib: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0]
|
||||
var size: size_t = 0
|
||||
|
||||
// Get size needed
|
||||
if sysctl(&mib, 4, nil, &size, nil, 0) != 0 {
|
||||
// If we can't get process list, return empty array
|
||||
return children
|
||||
}
|
||||
|
||||
// Allocate memory and get process list
|
||||
let count = size / MemoryLayout<kinfo_proc>.size
|
||||
var procs = Array<kinfo_proc>(repeating: kinfo_proc(), count: count)
|
||||
|
||||
if sysctl(&mib, 4, &procs, &size, nil, 0) != 0 {
|
||||
// If we can't get process list, return empty array
|
||||
return children
|
||||
}
|
||||
|
||||
// Find direct children of the given parent PID
|
||||
for proc in procs {
|
||||
let ppid = proc.kp_eproc.e_ppid
|
||||
let pid = proc.kp_proc.p_pid
|
||||
if ppid == parentPID && pid > 0 {
|
||||
children.append(pid)
|
||||
// Recursively get children of children
|
||||
if let grandchildren = try? getChildProcesses(of: pid) {
|
||||
children.append(contentsOf: grandchildren)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def check_json_format(tart, vm_name, running, noGraphics, expected):
|
||||
stdout, _ = tart.run(["get", vm_name, "--format", "json"])
|
||||
vm_info = json.loads(stdout)
|
||||
actual_running = vm_info["Running"]
|
||||
assert actual_running is running, f"Running is {actual_running}, expected {running}"
|
||||
assert vm_info.get("NoGraphics") is noGraphics, expected
|
||||
|
||||
def check_text_format(tart, vm_name, running, noGraphics, expected):
|
||||
stdout, _ = tart.run(["get", vm_name, "--format", "text"])
|
||||
assert "NoGraphics" in stdout, "NoGraphics field should be present in text output"
|
||||
|
||||
# Text format is tab-separated with headers in first line
|
||||
lines = stdout.strip().split('\n')
|
||||
if len(lines) >= 2:
|
||||
headers = lines[0].split()
|
||||
values = lines[1].split()
|
||||
info_dict = dict(zip(headers, values))
|
||||
else:
|
||||
info_dict = {}
|
||||
|
||||
# Convert "stopped" to false for Running field
|
||||
actual_running = info_dict.get("State") != "stopped"
|
||||
assert actual_running == running, f"Expected Running={running}, got State={actual_running}"
|
||||
assert info_dict.get("NoGraphics") == noGraphics, expected
|
||||
|
||||
@pytest.mark.skipif(os.environ.get("CI") == "true", reason="Normal graphics mode doesn't work in CI")
|
||||
def test_no_graphics_normal(tart):
|
||||
_test_no_graphics_impl(tart, [], False)
|
||||
|
||||
def test_no_graphics_disabled(tart):
|
||||
_test_no_graphics_impl(tart, ["--no-graphics"], True)
|
||||
|
||||
def _test_no_graphics_impl(tart, graphics_mode, expected_no_graphics):
|
||||
# Create a test VM (use Linux VM for faster tests)
|
||||
vm_name = f"integration-test-no-graphics-{uuid.uuid4()}"
|
||||
tart.run(["pull", "ghcr.io/cirruslabs/debian:latest"])
|
||||
tart.run(["clone", "ghcr.io/cirruslabs/debian:latest", vm_name])
|
||||
|
||||
# Test 1: VM not running - NoGraphics should be None in json format
|
||||
check_json_format(tart, vm_name, False, None, "NoGraphics should be None when VM is not running")
|
||||
|
||||
# Test 2: VM not running - NoGraphics should be NULL in text format
|
||||
check_text_format(tart, vm_name, False, "NULL", "NoGraphics should be NULL when VM is not running")
|
||||
|
||||
# Run VM with specified graphics mode
|
||||
tart_run_process = tart.run_async(["run"] + graphics_mode + [vm_name])
|
||||
time.sleep(3) # Give VM time to start
|
||||
|
||||
# Test 3: VM running - NoGraphics should be XX in json format
|
||||
check_json_format(tart, vm_name, True, expected_no_graphics, f"NoGraphics should be {expected_no_graphics} (JSON) when VM is running")
|
||||
|
||||
# Test 4: VM running - NoGraphics should be XX in text format
|
||||
check_text_format(tart, vm_name, True, str(expected_no_graphics).lower(), f"NoGraphics should be {expected_no_graphics} (TEXT) when VM is running")
|
||||
Loading…
Reference in New Issue