mirror of
https://github.com/trycua/computer.git
synced 2026-02-18 04:19:38 -06:00
Expose VNC port
This commit is contained in:
@@ -68,6 +68,9 @@ Command Options:
|
||||
--no-display Do not start the VNC client app
|
||||
--shared-dir <dir> Share directory with VM (format: path[:ro|rw])
|
||||
--mount <path> For Linux VMs only, attach a read-only disk image
|
||||
--registry <url> Container registry URL (default: ghcr.io)
|
||||
--organization <org> Organization to pull from (default: trycua)
|
||||
--vnc-port <port> Port to use for the VNC server (default: 0 for auto-assign)
|
||||
|
||||
set:
|
||||
--cpu <cores> New number of CPU cores (e.g., 4)
|
||||
|
||||
@@ -25,6 +25,9 @@ struct Run: AsyncParsableCommand {
|
||||
@Option(help: "Organization to pull the images from. Defaults to trycua")
|
||||
var organization: String = "trycua"
|
||||
|
||||
@Option(name: [.customLong("vnc-port")], help: "Port to use for the VNC server. Defaults to 0 (auto-assign)")
|
||||
var vncPort: Int = 0
|
||||
|
||||
private var parsedSharedDirectories: [SharedDirectory] {
|
||||
get throws {
|
||||
try sharedDirectories.map { dirString -> SharedDirectory in
|
||||
@@ -75,7 +78,8 @@ struct Run: AsyncParsableCommand {
|
||||
sharedDirectories: dirs,
|
||||
mount: mount,
|
||||
registry: registry,
|
||||
organization: organization
|
||||
organization: organization,
|
||||
vncPort: vncPort
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -125,6 +125,7 @@ enum VMError: Error, LocalizedError {
|
||||
case stopTimeout(String)
|
||||
case resizeTooSmall(current: UInt64, requested: UInt64)
|
||||
case vncNotConfigured
|
||||
case vncPortBindingFailed(requested: Int, actual: Int)
|
||||
case internalError(String)
|
||||
case unsupportedOS(String)
|
||||
case invalidDisplayResolution(String)
|
||||
@@ -148,6 +149,11 @@ enum VMError: Error, LocalizedError {
|
||||
return "Cannot resize disk to \(requested) bytes, current size is \(current) bytes"
|
||||
case .vncNotConfigured:
|
||||
return "VNC is not configured for this virtual machine"
|
||||
case .vncPortBindingFailed(let requested, let actual):
|
||||
if actual == -1 {
|
||||
return "Could not bind to VNC port \(requested) (port already in use). Try a different port or use port 0 for auto-assign."
|
||||
}
|
||||
return "Could not bind to VNC port \(requested) (port already in use). System assigned port \(actual) instead. Try a different port or use port 0 for auto-assign."
|
||||
case .internalError(let message):
|
||||
return "Internal error: \(message)"
|
||||
case .unsupportedOS(let os):
|
||||
|
||||
@@ -25,7 +25,6 @@ struct VMConfig: Codable {
|
||||
private var _display: VMDisplayResolution
|
||||
private var _hardwareModel: Data?
|
||||
private var _machineIdentifier: Data?
|
||||
private var _vncPort: Int?
|
||||
|
||||
// MARK: - Initialization
|
||||
init(
|
||||
@@ -36,8 +35,7 @@ struct VMConfig: Codable {
|
||||
macAddress: String? = nil,
|
||||
display: String,
|
||||
hardwareModel: Data? = nil,
|
||||
machineIdentifier: Data? = nil,
|
||||
vncPort: Int? = nil
|
||||
machineIdentifier: Data? = nil
|
||||
) throws {
|
||||
self.os = os
|
||||
self._cpuCount = cpuCount
|
||||
@@ -47,7 +45,6 @@ struct VMConfig: Codable {
|
||||
self._display = VMDisplayResolution(string: display) ?? VMDisplayResolution(string: "1024x768")!
|
||||
self._hardwareModel = hardwareModel
|
||||
self._machineIdentifier = machineIdentifier
|
||||
self._vncPort = vncPort
|
||||
}
|
||||
|
||||
var display: VMDisplayResolution {
|
||||
@@ -84,11 +81,6 @@ struct VMConfig: Codable {
|
||||
get { _macAddress }
|
||||
set { _macAddress = newValue }
|
||||
}
|
||||
|
||||
var vncPort: Int? {
|
||||
get { _vncPort }
|
||||
set { _vncPort = newValue }
|
||||
}
|
||||
|
||||
mutating func setCpuCount(_ count: Int) {
|
||||
_cpuCount = count
|
||||
@@ -118,10 +110,6 @@ struct VMConfig: Codable {
|
||||
self._display = newDisplay
|
||||
}
|
||||
|
||||
mutating func setVNCPort(_ port: Int) {
|
||||
_vncPort = port
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case _cpuCount = "cpuCount"
|
||||
@@ -132,7 +120,6 @@ struct VMConfig: Codable {
|
||||
case _hardwareModel = "hardwareModel"
|
||||
case _machineIdentifier = "machineIdentifier"
|
||||
case os
|
||||
case _vncPort = "vncPort"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
@@ -146,7 +133,6 @@ struct VMConfig: Codable {
|
||||
_display = VMDisplayResolution(string: try container.decode(String.self, forKey: .display))!
|
||||
_hardwareModel = try container.decodeIfPresent(Data.self, forKey: ._hardwareModel)
|
||||
_machineIdentifier = try container.decodeIfPresent(Data.self, forKey: ._machineIdentifier)
|
||||
_vncPort = try container.decodeIfPresent(Int.self, forKey: ._vncPort)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
@@ -160,6 +146,5 @@ struct VMConfig: Codable {
|
||||
try container.encode(display.string, forKey: .display)
|
||||
try container.encodeIfPresent(_hardwareModel, forKey: ._hardwareModel)
|
||||
try container.encodeIfPresent(_machineIdentifier, forKey: ._machineIdentifier)
|
||||
try container.encodeIfPresent(_vncPort, forKey: ._vncPort)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,7 +255,8 @@ final class LumeController {
|
||||
sharedDirectories: [SharedDirectory] = [],
|
||||
mount: Path? = nil,
|
||||
registry: String = "ghcr.io",
|
||||
organization: String = "trycua"
|
||||
organization: String = "trycua",
|
||||
vncPort: Int = 0
|
||||
) async throws {
|
||||
let normalizedName = normalizeVMName(name: name)
|
||||
Logger.info(
|
||||
@@ -265,6 +266,7 @@ final class LumeController {
|
||||
"no_display": "\(noDisplay)",
|
||||
"shared_directories": "\(sharedDirectories.map( { $0.string } ).joined(separator: ", "))",
|
||||
"mount": mount?.path ?? "none",
|
||||
"vnc_port": "\(vncPort)",
|
||||
])
|
||||
|
||||
do {
|
||||
@@ -284,7 +286,7 @@ final class LumeController {
|
||||
|
||||
let vm = try get(name: normalizedName)
|
||||
SharedVM.shared.setVM(name: normalizedName, vm: vm)
|
||||
try await vm.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount)
|
||||
try await vm.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort)
|
||||
Logger.info("VM started successfully", metadata: ["name": normalizedName])
|
||||
} catch {
|
||||
SharedVM.shared.removeVM(name: normalizedName)
|
||||
|
||||
@@ -88,7 +88,7 @@ class VM {
|
||||
|
||||
// MARK: - VM Lifecycle Management
|
||||
|
||||
func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?) async throws {
|
||||
func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0) async throws {
|
||||
guard vmDirContext.initialized else {
|
||||
throw VMError.notInitialized(vmDirContext.name)
|
||||
}
|
||||
@@ -111,7 +111,8 @@ class VM {
|
||||
"diskSize": "\(vmDirContext.config.diskSize ?? 0)",
|
||||
"sharedDirectories": sharedDirectories.map(
|
||||
{ $0.string }
|
||||
).joined(separator: ", ")
|
||||
).joined(separator: ", "),
|
||||
"vncPort": "\(vncPort)"
|
||||
])
|
||||
|
||||
// Create and configure the VM
|
||||
@@ -125,7 +126,7 @@ class VM {
|
||||
)
|
||||
virtualizationService = try virtualizationServiceFactory(config)
|
||||
|
||||
let vncInfo = try await setupVNC(noDisplay: noDisplay)
|
||||
let vncInfo = try await setupVNC(noDisplay: noDisplay, port: vncPort)
|
||||
Logger.info("VNC info", metadata: ["vncInfo": vncInfo])
|
||||
|
||||
// Start the VM
|
||||
@@ -337,13 +338,11 @@ class VM {
|
||||
return vncService.url
|
||||
}
|
||||
|
||||
private func setupVNC(noDisplay: Bool) async throws -> String {
|
||||
private func setupVNC(noDisplay: Bool, port: Int = 0) async throws -> String {
|
||||
guard let service = virtualizationService else {
|
||||
throw VMError.internalError("Virtualization service not initialized")
|
||||
}
|
||||
|
||||
// Use configured port or default to 0 (auto-assign)
|
||||
let port = vmDirContext.config.vncPort ?? 0
|
||||
try await vncService.start(port: port, virtualMachine: service.getVirtualMachine())
|
||||
|
||||
guard let url = vncService.url else {
|
||||
@@ -401,13 +400,4 @@ class VM {
|
||||
func finalize(to name: String, home: Home) throws {
|
||||
try vmDirContext.finalize(to: name)
|
||||
}
|
||||
|
||||
// Add method to set VNC port
|
||||
func setVNCPort(_ port: Int) throws {
|
||||
guard !isRunning else {
|
||||
throw VMError.alreadyRunning(vmDirContext.name)
|
||||
}
|
||||
vmDirContext.config.setVNCPort(port)
|
||||
try vmDirContext.saveConfig()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
final class PassphraseGenerator {
|
||||
private let words: [String]
|
||||
@@ -9,11 +10,39 @@ final class PassphraseGenerator {
|
||||
|
||||
func prefix(_ count: Int) -> [String] {
|
||||
guard count > 0 else { return [] }
|
||||
return (0..<count).map { _ in words.randomElement() ?? words[0] }
|
||||
|
||||
// Use secure random number generation
|
||||
var result: [String] = []
|
||||
for _ in 0..<count {
|
||||
let randomBytes = (0..<4).map { _ in UInt8.random(in: 0...255) }
|
||||
let randomNumber = Data(randomBytes).withUnsafeBytes { bytes in
|
||||
bytes.load(as: UInt32.self)
|
||||
}
|
||||
let index = Int(randomNumber % UInt32(words.count))
|
||||
result.append(words[index])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// A much larger set of common, easy-to-type words
|
||||
private static let defaultWords = [
|
||||
"apple", "banana", "cherry", "date",
|
||||
"elder", "fig", "grape", "honey"
|
||||
"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf", "hotel",
|
||||
"india", "juliet", "kilo", "lima", "mike", "november", "oscar", "papa",
|
||||
"quebec", "romeo", "sierra", "tango", "uniform", "victor", "whiskey", "xray",
|
||||
"yankee", "zulu", "zero", "one", "two", "three", "four", "five",
|
||||
"six", "seven", "eight", "nine", "apple", "banana", "cherry", "date",
|
||||
"elder", "fig", "grape", "honey", "iris", "jade", "kiwi", "lemon",
|
||||
"mango", "nectarine", "orange", "peach", "quince", "raspberry", "strawberry", "tangerine",
|
||||
"red", "blue", "green", "yellow", "purple", "orange", "pink", "brown",
|
||||
"black", "white", "gray", "silver", "gold", "copper", "bronze", "steel",
|
||||
"north", "south", "east", "west", "spring", "summer", "autumn", "winter",
|
||||
"river", "ocean", "mountain", "valley", "forest", "desert", "island", "beach",
|
||||
"sun", "moon", "star", "cloud", "rain", "snow", "wind", "storm",
|
||||
"happy", "brave", "calm", "swift", "wise", "kind", "bold", "free",
|
||||
"safe", "strong", "bright", "clear", "light", "soft", "warm", "cool",
|
||||
"eagle", "falcon", "hawk", "owl", "robin", "sparrow", "swan", "dove",
|
||||
"tiger", "lion", "bear", "wolf", "deer", "horse", "dolphin", "whale",
|
||||
"maple", "oak", "pine", "birch", "cedar", "fir", "palm", "willow",
|
||||
"rose", "lily", "daisy", "tulip", "lotus", "orchid", "violet", "jasmine"
|
||||
]
|
||||
}
|
||||
@@ -42,25 +42,45 @@ final class DefaultVNCService: VNCService {
|
||||
|
||||
vncServer = server
|
||||
|
||||
// Wait for port to be assigned if using port 0 (auto-assign)
|
||||
while port == 0 {
|
||||
if let assignedPort: UInt16 = server.port.asUInt16, assignedPort != 0 {
|
||||
// Get the local IP address for the URL - prefer IPv4
|
||||
let hostIP = try getLocalIPAddress() ?? "127.0.0.1"
|
||||
let url = "vnc://:\(password)@127.0.0.1:\(assignedPort)" // Use localhost for local connections
|
||||
let externalUrl = "vnc://:\(password)@\(hostIP):\(assignedPort)" // External URL for remote connections
|
||||
|
||||
Logger.info("VNC server started", metadata: [
|
||||
"local": url,
|
||||
"external": externalUrl
|
||||
])
|
||||
|
||||
// Save session information with local URL for the client
|
||||
let session = VNCSession(url: url)
|
||||
try vmDirectory.saveSession(session)
|
||||
break
|
||||
// Wait for port to be assigned (both for auto-assign and specific port)
|
||||
var attempts = 0
|
||||
let maxAttempts = 20 // 1 second total wait time
|
||||
while true {
|
||||
if let assignedPort: UInt16 = server.port.asUInt16 {
|
||||
// If we got a non-zero port, check if it matches our request
|
||||
if assignedPort != 0 {
|
||||
// For specific port requests, verify we got the requested port
|
||||
if port != 0 && Int(assignedPort) != port {
|
||||
throw VMError.vncPortBindingFailed(requested: port, actual: Int(assignedPort))
|
||||
}
|
||||
|
||||
// Get the local IP address for the URL - prefer IPv4
|
||||
let hostIP = try getLocalIPAddress() ?? "127.0.0.1"
|
||||
let url = "vnc://:\(password)@127.0.0.1:\(assignedPort)" // Use localhost for local connections
|
||||
let externalUrl = "vnc://:\(password)@\(hostIP):\(assignedPort)" // External URL for remote connections
|
||||
|
||||
Logger.info("VNC server started", metadata: [
|
||||
"local": url,
|
||||
"external": externalUrl
|
||||
])
|
||||
|
||||
// Save session information with local URL for the client
|
||||
let session = VNCSession(url: url)
|
||||
try vmDirectory.saveSession(session)
|
||||
break
|
||||
}
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 50_000_000)
|
||||
|
||||
attempts += 1
|
||||
if attempts >= maxAttempts {
|
||||
// If we've timed out and we requested a specific port, it likely means binding failed
|
||||
vncServer = nil
|
||||
if port != 0 {
|
||||
throw VMError.vncPortBindingFailed(requested: port, actual: -1)
|
||||
}
|
||||
throw VMError.internalError("Timeout waiting for VNC server to start")
|
||||
}
|
||||
try await Task.sleep(nanoseconds: 50_000_000) // 50ms delay between checks
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ class MockVM: VM {
|
||||
try vmDirContext.saveConfig()
|
||||
}
|
||||
|
||||
override func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?) async throws {
|
||||
override func run(noDisplay: Bool, sharedDirectories: [SharedDirectory], mount: Path?, vncPort: Int = 0) async throws {
|
||||
mockIsRunning = true
|
||||
try await super.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount)
|
||||
try await super.run(noDisplay: noDisplay, sharedDirectories: sharedDirectories, mount: mount, vncPort: vncPort)
|
||||
}
|
||||
|
||||
override func stop() async throws {
|
||||
|
||||
@@ -95,7 +95,7 @@ func testVMRunAndStop() async throws {
|
||||
|
||||
// Test running VM
|
||||
let runTask = Task {
|
||||
try await vm.run(noDisplay: false, sharedDirectories: [], mount: nil)
|
||||
try await vm.run(noDisplay: false, sharedDirectories: [], mount: nil, vncPort: 0)
|
||||
}
|
||||
|
||||
// Give the VM time to start
|
||||
|
||||
Reference in New Issue
Block a user