Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,12 @@ extension CloudMachine {
default: .unknown
}
// The record's `capabilities` are machine capability names
// (`agent_session`, …), not provider operations. The operations are
// what this app reaches for a Worker machine: app server ops (resize,
// snapshot, restore) and the Worker's exec and file routes. Fork,
// port links and stats have no Worker route yet, so they are refused
// here instead of failing on `/api/vm`, where the machine has no record.
let capabilities = Capabilities(snapshot: true, restore: true, exec: true, sizing: true)
// The idle auto-pause: `/api/vm` names when the backend sends them, else the
// record's own `idle_policy.idle_seconds` (what cloud.machine.idle_policy.set writes).
let policySeconds = value["idle_policy"]?["idle_seconds"]?.doubleValue
// (`agent_session`, …), not provider operations, so the machine
// declares none and every Cloud action stays available.
// The idle auto-pause fields, when the backend sends them (`/api/vm` names).
let minutes = (value["idlePauseMinutes"]?.doubleValue).flatMap { Int(exactly: $0) }
?? policySeconds.flatMap { Int(exactly: $0 / 60) }
let idlePause = Self.idlePauseDiagnostics(value["idlePause"])
self.init(id: id, provider: "cmux-next", status: status, displayName: value["name"]?.stringValue, capabilities: capabilities,
self.init(id: id, provider: "cmux-next", status: status, displayName: value["name"]?.stringValue,
pauseReason: Self.nextPauseReason(value["pause_reason"] ?? value["pauseReason"]), idlePauseMinutes: minutes,
idlePause: idlePause, lastChangeFailure: Self.changeFailure(value["error"]))
serverRevision = value["revision"]?.stringValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,66 +69,6 @@ extension CloudService {
return try await appServerFlow(team: team, run: appOps).approvedCall(op, args: args, confirm: confirm)
}

/// A machine action (Kill, Pause, Resume, Rename, idle pause, resize, snapshots, restore) on a
/// Worker machine: its app server op, never `/api/vm`, where the machine has no record.
/// `byPerson`: the person's own gesture in this app, already confirmed where the action needs it
/// (the registry's sheet for Kill). Only then is a server-held approval answered with the
/// person's own session. Any other caller (the CLI, MCP, an agent, a script) sends the op as
/// `script` and gets the server's `approval.pending` back, for the person to answer in the feed.
@discardableResult
func appServerMachineCall(_ op: String, _ machineID: String, _ fields: [String: JSONValue] = [:], byPerson: Bool) async throws -> JSONValue {
var args = fields
args["machine"] = .string(machineID)
return try await appServerCall(op, args: .object(args), byPerson: byPerson)
}

/// One app server mutation, by the person (approval loop, `user`) or not (`script`, no approval answered).
func appServerCall(_ op: String, args: JSONValue, byPerson: Bool) async throws -> JSONValue {
guard byPerson else { return try await appOps(op, args, UUID().uuidString, .script) }
return try await appServerApprovedCall(op, args: args, confirm: { true })
}

/// `cloud.machine.get`: a Worker machine's live record (the classic `/api/vm/{id}` read).
func appServerMachine(_ machineID: String) async throws -> CloudMachine {
let answer = try await appOps("cloud.machine.get", .object(["machine": .string(machineID)]), nil, .script)
guard let machine = CloudMachine(next: answer) ?? answer["machine"].flatMap(CloudMachine.init(next:)) else {
throw CloudAppOpFailure(code: "cmux.cloud.bad_response", message: "cloud.machine.get answered no machine")
}
return machine
}

/// `cloud.machine.resize` with the classic resize's fields (vCPUs, MiB of memory and disk).
func appServerResize(_ machineID: String, cpu: Int?, memoryMb: Int?, storageMb: Int?, byPerson: Bool) async throws {
var size: [String: JSONValue] = [:]
if let cpu { size["cpu"] = .number(Double(cpu)) }
if let memoryMb { size["memory_mb"] = .number(Double(memoryMb)) }
if let storageMb { size["disk_mb"] = .number(Double(storageMb)) }
try await appServerMachineCall("cloud.machine.resize", machineID, ["size": .object(size)], byPerson: byPerson)
}

/// `cloud.snapshot.create`: the new snapshot's id.
func appServerSnapshot(_ machineID: String, name: String?, byPerson: Bool) async throws -> String {
let answer = try await appServerMachineCall("cloud.snapshot.create", machineID, name.map { ["name": .string($0)] } ?? [:], byPerson: byPerson)
guard let id = answer["snapshot"]?["id"]?.stringValue else {
throw CloudAppOpFailure(code: "cmux.cloud.bad_response", message: "cloud.snapshot.create answered no snapshot")
}
return id
}

/// `cloud.snapshot.delete`.
func appServerDeleteSnapshot(_ snapshotID: String, byPerson: Bool) async throws {
_ = try await appServerCall("cloud.snapshot.delete", args: .object(["snapshot": .string(snapshotID)]), byPerson: byPerson)
}

/// `cloud.snapshot.restore`: the new machine's id.
func appServerRestore(snapshotID: String, byPerson: Bool) async throws -> String {
let answer = try await appServerCall("cloud.snapshot.restore", args: .object(["snapshot": .string(snapshotID)]), byPerson: byPerson)
guard let id = answer["machine"]?["id"]?.stringValue else {
throw CloudAppOpFailure(code: "cmux.cloud.bad_response", message: "cloud.snapshot.restore answered no machine")
}
return id
}

/// The team's Cloud environments for the New Cloud Workspace dialog; none when the list fails
/// (the dialog then offers only a new one).
func appServerEnvironmentOptions() async -> [CloudWorkspaceSetupWizard.EnvironmentOption] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,12 +773,12 @@ final class CloudService {
return await current()
}
/// Kill, Pause and Resume show their transition from the click until the next list settles it.
func deleteMachine(_ machineID: String, byPerson: Bool = false) async throws {
func deleteMachine(_ machineID: String) async throws {
// Retain credentials and broker registration until a list confirms removal. Both
// the request and the asynchronous provider deletion can still fail.
try await changes.run(.deleting, machineID: machineID, of: { machines.session(machineID) },
onRollback: { [weak self] id in self?.machines.session(machineID)?.mutationFailed(id) }) {
if configuration.linkSource == .appServer { try await appServerMachineCall("cloud.machine.delete", machineID, byPerson: byPerson) } else { try await api.deleteMachine(machineID) }
try await api.deleteMachine(machineID)
}
// A list in flight was asked while this machine still existed.
listGeneration += 1
Expand All @@ -787,31 +787,26 @@ final class CloudService {
logger.info("deleted machine \(machineID, privacy: .public)")
}

func pauseMachine(_ machineID: String, byPerson: Bool = false) async throws {
func pauseMachine(_ machineID: String) async throws {
try await changes.run(.pausing, machineID: machineID, of: { machines.session(machineID) },
onRollback: { [weak self] id in self?.machines.session(machineID)?.mutationFailed(id) }) {
if configuration.linkSource == .appServer { try await appServerMachineCall("cloud.machine.pause", machineID, byPerson: byPerson) } else { try await api.pauseMachine(machineID) }
try await api.pauseMachine(machineID)
}
changes.began { [weak self] in _ = await self?.refresh() }; await refresh(); logger.info("paused machine \(machineID, privacy: .public)")
}

func resumeMachine(_ machineID: String, byPerson: Bool = false) async throws {
func resumeMachine(_ machineID: String) async throws {
try await changes.run(.starting, machineID: machineID, of: { machines.session(machineID) },
onRollback: { [weak self] id in self?.machines.session(machineID)?.mutationFailed(id) }) {
if configuration.linkSource == .appServer { try await appServerMachineCall("cloud.machine.start", machineID, byPerson: byPerson) } else { try await api.resumeMachine(machineID) }
try await api.resumeMachine(machineID)
}
changes.began { [weak self] in _ = await self?.refresh() }; await refresh(); logger.info("resumed machine \(machineID, privacy: .public)")
}

/// The machine's idle auto-pause (`PUT /api/vm/{id}/idle-pause`, or
/// `cloud.machine.idle_policy.set` on the app server link): minutes of idle
/// before cmux Cloud pauses it; 0 is Off.
func setIdlePause(_ machineID: String, minutes: Int, byPerson: Bool = false) async throws {
if configuration.linkSource == .appServer {
try await appServerMachineCall("cloud.machine.idle_policy.set", machineID, ["idle_seconds": .number(Double(minutes * 60))], byPerson: byPerson)
} else {
try await api.setIdlePause(machineID, minutes: minutes)
}
/// The machine's idle auto-pause (`PUT /api/vm/{id}/idle-pause`): minutes
/// of idle before cmux Cloud pauses it; 0 is Off.
func setIdlePause(_ machineID: String, minutes: Int) async throws {
try await api.setIdlePause(machineID, minutes: minutes)
machines.session(machineID)?.machine.idlePauseMinutes = minutes
logger.info("set idle pause \(machineID, privacy: .public) to \(minutes) min")
}
Expand All @@ -829,14 +824,8 @@ final class CloudService {
do { _ = try await hub.linkedSocketPath() } catch { logger.error("wireguard hub for ssh routes: \(String(describing: error), privacy: .public)") }
}

func renameMachine(_ machineID: String, to name: String, byPerson: Bool = false) async throws {
if configuration.linkSource == .appServer {
// The app server keeps a name on every machine (1 to 80 characters): no clearing it there.
guard !name.isEmpty else { return }
try await appServerMachineCall("cloud.machine.rename", machineID, ["name": .string(name)], byPerson: byPerson)
} else {
try await api.renameMachine(machineID, to: name.isEmpty ? nil : name)
}
func renameMachine(_ machineID: String, to name: String) async throws {
try await api.renameMachine(machineID, to: name.isEmpty ? nil : name)
machines.session(machineID)?.machine.displayName = name.isEmpty ? nil : name
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ enum CloudGitHubHandlers {
// The cmux GitHub App by default; the machine's own sign-in only when the App, the team's
// connection or the machine's Cloud agent is not there.
let setUp: @MainActor (CloudGitHubSetup.GitIdentity, CloudGitHubApp) async throws -> CloudGitHubSetup.Outcome = { identity, app in
let ran = try await cloud.machineAPI.exec(session.machineID, command: CloudGitHubSetup.command(identity, app: app == .ready),
let ran = try await cloud.api.exec(session.machineID, command: CloudGitHubSetup.command(identity, app: app == .ready),
timeoutMs: CloudGitHubSetup.timeoutMs)
return CloudGitHubSetup.parse(exitCode: ran.exitCode, stdout: ran.stdout)
}
Expand Down
Loading
Loading