From 4a7cfd4b0f9dd5ddc4b5a03dc8a12d10fb62717e Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 23 Oct 2024 11:52:20 -0400 Subject: [PATCH 1/3] refactor: update vm manager + code suggestions ### What does this PR do? * Updates the VM manager so that it is more modular * Fixes suggested code changes * Updates DiskImageDetailsVirtualMachine check for websocket with a timeout. ### Screenshot / video of UI N/A ### What issues does this PR fix or reference? Closes https://github.com/containers/podman-desktop-extension-bootc/issues/953 ### How to test this PR? Everything should work like normal launching a VM for arm or amd64 Signed-off-by: Charlie Drage --- packages/backend/src/api-impl.ts | 9 +- packages/backend/src/vm-manager.ts | 172 +++++++++--------- .../DiskImageDetailsVirtualMachine.svelte | 7 + 3 files changed, 102 insertions(+), 86 deletions(-) diff --git a/packages/backend/src/api-impl.ts b/packages/backend/src/api-impl.ts index bfa1a51f..5d952d3a 100644 --- a/packages/backend/src/api-impl.ts +++ b/packages/backend/src/api-impl.ts @@ -29,7 +29,7 @@ import { checkPrereqs, isLinux, isMac, getUidGid } from './machine-utils'; import * as fs from 'node:fs'; import path from 'node:path'; import { getContainerEngine } from './container-utils'; -import VMManager from './vm-manager'; +import { createVMManager, stopCurrentVM } from './vm-manager'; import examplesCatalog from '../assets/examples.json'; import type { ExamplesList } from '/@shared/src/models/examples'; @@ -54,7 +54,7 @@ export class BootcApiImpl implements BootcApi { } async checkVMLaunchPrereqs(build: BootcBuildInfo): Promise { - return new VMManager(build).checkVMLaunchPrereqs(); + return createVMManager(build).checkVMLaunchPrereqs(); } async buildExists(folder: string, types: BuildType[]): Promise { @@ -67,7 +67,7 @@ export class BootcApiImpl implements BootcApi { async launchVM(build: BootcBuildInfo): Promise { try { - await new VMManager(build).launchVM(); + await createVMManager(build).launchVM(); // Notify it has successfully launched await this.notify(Messages.MSG_VM_LAUNCH_ERROR, { success: 'Launched!', error: '' }); } catch (e) { @@ -82,12 +82,11 @@ export class BootcApiImpl implements BootcApi { } await this.notify(Messages.MSG_VM_LAUNCH_ERROR, { success: '', error: errorMessage }); } - return Promise.resolve(); } // Stop VM by pid file on the system async stopCurrentVM(): Promise { - return await new VMManager().stopCurrentVM(); + return stopCurrentVM(); } async deleteBuilds(builds: BootcBuildInfo[]): Promise { diff --git a/packages/backend/src/vm-manager.ts b/packages/backend/src/vm-manager.ts index 5351e9d2..086cb3d3 100644 --- a/packages/backend/src/vm-manager.ts +++ b/packages/backend/src/vm-manager.ts @@ -37,16 +37,18 @@ const memorySize = '4G'; const websocketPort = '45252'; const rawImageLocation = 'image/disk.raw'; -export default class VMManager { - private build: BootcBuildInfo; +// Abstract base class +export abstract class VMManagerBase { + protected build: BootcBuildInfo; - // Only values needed is the location of the VM file as well as the architecture of the image that - // will be used. - constructor(build?: BootcBuildInfo) { - this.build = build!; + constructor(build: BootcBuildInfo) { + this.build = build; } - // Launch the VM by generating the appropriate QEMU command and then launching it with process.exec + public abstract checkVMLaunchPrereqs(): Promise; + + protected abstract generateLaunchCommand(diskImage: string): string[]; + public async launchVM(): Promise { const diskImage = this.getDiskImagePath(); @@ -65,112 +67,98 @@ export default class VMManager { await extensionApi.process.exec('sh', ['-c', `${command.join(' ')}`]); } catch (e) { - this.handleError(e); + handleStdError(e); } } - // We only support running one VM at at a time, so we kill the process by reading the pid from the universal pid file we use. - public async stopCurrentVM(): Promise { - try { - await extensionApi.process.exec('sh', ['-c', `kill -9 \`cat ${pidFile}\``]); - } catch (e) { - // Ignore if it contains 'No such process' as that means the VM is already stopped / not running. - if (e instanceof Error && 'stderr' in e && typeof e.stderr === 'string' && e.stderr.includes('No such process')) { - return; - } - this.handleError(e); - } + protected getDiskImagePath(): string { + return path.join(this.build.folder, rawImageLocation); } +} - // Prerequisite checks before launching the VM which includes checking if QEMU is installed as well as other OS specific checks. +// Mac ARM VM Manager +class MacArmNativeVMManager extends VMManagerBase { public async checkVMLaunchPrereqs(): Promise { const diskImage = this.getDiskImagePath(); if (!fs.existsSync(diskImage)) { return `Raw disk image not found at ${diskImage}. Please build a .raw disk image first.`; } - if (!this.isArchitectureSupported()) { + if (this.build.arch !== 'arm64') { return `Unsupported architecture: ${this.build.arch}`; } - // Future support for Mac Intel, Linux ARM, Linux X86 and Windows ARM, Windows X86 to be added here. - if (isMac() && isArm()) { - return this.checkMacPrereqs(); - } else { - return 'Unsupported OS. Only MacOS Silicon is supported.'; - } - } - - private getDiskImagePath(): string { - return path.join(this.build.folder, rawImageLocation); - } - - private isArchitectureSupported(): boolean { - return this.build.arch === 'amd64' || this.build.arch === 'arm64'; - } - - private checkMacPrereqs(): string | undefined { const installDisclaimer = 'Please install qemu via our installation document'; - if (!fs.existsSync(macQemuX86Binary)) { - return `QEMU x86 binary not found at ${macQemuX86Binary}. ${installDisclaimer}`; - } - if (this.build.arch === 'arm64' && !fs.existsSync(macQemuArm64Binary)) { + if (!fs.existsSync(macQemuArm64Binary)) { return `QEMU arm64 binary not found at ${macQemuArm64Binary}. ${installDisclaimer}`; } - if (this.build.arch === 'arm64' && !fs.existsSync(macQemuArm64Edk2)) { + if (!fs.existsSync(macQemuArm64Edk2)) { return `QEMU arm64 edk2-aarch64-code.fd file not found at ${macQemuArm64Edk2}. ${installDisclaimer}`; } return undefined; } - // Supported: MacOS Silicon - // Unsupported: MacOS Intel, Linux, Windows - private generateLaunchCommand(diskImage: string): string[] { - // Future support for Mac Intel, Linux ARM, Linux X86 and Windows ARM, Windows X86 to be added here. - if (isMac() && isArm()) { - switch (this.build.arch) { - case 'amd64': - return this.generateMacX86Command(diskImage); - case 'arm64': - return this.generateMacArm64Command(diskImage); - } - } - return []; - } - - private generateMacX86Command(diskImage: string): string[] { + protected generateLaunchCommand(diskImage: string): string[] { return [ - macQemuX86Binary, + macQemuArm64Binary, '-m', memorySize, '-nographic', + '-M', + 'virt', + '-accel', + 'hvf', '-cpu', - 'Broadwell-v4', - '-pidfile', - pidFile, + 'host', + '-smp', + '4', '-serial', `websocket:127.0.0.1:${websocketPort},server,nowait`, + '-pidfile', + pidFile, '-netdev', - `user,id=mynet0,${hostForwarding}`, + `user,id=usernet,${hostForwarding}`, '-device', - 'e1000,netdev=mynet0', + 'virtio-net,netdev=usernet', + '-drive', + `file=${macQemuArm64Edk2},format=raw,if=pflash,readonly=on`, '-snapshot', diskImage, ]; } +} + +// Mac ARM running x86 images VM Manager +class MacArmX86VMManager extends VMManagerBase { + public async checkVMLaunchPrereqs(): Promise { + const diskImage = this.getDiskImagePath(); + if (!fs.existsSync(diskImage)) { + return `Raw disk image not found at ${diskImage}. Please build a .raw disk image first.`; + } + + if (this.build.arch !== 'amd64') { + return `Unsupported architecture: ${this.build.arch}`; + } + + const installDisclaimer = 'Please install qemu via our installation document'; + if (!fs.existsSync(macQemuX86Binary)) { + return `QEMU x86 binary not found at ${macQemuX86Binary}. ${installDisclaimer}`; + } + return undefined; + } - private generateMacArm64Command(diskImage: string): string[] { + protected generateLaunchCommand(diskImage: string): string[] { return [ - macQemuArm64Binary, + macQemuX86Binary, '-m', memorySize, '-nographic', - '-M', - 'virt', + '-cpu', + 'qemu64', + '-machine', + 'q35', '-accel', 'hvf', - '-cpu', - 'host', '-smp', '4', '-serial', @@ -180,21 +168,43 @@ export default class VMManager { '-netdev', `user,id=usernet,${hostForwarding}`, '-device', - 'virtio-net,netdev=usernet', - '-drive', - `file=${macQemuArm64Edk2},format=raw,if=pflash,readonly=on`, + 'e1000,netdev=usernet', '-snapshot', diskImage, ]; } +} - // When running process.exec we should TRY and get stderr which it outputs (sometimes) so we do not get an "exit code 1" error with - // no information. - private handleError(e: unknown): void { - if (e instanceof Error && 'stderr' in e) { - throw new Error(typeof e.stderr === 'string' ? e.stderr : 'Unknown error'); - } else { - throw new Error('Unknown error'); +// Factory function to create the appropriate VM Manager +export function createVMManager(build: BootcBuildInfo): VMManagerBase { + // Only thing that we support is Mac M1 at the moment + if (isMac() && isArm()) { + if (build.arch === 'arm64') { + return new MacArmNativeVMManager(build); + } else if (build.arch === 'amd64') { + return new MacArmX86VMManager(build); } } + throw new Error('Unsupported OS or architecture'); +} + +// Function to stop the current VM +export async function stopCurrentVM(): Promise { + try { + await extensionApi.process.exec('sh', ['-c', `kill -9 \`cat ${pidFile}\``]); + } catch (e) { + if (e instanceof Error && 'stderr' in e && typeof e.stderr === 'string' && e.stderr.includes('No such process')) { + return; + } + handleStdError(e); + } +} + +// Error handling function +function handleStdError(e: unknown): void { + if (e instanceof Error && 'stderr' in e) { + throw new Error(typeof e.stderr === 'string' ? e.stderr : 'Unknown error'); + } else { + throw new Error('Unknown error'); + } } diff --git a/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte b/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte index 161285fa..b8c80d4a 100644 --- a/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte +++ b/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte @@ -187,7 +187,14 @@ async function launchVM(build: BootcBuildInfo): Promise { // To avoid a blank terminal wait until terminal has logs and and then show it // logs.terminal.buffer.normal will contain the "ascii cursor" with a value of 1 until there is more logs. // we wait until buffer.normal.length is more than 1. + const startTime = Date.now(); + const timeout = 30000; // 30 seconds + while (logsTerminal.buffer.normal.length < 1) { + if (Date.now() - startTime > timeout) { + console.error('Timeout waiting for terminal logs'); + break; + } await new Promise(resolve => setTimeout(resolve, 500)); } From ad53e0a983d2b2ec967e682c144f41d90722ffd0 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 23 Oct 2024 13:46:58 -0400 Subject: [PATCH 2/3] Update packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte Co-authored-by: Florent BENOIT --- .../src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte b/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte index b8c80d4a..f8d7f367 100644 --- a/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte +++ b/packages/frontend/src/lib/disk-image/DiskImageDetailsVirtualMachine.svelte @@ -188,7 +188,7 @@ async function launchVM(build: BootcBuildInfo): Promise { // logs.terminal.buffer.normal will contain the "ascii cursor" with a value of 1 until there is more logs. // we wait until buffer.normal.length is more than 1. const startTime = Date.now(); - const timeout = 30000; // 30 seconds + const timeout = 30_000; // 30 seconds while (logsTerminal.buffer.normal.length < 1) { if (Date.now() - startTime > timeout) { From bcf9268ae3c220398514164353f2901efee04b59 Mon Sep 17 00:00:00 2001 From: Charlie Drage Date: Wed, 23 Oct 2024 13:47:03 -0400 Subject: [PATCH 3/3] Update packages/backend/src/vm-manager.ts Co-authored-by: Florent BENOIT --- packages/backend/src/vm-manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/vm-manager.ts b/packages/backend/src/vm-manager.ts index 086cb3d3..8ecc9e5e 100644 --- a/packages/backend/src/vm-manager.ts +++ b/packages/backend/src/vm-manager.ts @@ -192,7 +192,7 @@ export function createVMManager(build: BootcBuildInfo): VMManagerBase { export async function stopCurrentVM(): Promise { try { await extensionApi.process.exec('sh', ['-c', `kill -9 \`cat ${pidFile}\``]); - } catch (e) { + } catch (e: unknown) { if (e instanceof Error && 'stderr' in e && typeof e.stderr === 'string' && e.stderr.includes('No such process')) { return; }