A simple, zero-configuration script to quickly boot FreeBSD ISO images using QEMU

Add VM logging, restart, and port forwarding features

- Introduced 'logs' command to view VM logs with optional follow flag.
- Added 'restart' command to restart a virtual machine.
- Enhanced 'start' command to support running VMs in detached mode with logging.
- Updated database schema to include port forwarding configuration.
- Implemented utility functions for setting up port forwarding arguments.

+475 -119
+24 -2
main.ts
··· 3 3 import { Command } from "@cliffy/command"; 4 4 import { createBridgeNetworkIfNeeded } from "./src/network.ts"; 5 5 import inspect from "./src/subcommands/inspect.ts"; 6 + import logs from "./src/subcommands/logs.ts"; 6 7 import ps from "./src/subcommands/ps.ts"; 8 + import restart from "./src/subcommands/restart.ts"; 7 9 import rm from "./src/subcommands/rm.ts"; 8 10 import start from "./src/subcommands/start.ts"; 9 11 import stop from "./src/subcommands/stop.ts"; ··· 54 56 .option( 55 57 "-b, --bridge <name:string>", 56 58 "Name of the network bridge to use for networking (e.g., br0)", 59 + ) 60 + .option( 61 + "-d, --detach", 62 + "Run VM in the background and print VM name", 63 + ) 64 + .option( 65 + "-p, --port-forward <mappings:string>", 66 + "Port forwarding rules in the format hostPort:guestPort (comma-separated for multiple)", 57 67 ) 58 68 .example( 59 69 "Default usage", ··· 123 133 }) 124 134 .command("start", "Start a virtual machine") 125 135 .arguments("<vm-name:string>") 126 - .action(async (_options: unknown, vmName: string) => { 127 - await start(vmName); 136 + .option("--detach, -d", "Run VM in the background and print VM name") 137 + .action(async (options: unknown, vmName: string) => { 138 + await start(vmName, Boolean((options as { detach: boolean }).detach)); 128 139 }) 129 140 .command("stop", "Stop a virtual machine") 130 141 .arguments("<vm-name:string>") ··· 140 151 .arguments("<vm-name:string>") 141 152 .action(async (_options: unknown, vmName: string) => { 142 153 await rm(vmName); 154 + }) 155 + .command("logs", "View logs of a virtual machine") 156 + .option("--follow, -f", "Follow log output") 157 + .arguments("<vm-name:string>") 158 + .action(async (options: unknown, vmName: string) => { 159 + await logs(vmName, Boolean((options as { follow: boolean }).follow)); 160 + }) 161 + .command("restart", "Restart a virtual machine") 162 + .arguments("<vm-name:string>") 163 + .action(async (_options: unknown, vmName: string) => { 164 + await restart(vmName); 143 165 }) 144 166 .parse(Deno.args); 145 167 }
+1
src/constants.ts
··· 1 1 export const CONFIG_DIR: string = `${Deno.env.get("HOME")}/.freebsd-up`; 2 2 export const DB_PATH: string = `${CONFIG_DIR}/state.sqlite`; 3 + export const LOGS_DIR: string = `${CONFIG_DIR}/logs`;
+18 -1
src/db.ts
··· 35 35 drivePath?: string; 36 36 diskFormat: string; 37 37 isoPath?: string; 38 + portForward?: string; 38 39 version: string; 39 40 status: STATUS; 40 41 pid: number; ··· 68 69 .addColumn("diskFormat", "varchar") 69 70 .addColumn("isoPath", "varchar") 70 71 .addColumn("status", "varchar", (col) => col.notNull()) 71 - .addColumn("pid", "integer", (col) => col.notNull().unique()) 72 + .addColumn("pid", "integer") 72 73 .addColumn( 73 74 "createdAt", 74 75 "varchar", ··· 84 85 85 86 async down(db: Kysely<unknown>): Promise<void> { 86 87 await db.schema.dropTable("virtual_machines").execute(); 88 + }, 89 + }; 90 + 91 + migrations["002"] = { 92 + async up(db: Kysely<unknown>): Promise<void> { 93 + await db.schema 94 + .alterTable("virtual_machines") 95 + .addColumn("portForward", "varchar") 96 + .execute(); 97 + }, 98 + 99 + async down(db: Kysely<unknown>): Promise<void> { 100 + await db.schema 101 + .alterTable("virtual_machines") 102 + .dropColumn("portForward") 103 + .execute(); 87 104 }, 88 105 }; 89 106
+5 -1
src/state.ts
··· 14 14 pid?: number, 15 15 ) { 16 16 await ctx.db.updateTable("virtual_machines") 17 - .set({ status, pid }) 17 + .set({ 18 + status, 19 + pid, 20 + updatedAt: new Date().toISOString(), 21 + }) 18 22 .where((eb) => 19 23 eb.or([ 20 24 eb("name", "=", name),
+23
src/subcommands/logs.ts
··· 1 + import { LOGS_DIR } from "../constants.ts"; 2 + 3 + export default async function (name: string, follow: boolean) { 4 + await Deno.mkdir(LOGS_DIR, { recursive: true }); 5 + const logPath = `${LOGS_DIR}/${name}.log`; 6 + 7 + const cmd = new Deno.Command(follow ? "tail" : "cat", { 8 + args: [ 9 + ...(follow ? ["-n", "100", "-f"] : []), 10 + logPath, 11 + ], 12 + stdin: "inherit", 13 + stdout: "inherit", 14 + stderr: "inherit", 15 + }); 16 + 17 + const status = await cmd.spawn().status; 18 + 19 + if (!status.success) { 20 + console.error(`Failed to view logs for virtual machine ${name}.`); 21 + Deno.exit(status.code); 22 + } 23 + }
+29 -3
src/subcommands/ps.ts
··· 3 3 import relativeTime from "dayjs/plugin/relativeTime.js"; 4 4 import utc from "dayjs/plugin/utc.js"; 5 5 import { ctx } from "../context.ts"; 6 + import type { VirtualMachine } from "../db.ts"; 6 7 7 8 dayjs.extend(relativeTime); 8 9 dayjs.extend(utc); ··· 19 20 .execute(); 20 21 21 22 const table: Table = new Table( 22 - ["NAME", "VCPU", "MEMORY", "STATUS", "PID", "BRIDGE", "MAC", "CREATED"], 23 + ["NAME", "VCPU", "MEMORY", "STATUS", "PID", "BRIDGE", "PORTS", "CREATED"], 23 24 ); 24 25 25 26 for (const vm of results) { ··· 27 28 vm.name, 28 29 vm.cpus.toString(), 29 30 vm.memory, 30 - vm.status, 31 + formatStatus(vm), 31 32 vm.pid?.toString() ?? "-", 32 33 vm.bridge ?? "-", 33 - vm.macAddress, 34 + formatPorts(vm.portForward), 34 35 dayjs.utc(vm.createdAt).local().fromNow(), 35 36 ]); 36 37 } 37 38 38 39 console.log(table.padding(2).toString()); 39 40 } 41 + 42 + function formatStatus(vm: VirtualMachine) { 43 + switch (vm.status) { 44 + case "RUNNING": 45 + return `Up ${ 46 + dayjs.utc(vm.updatedAt).local().fromNow().replace("ago", "") 47 + }`; 48 + case "STOPPED": 49 + return `Exited ${dayjs.utc(vm.updatedAt).local().fromNow()}`; 50 + default: 51 + return vm.status; 52 + } 53 + } 54 + 55 + function formatPorts(portForward?: string) { 56 + if (!portForward) { 57 + return "-"; 58 + } 59 + 60 + const mappings = portForward.split(","); 61 + return mappings.map((mapping) => { 62 + const [hostPort, guestPort] = mapping.split(":"); 63 + return `${hostPort}->${guestPort}`; 64 + }).join(", "); 65 + }
+107
src/subcommands/restart.ts
··· 1 + import chalk from "chalk"; 2 + import _ from "lodash"; 3 + import { LOGS_DIR } from "../constants.ts"; 4 + import { getInstanceState, updateInstanceState } from "../state.ts"; 5 + import { 6 + safeKillQemu, 7 + setupFirmwareFilesIfNeeded, 8 + setupNATNetworkArgs, 9 + } from "../utils.ts"; 10 + 11 + export default async function (name: string) { 12 + const vm = await getInstanceState(name); 13 + if (!vm) { 14 + console.error( 15 + `Virtual machine with name or ID ${chalk.greenBright(name)} not found.`, 16 + ); 17 + Deno.exit(1); 18 + } 19 + 20 + const success = await safeKillQemu(vm.pid, Boolean(vm.bridge)); 21 + 22 + if (!success) { 23 + console.error( 24 + `Failed to stop virtual machine ${chalk.greenBright(vm.name)}.`, 25 + ); 26 + Deno.exit(1); 27 + } 28 + await updateInstanceState(vm.id, "STOPPED"); 29 + 30 + await new Promise((resolve) => setTimeout(resolve, 2000)); 31 + 32 + await Deno.mkdir(LOGS_DIR, { recursive: true }); 33 + const logPath = `${LOGS_DIR}/${vm.name}.log`; 34 + 35 + const qemu = Deno.build.arch === "aarch64" 36 + ? "qemu-system-aarch64" 37 + : "qemu-system-x86_64"; 38 + 39 + const qemuArgs = [ 40 + ..._.compact([vm.bridge && qemu]), 41 + ..._.compact( 42 + Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"], 43 + ), 44 + ..._.compact( 45 + Deno.build.arch === "aarch64" && ["-machine", "virt,highmem=on"], 46 + ), 47 + "-cpu", 48 + vm.cpu, 49 + "-m", 50 + vm.memory, 51 + "-smp", 52 + vm.cpus.toString(), 53 + ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]), 54 + "-netdev", 55 + vm.bridge 56 + ? `bridge,id=net0,br=${vm.bridge}` 57 + : setupNATNetworkArgs(vm.portForward), 58 + "-device", 59 + `e1000,netdev=net0,mac=${vm.macAddress}`, 60 + "-nographic", 61 + "-monitor", 62 + "none", 63 + "-chardev", 64 + "stdio,id=con0,signal=off", 65 + "-serial", 66 + "chardev:con0", 67 + ...await setupFirmwareFilesIfNeeded(), 68 + ..._.compact( 69 + vm.drivePath && [ 70 + "-drive", 71 + `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 72 + ], 73 + ), 74 + ]; 75 + 76 + const fullCommand = vm.bridge 77 + ? `sudo ${qemu} ${ 78 + qemuArgs.slice(1).join(" ") 79 + } >> "${logPath}" 2>&1 & echo $!` 80 + : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 81 + 82 + const cmd = new Deno.Command("sh", { 83 + args: ["-c", fullCommand], 84 + stdin: "null", 85 + stdout: "piped", 86 + }); 87 + 88 + const { stdout } = await cmd.spawn().output(); 89 + const qemuPid = parseInt(new TextDecoder().decode(stdout).trim(), 10); 90 + 91 + await new Promise((resolve) => setTimeout(resolve, 2000)); 92 + 93 + await updateInstanceState(vm.id, "RUNNING", qemuPid); 94 + 95 + console.log( 96 + `${chalk.greenBright(vm.name)} restarted with PID ${ 97 + chalk.greenBright(qemuPid) 98 + }.`, 99 + ); 100 + console.log( 101 + `Logs are being written to ${chalk.blueBright(logPath)}`, 102 + ); 103 + 104 + await new Promise((resolve) => setTimeout(resolve, 2000)); 105 + 106 + Deno.exit(0); 107 + }
+83 -51
src/subcommands/start.ts
··· 1 1 import _ from "lodash"; 2 + import { LOGS_DIR } from "../constants.ts"; 2 3 import { getInstanceState, updateInstanceState } from "../state.ts"; 3 - import { setupFirmwareFilesIfNeeded } from "../utils.ts"; 4 + import { setupFirmwareFilesIfNeeded, setupNATNetworkArgs } from "../utils.ts"; 4 5 5 - export default async function (name: string) { 6 + export default async function (name: string, detach: boolean = false) { 6 7 const vm = await getInstanceState(name); 7 8 if (!vm) { 8 9 console.error( ··· 16 17 const qemu = Deno.build.arch === "aarch64" 17 18 ? "qemu-system-aarch64" 18 19 : "qemu-system-x86_64"; 19 - const cmd = new Deno.Command(vm.bridge ? "sudo" : qemu, { 20 - args: [ 21 - ..._.compact([vm.bridge && qemu]), 22 - ..._.compact( 23 - Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"], 24 - ), 25 - ..._.compact( 26 - Deno.build.arch === "aarch64" && ["-machine", "virt,highmem=on"], 27 - ), 28 - "-cpu", 29 - vm.cpu, 30 - "-m", 31 - vm.memory, 32 - "-smp", 33 - vm.cpus.toString(), 34 - ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]), 35 - "-netdev", 36 - vm.bridge 37 - ? `bridge,id=net0,br=${vm.bridge}` 38 - : "user,id=net0,hostfwd=tcp::2222-:22", 39 - "-device", 40 - `e1000,netdev=net0,mac=${vm.macAddress}`, 41 - "-display", 42 - "none", 43 - "-vga", 44 - "none", 45 - "-monitor", 46 - "none", 47 - "-chardev", 48 - "stdio,id=con0,signal=off", 49 - "-serial", 50 - "chardev:con0", 51 - ...await setupFirmwareFilesIfNeeded(), 52 - ..._.compact( 53 - vm.drivePath && [ 54 - "-drive", 55 - `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 56 - ], 57 - ), 58 - ], 59 - stdin: "inherit", 60 - stdout: "inherit", 61 - stderr: "inherit", 62 - }).spawn(); 63 20 64 - await updateInstanceState(name, "RUNNING", cmd.pid); 21 + const qemuArgs = [ 22 + ..._.compact([vm.bridge && qemu]), 23 + ..._.compact( 24 + Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"], 25 + ), 26 + ..._.compact( 27 + Deno.build.arch === "aarch64" && ["-machine", "virt,highmem=on"], 28 + ), 29 + "-cpu", 30 + vm.cpu, 31 + "-m", 32 + vm.memory, 33 + "-smp", 34 + vm.cpus.toString(), 35 + ..._.compact([vm.isoPath && "-cdrom", vm.isoPath]), 36 + "-netdev", 37 + vm.bridge 38 + ? `bridge,id=net0,br=${vm.bridge}` 39 + : setupNATNetworkArgs(vm.portForward), 40 + "-device", 41 + `e1000,netdev=net0,mac=${vm.macAddress}`, 42 + "-nographic", 43 + "-monitor", 44 + "none", 45 + "-chardev", 46 + "stdio,id=con0,signal=off", 47 + "-serial", 48 + "chardev:con0", 49 + ...await setupFirmwareFilesIfNeeded(), 50 + ..._.compact( 51 + vm.drivePath && [ 52 + "-drive", 53 + `file=${vm.drivePath},format=${vm.diskFormat},if=virtio`, 54 + ], 55 + ), 56 + ]; 65 57 66 - const status = await cmd.status; 58 + if (detach) { 59 + await Deno.mkdir(LOGS_DIR, { recursive: true }); 60 + const logPath = `${LOGS_DIR}/${vm.name}.log`; 61 + 62 + const fullCommand = vm.bridge 63 + ? `sudo ${qemu} ${ 64 + qemuArgs.slice(1).join(" ") 65 + } >> "${logPath}" 2>&1 & echo $!` 66 + : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 67 + 68 + const cmd = new Deno.Command("sh", { 69 + args: ["-c", fullCommand], 70 + stdin: "null", 71 + stdout: "piped", 72 + }); 73 + 74 + const { stdout } = await cmd.spawn().output(); 75 + const qemuPid = parseInt(new TextDecoder().decode(stdout).trim(), 10); 76 + 77 + await updateInstanceState(name, "RUNNING", qemuPid); 78 + 79 + console.log( 80 + `Virtual machine ${vm.name} started in background (PID: ${qemuPid})`, 81 + ); 82 + console.log(`Logs will be written to: ${logPath}`); 83 + 84 + // Exit successfully while keeping VM running in background 85 + Deno.exit(0); 86 + } else { 87 + const cmd = new Deno.Command(vm.bridge ? "sudo" : qemu, { 88 + args: qemuArgs, 89 + stdin: "inherit", 90 + stdout: "inherit", 91 + stderr: "inherit", 92 + }); 67 93 68 - await updateInstanceState(name, "STOPPED", cmd.pid); 94 + const child = cmd.spawn(); 95 + await updateInstanceState(name, "RUNNING", child.pid); 69 96 70 - if (!status.success) { 71 - Deno.exit(status.code); 97 + const status = await child.status; 98 + 99 + await updateInstanceState(name, "STOPPED", child.pid); 100 + 101 + if (!status.success) { 102 + Deno.exit(status.code); 103 + } 72 104 } 73 105 }
+185 -61
src/utils.ts
··· 2 2 import chalk from "chalk"; 3 3 import _ from "lodash"; 4 4 import Moniker from "moniker"; 5 + import { LOGS_DIR } from "./constants.ts"; 5 6 import { generateRandomMacAddress } from "./network.ts"; 6 - import { saveInstanceState } from "./state.ts"; 7 + import { saveInstanceState, updateInstanceState } from "./state.ts"; 7 8 8 9 const DEFAULT_VERSION = "14.3-RELEASE"; 9 10 ··· 16 17 diskFormat: string; 17 18 size: string; 18 19 bridge?: string; 20 + portForward?: string; 21 + detach?: boolean; 19 22 } 20 23 21 24 async function du(path: string): Promise<number> { ··· 135 138 ]; 136 139 } 137 140 141 + export function setupPortForwardingArgs(portForward?: string): string { 142 + if (!portForward) { 143 + return ""; 144 + } 145 + 146 + const forwards = portForward.split(",").map((pair) => { 147 + const [hostPort, guestPort] = pair.split(":"); 148 + return `hostfwd=tcp::${hostPort}-:${guestPort}`; 149 + }); 150 + 151 + return forwards.join(","); 152 + } 153 + 154 + export function setupNATNetworkArgs(portForward?: string): string { 155 + if (!portForward) { 156 + return "user,id=net0"; 157 + } 158 + 159 + const portForwarding = setupPortForwardingArgs(portForward); 160 + return `user,id=net0,${portForwarding}`; 161 + } 162 + 138 163 export async function runQemu( 139 164 isoPath: string | null, 140 165 options: Options, ··· 145 170 ? "qemu-system-aarch64" 146 171 : "qemu-system-x86_64"; 147 172 148 - const cmd = new Deno.Command(options.bridge ? "sudo" : qemu, { 149 - args: [ 150 - ..._.compact([options.bridge && qemu]), 151 - ..._.compact( 152 - Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"], 153 - ), 154 - ..._.compact( 155 - Deno.build.arch === "aarch64" && ["-machine", "virt,highmem=on"], 156 - ), 157 - "-cpu", 158 - options.cpu, 159 - "-m", 160 - options.memory, 161 - "-smp", 162 - options.cpus.toString(), 163 - ..._.compact([isoPath && "-cdrom", isoPath]), 164 - "-netdev", 165 - options.bridge 166 - ? `bridge,id=net0,br=${options.bridge}` 167 - : "user,id=net0,hostfwd=tcp::2222-:22", 168 - "-device", 169 - `e1000,netdev=net0,mac=${macAddress}`, 170 - "-nographic", 171 - "-monitor", 172 - "none", 173 - "-chardev", 174 - "stdio,id=con0,signal=off", 175 - "-serial", 176 - "chardev:con0", 177 - ...await setupFirmwareFilesIfNeeded(), 178 - ..._.compact( 179 - options.image && [ 180 - "-drive", 181 - `file=${options.image},format=${options.diskFormat},if=virtio`, 182 - ], 183 - ), 184 - ], 185 - stdin: "inherit", 186 - stdout: "inherit", 187 - stderr: "inherit", 188 - }).spawn(); 173 + const qemuArgs = [ 174 + ..._.compact([options.bridge && qemu]), 175 + ..._.compact( 176 + Deno.build.os === "darwin" ? ["-accel", "hvf"] : ["-enable-kvm"], 177 + ), 178 + ..._.compact( 179 + Deno.build.arch === "aarch64" && ["-machine", "virt,highmem=on"], 180 + ), 181 + "-cpu", 182 + options.cpu, 183 + "-m", 184 + options.memory, 185 + "-smp", 186 + options.cpus.toString(), 187 + ..._.compact([isoPath && "-cdrom", isoPath]), 188 + "-netdev", 189 + options.bridge 190 + ? `bridge,id=net0,br=${options.bridge}` 191 + : setupNATNetworkArgs(options.portForward), 192 + "-device", 193 + `e1000,netdev=net0,mac=${macAddress}`, 194 + "-nographic", 195 + "-monitor", 196 + "none", 197 + "-chardev", 198 + "stdio,id=con0,signal=off", 199 + "-serial", 200 + "chardev:con0", 201 + ...await setupFirmwareFilesIfNeeded(), 202 + ..._.compact( 203 + options.image && [ 204 + "-drive", 205 + `file=${options.image},format=${options.diskFormat},if=virtio`, 206 + ], 207 + ), 208 + ]; 189 209 190 - await saveInstanceState({ 191 - id: createId(), 192 - name: Moniker.choose(), 193 - bridge: options.bridge, 194 - macAddress, 195 - memory: options.memory, 196 - cpus: options.cpus, 197 - cpu: options.cpu, 198 - diskSize: options.size, 199 - diskFormat: options.diskFormat, 200 - isoPath: isoPath ? Deno.realPathSync(isoPath) : undefined, 201 - drivePath: options.image ? Deno.realPathSync(options.image) : undefined, 202 - version: DEFAULT_VERSION, 203 - status: "RUNNING", 204 - pid: cmd.pid, 205 - }); 210 + const name = Moniker.choose(); 206 211 207 - const status = await cmd.status; 212 + if (options.detach) { 213 + await Deno.mkdir(LOGS_DIR, { recursive: true }); 214 + const logPath = `${LOGS_DIR}/${name}.log`; 208 215 209 - if (!status.success) { 210 - Deno.exit(status.code); 216 + const fullCommand = options.bridge 217 + ? `sudo ${qemu} ${ 218 + qemuArgs.slice(1).join(" ") 219 + } >> "${logPath}" 2>&1 & echo $!` 220 + : `${qemu} ${qemuArgs.join(" ")} >> "${logPath}" 2>&1 & echo $!`; 221 + 222 + const cmd = new Deno.Command("sh", { 223 + args: ["-c", fullCommand], 224 + stdin: "null", 225 + stdout: "piped", 226 + }); 227 + 228 + const { stdout } = await cmd.spawn().output(); 229 + const qemuPid = parseInt(new TextDecoder().decode(stdout).trim(), 10); 230 + 231 + await saveInstanceState({ 232 + id: createId(), 233 + name, 234 + bridge: options.bridge, 235 + macAddress, 236 + memory: options.memory, 237 + cpus: options.cpus, 238 + cpu: options.cpu, 239 + diskSize: options.size, 240 + diskFormat: options.diskFormat, 241 + portForward: options.portForward, 242 + isoPath: isoPath ? Deno.realPathSync(isoPath) : undefined, 243 + drivePath: options.image ? Deno.realPathSync(options.image) : undefined, 244 + version: DEFAULT_VERSION, 245 + status: "RUNNING", 246 + pid: qemuPid, 247 + }); 248 + 249 + console.log( 250 + `Virtual machine ${name} started in background (PID: ${qemuPid})`, 251 + ); 252 + console.log(`Logs will be written to: ${logPath}`); 253 + 254 + // Exit successfully while keeping VM running in background 255 + Deno.exit(0); 256 + } else { 257 + const cmd = new Deno.Command(options.bridge ? "sudo" : qemu, { 258 + args: qemuArgs, 259 + stdin: "inherit", 260 + stdout: "inherit", 261 + stderr: "inherit", 262 + }) 263 + .spawn(); 264 + 265 + await saveInstanceState({ 266 + id: createId(), 267 + name, 268 + bridge: options.bridge, 269 + macAddress, 270 + memory: options.memory, 271 + cpus: options.cpus, 272 + cpu: options.cpu, 273 + diskSize: options.size, 274 + diskFormat: options.diskFormat, 275 + portForward: options.portForward, 276 + isoPath: isoPath ? Deno.realPathSync(isoPath) : undefined, 277 + drivePath: options.image ? Deno.realPathSync(options.image) : undefined, 278 + version: DEFAULT_VERSION, 279 + status: "RUNNING", 280 + pid: cmd.pid, 281 + }); 282 + 283 + const status = await cmd.status; 284 + 285 + await updateInstanceState(name, "STOPPED"); 286 + 287 + if (!status.success) { 288 + Deno.exit(status.code); 289 + } 211 290 } 212 291 } 213 292 ··· 235 314 } 236 315 237 316 return input; 317 + } 318 + 319 + export async function safeKillQemu( 320 + pid: number, 321 + useSudo: boolean = false, 322 + ): Promise<boolean> { 323 + const killArgs = useSudo 324 + ? ["sudo", "kill", "-TERM", pid.toString()] 325 + : ["kill", "-TERM", pid.toString()]; 326 + 327 + const termCmd = new Deno.Command(killArgs[0], { 328 + args: killArgs.slice(1), 329 + stdout: "null", 330 + stderr: "null", 331 + }); 332 + 333 + const termStatus = await termCmd.spawn().status; 334 + 335 + if (termStatus.success) { 336 + await new Promise((resolve) => setTimeout(resolve, 3000)); 337 + 338 + const checkCmd = new Deno.Command("kill", { 339 + args: ["-0", pid.toString()], 340 + stdout: "null", 341 + stderr: "null", 342 + }); 343 + 344 + const checkStatus = await checkCmd.spawn().status; 345 + if (!checkStatus.success) { 346 + return true; 347 + } 348 + } 349 + 350 + const killKillArgs = useSudo 351 + ? ["sudo", "kill", "-KILL", pid.toString()] 352 + : ["kill", "-KILL", pid.toString()]; 353 + 354 + const killCmd = new Deno.Command(killKillArgs[0], { 355 + args: killKillArgs.slice(1), 356 + stdout: "null", 357 + stderr: "null", 358 + }); 359 + 360 + const killStatus = await killCmd.spawn().status; 361 + return killStatus.success; 238 362 } 239 363 240 364 export async function createDriveImageIfNeeded(