diff --git a/main.ts b/main.ts index 7f5f1ee..9b11e6f 100755 --- a/main.ts +++ b/main.ts @@ -104,32 +104,35 @@ if (import.meta.main) { "dflybsd-up inspect my-vm", ) .action(async (options: Options, input?: string) => { - const resolvedInput = handleInput(input); - let isoPath: string | null = resolvedInput; + const program = Effect.gen(function* () { + const resolvedInput = handleInput(input); + let isoPath: string | null = resolvedInput; - if ( - resolvedInput.startsWith("https://") || - resolvedInput.startsWith("http://") - ) { - isoPath = await Effect.runPromise(downloadIso(resolvedInput, options)); - } + if ( + resolvedInput.startsWith("https://") || + resolvedInput.startsWith("http://") + ) { + isoPath = yield* downloadIso(resolvedInput, options); + } - if (options.image) { - await Effect.runPromise(createDriveImageIfNeeded(options)); - } + if (options.image) { + yield* createDriveImageIfNeeded(options); + } - if ( - !input && options.image && - !await Effect.runPromise(emptyDiskImage(options.image)) - ) { - isoPath = null; - } + if ( + !input && options.image && + !(yield* emptyDiskImage(options.image)) + ) { + isoPath = null; + } - if (options.bridge) { - await createBridgeNetworkIfNeeded(options.bridge); - } + if (options.bridge) { + yield* createBridgeNetworkIfNeeded(options.bridge); + } + yield* runQemu(isoPath, options); + }); - await Effect.runPromise(runQemu(isoPath, options)); + await Effect.runPromise(program); }) .command("ps", "List all virtual machines") .option("--all, -a", "Show all virtual machines, including stopped ones") diff --git a/src/network.ts b/src/network.ts index a5e5f7e..77c9044 100644 --- a/src/network.ts +++ b/src/network.ts @@ -1,120 +1,138 @@ import chalk from "chalk"; +import { Data, Effect } from "effect"; -export async function setupQemuBridge(bridgeName: string): Promise { - const bridgeConfPath = "/etc/qemu/bridge.conf"; - const bridgeConfContent = await Deno.readTextFile(bridgeConfPath).catch( - () => "", - ); - if (bridgeConfContent.includes(`allow ${bridgeName}`)) { - console.log( - chalk.greenBright( - `QEMU bridge configuration for ${bridgeName} already exists.`, - ), - ); - return; - } - - console.log( - chalk.blueBright( - `Adding QEMU bridge configuration for ${bridgeName}...`, - ), - ); - - const cmd = new Deno.Command("sudo", { - args: [ - "sh", - "-c", - `mkdir -p /etc/qemu && echo "allow ${bridgeName}" >> ${bridgeConfPath}`, - ], - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", +export class NetworkError extends Data.TaggedError("NetworkError")<{ + cause?: unknown; +}> {} + +export class BridgeSetupError extends Data.TaggedError("BridgeSetupError")<{ + cause?: unknown; +}> {} + +export const setupQemuBridge = (bridgeName: string) => + Effect.tryPromise({ + try: async () => { + const bridgeConfPath = "/etc/qemu/bridge.conf"; + const bridgeConfContent = await Deno.readTextFile(bridgeConfPath).catch( + () => "", + ); + if (bridgeConfContent.includes(`allow ${bridgeName}`)) { + console.log( + chalk.greenBright( + `QEMU bridge configuration for ${bridgeName} already exists.`, + ), + ); + return; + } + + console.log( + chalk.blueBright( + `Adding QEMU bridge configuration for ${bridgeName}...`, + ), + ); + + const cmd = new Deno.Command("sudo", { + args: [ + "sh", + "-c", + `mkdir -p /etc/qemu && echo "allow ${bridgeName}" >> ${bridgeConfPath}`, + ], + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + const status = await cmd.spawn().status; + + if (!status.success) { + console.error( + chalk.redBright( + `Failed to add QEMU bridge configuration for ${bridgeName}.`, + ), + ); + Deno.exit(status.code); + } + + console.log( + chalk.greenBright( + `QEMU bridge configuration for ${bridgeName} added successfully.`, + ), + ); + }, + catch: (error) => new BridgeSetupError({ cause: error }), }); - const status = await cmd.spawn().status; - - if (!status.success) { - console.error( - chalk.redBright( - `Failed to add QEMU bridge configuration for ${bridgeName}.`, - ), - ); - Deno.exit(status.code); - } - - console.log( - chalk.greenBright( - `QEMU bridge configuration for ${bridgeName} added successfully.`, - ), - ); -} - -export async function createBridgeNetworkIfNeeded( + +export const createBridgeNetworkIfNeeded = ( bridgeName: string, -): Promise { - const bridgeExistsCmd = new Deno.Command("ip", { - args: ["link", "show", bridgeName], - stdout: "null", - stderr: "null", - }); +) => + Effect.tryPromise({ + try: async () => { + const bridgeExistsCmd = new Deno.Command("ip", { + args: ["link", "show", bridgeName], + stdout: "null", + stderr: "null", + }); - const bridgeExistsStatus = await bridgeExistsCmd.spawn().status; - if (bridgeExistsStatus.success) { - console.log( - chalk.greenBright(`Network bridge ${bridgeName} already exists.`), - ); - await setupQemuBridge(bridgeName); - return; - } - - console.log(chalk.blueBright(`Creating network bridge ${bridgeName}...`)); - const createBridgeCmd = new Deno.Command("sudo", { - args: ["ip", "link", "add", bridgeName, "type", "bridge"], - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - }); + const bridgeExistsStatus = await bridgeExistsCmd.spawn().status; + if (bridgeExistsStatus.success) { + console.log( + chalk.greenBright(`Network bridge ${bridgeName} already exists.`), + ); + await setupQemuBridge(bridgeName); + return; + } - let status = await createBridgeCmd.spawn().status; - if (!status.success) { - console.error( - chalk.redBright(`Failed to create network bridge ${bridgeName}.`), - ); - Deno.exit(status.code); - } - - const bringUpBridgeCmd = new Deno.Command("sudo", { - args: ["ip", "link", "set", "dev", bridgeName, "up"], - stdin: "inherit", - stdout: "inherit", - stderr: "inherit", - }); - status = await bringUpBridgeCmd.spawn().status; - if (!status.success) { - console.error( - chalk.redBright(`Failed to bring up network bridge ${bridgeName}.`), - ); - Deno.exit(status.code); - } - - console.log( - chalk.greenBright(`Network bridge ${bridgeName} created and up.`), - ); - - await setupQemuBridge(bridgeName); -} - -export function generateRandomMacAddress(): string { - const hexDigits = "0123456789ABCDEF"; - let macAddress = "52:54:00"; - - for (let i = 0; i < 3; i++) { - macAddress += ":"; - for (let j = 0; j < 2; j++) { - macAddress += hexDigits.charAt( - Math.floor(Math.random() * hexDigits.length), + console.log(chalk.blueBright(`Creating network bridge ${bridgeName}...`)); + const createBridgeCmd = new Deno.Command("sudo", { + args: ["ip", "link", "add", bridgeName, "type", "bridge"], + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + + let status = await createBridgeCmd.spawn().status; + if (!status.success) { + console.error( + chalk.redBright(`Failed to create network bridge ${bridgeName}.`), + ); + Deno.exit(status.code); + } + + const bringUpBridgeCmd = new Deno.Command("sudo", { + args: ["ip", "link", "set", "dev", bridgeName, "up"], + stdin: "inherit", + stdout: "inherit", + stderr: "inherit", + }); + status = await bringUpBridgeCmd.spawn().status; + if (!status.success) { + console.error( + chalk.redBright(`Failed to bring up network bridge ${bridgeName}.`), + ); + Deno.exit(status.code); + } + + console.log( + chalk.greenBright(`Network bridge ${bridgeName} created and up.`), ); + + await setupQemuBridge(bridgeName); + }, + catch: (error) => new NetworkError({ cause: error }), + }); + +export const generateRandomMacAddress = () => + Effect.sync(() => { + const hexDigits = "0123456789ABCDEF"; + let macAddress = "52:54:00"; + + for (let i = 0; i < 3; i++) { + macAddress += ":"; + for (let j = 0; j < 2; j++) { + macAddress += hexDigits.charAt( + Math.floor(Math.random() * hexDigits.length), + ); + } } - } - return macAddress; -} + return macAddress; + });