A convenient CLI tool to quickly spin up DragonflyBSD virtual machines using QEMU with sensible defaults.

Refactor stop command to improve error handling and control flow using Effect

+80 -80
+80 -80
src/subcommands/stop.ts
··· 1 import _ from "@es-toolkit/es-toolkit/compat"; 2 import chalk from "chalk"; 3 - import { Effect, pipe } from "effect"; 4 import type { VirtualMachine } from "../db.ts"; 5 - import { getInstanceStateOrFail, updateInstanceState } from "../state.ts"; 6 7 - const logStoppingMessage = (vm: VirtualMachine) => 8 Effect.sync(() => { 9 console.log( 10 `Stopping virtual machine ${chalk.greenBright(vm.name)} (ID: ${ ··· 13 ); 14 }); 15 16 - const buildKillCommand = (vm: VirtualMachine) => ({ 17 - command: vm.bridge ? "sudo" : "kill", 18 - args: [ 19 - ..._.compact([vm.bridge && "kill"]), 20 - "-TERM", 21 - vm.pid.toString(), 22 - ], 23 - }); 24 - 25 - const executeKillCommand = (vm: VirtualMachine) => { 26 - const { command, args } = buildKillCommand(vm); 27 - 28 - return Effect.tryPromise({ 29 try: async () => { 30 - const cmd = new Deno.Command(command, { 31 - args, 32 stdin: "inherit", 33 stdout: "inherit", 34 stderr: "inherit", 35 }); 36 37 - return await cmd.spawn().status; 38 }, 39 - catch: (cause) => new Error(`Failed to execute kill command: ${cause}`), 40 - }); 41 - }; 42 - 43 - const validateKillResult = (vm: VirtualMachine, status: Deno.CommandStatus) => 44 - Effect.sync(() => { 45 - if (!status.success) { 46 - throw new Error(`Failed to stop virtual machine ${vm.name}`); 47 - } 48 - return vm; 49 - }); 50 51 - const markInstanceAsStopped = (vm: VirtualMachine) => 52 - updateInstanceState(vm.name, "STOPPED"); 53 54 - const logStopSuccess = (vm: VirtualMachine) => 55 Effect.sync(() => { 56 console.log(`Virtual machine ${chalk.greenBright(vm.name)} stopped.`); 57 }); 58 59 - const stopVirtualMachineProcess = (vm: VirtualMachine) => 60 - pipe( 61 - executeKillCommand(vm), 62 - Effect.flatMap((status) => validateKillResult(vm, status)), 63 - Effect.flatMap(() => markInstanceAsStopped(vm)), 64 - Effect.flatMap(() => logStopSuccess(vm)), 65 - ); 66 67 - const stopVirtualMachine = (name: string) => 68 pipe( 69 - getInstanceStateOrFail(name), 70 - Effect.flatMap((vm) => 71 - pipe( 72 - logStoppingMessage(vm), 73 - Effect.flatMap(() => stopVirtualMachineProcess(vm)), 74 - ) 75 - ), 76 ); 77 78 export default async function (name: string) { 79 - const program = pipe( 80 - stopVirtualMachine(name), 81 - Effect.catchTags({ 82 - InstanceNotFoundError: (_error) => 83 - Effect.sync(() => { 84 - console.error( 85 - `Virtual machine with name or ID ${ 86 - chalk.greenBright(name) 87 - } not found.`, 88 - ); 89 - Deno.exit(1); 90 - }), 91 - DatabaseQueryError: (error) => 92 - Effect.sync(() => { 93 - console.error(`Database error: ${error.message}`); 94 - Deno.exit(1); 95 - }), 96 - DatabaseUpdateError: (error) => 97 - Effect.sync(() => { 98 - console.error(`Failed to update database: ${error.message}`); 99 - Deno.exit(1); 100 - }), 101 - }), 102 - Effect.catchAll((error) => 103 - Effect.sync(() => { 104 - console.error(`Error: ${error.message}`); 105 - Deno.exit(1); 106 - }) 107 - ), 108 - ); 109 - 110 - await Effect.runPromise(program); 111 }
··· 1 import _ from "@es-toolkit/es-toolkit/compat"; 2 import chalk from "chalk"; 3 + import { Data, Effect, pipe } from "effect"; 4 import type { VirtualMachine } from "../db.ts"; 5 + import { getInstanceState, updateInstanceState } from "../state.ts"; 6 7 + class VmNotFoundError extends Data.TaggedError("VmNotFoundError")<{ 8 + name: string; 9 + }> {} 10 + 11 + class StopCommandError extends Data.TaggedError("StopCommandError")<{ 12 + vmName: string; 13 + exitCode: number; 14 + }> {} 15 + 16 + class CommandError extends Data.TaggedError("CommandError")<{ 17 + cause?: unknown; 18 + }> {} 19 + 20 + const findVm = (name: string) => 21 + pipe( 22 + getInstanceState(name), 23 + Effect.flatMap((vm) => 24 + vm ? Effect.succeed(vm) : Effect.fail(new VmNotFoundError({ name })) 25 + ), 26 + ); 27 + 28 + const logStopping = (vm: VirtualMachine) => 29 Effect.sync(() => { 30 console.log( 31 `Stopping virtual machine ${chalk.greenBright(vm.name)} (ID: ${ ··· 34 ); 35 }); 36 37 + const killProcess = (vm: VirtualMachine) => 38 + Effect.tryPromise({ 39 try: async () => { 40 + const cmd = new Deno.Command(vm.bridge ? "sudo" : "kill", { 41 + args: [ 42 + ..._.compact([vm.bridge && "kill"]), 43 + "-TERM", 44 + vm.pid.toString(), 45 + ], 46 stdin: "inherit", 47 stdout: "inherit", 48 stderr: "inherit", 49 }); 50 51 + const status = await cmd.spawn().status; 52 + return { vm, status }; 53 }, 54 + catch: (error) => new CommandError({ cause: error }), 55 + }).pipe( 56 + Effect.flatMap(({ vm, status }) => 57 + status.success ? Effect.succeed(vm) : Effect.fail( 58 + new StopCommandError({ 59 + vmName: vm.name, 60 + exitCode: status.code || 1, 61 + }), 62 + ) 63 + ), 64 + ); 65 66 + const updateToStopped = (vm: VirtualMachine) => 67 + pipe( 68 + updateInstanceState(vm.name, "STOPPED"), 69 + Effect.map(() => vm), 70 + ); 71 72 + const logSuccess = (vm: VirtualMachine) => 73 Effect.sync(() => { 74 console.log(`Virtual machine ${chalk.greenBright(vm.name)} stopped.`); 75 }); 76 77 + const handleError = ( 78 + error: VmNotFoundError | StopCommandError | CommandError | Error, 79 + ) => 80 + Effect.sync(() => { 81 + if (error instanceof VmNotFoundError) { 82 + console.error( 83 + `Virtual machine with name or ID ${ 84 + chalk.greenBright(error.name) 85 + } not found.`, 86 + ); 87 + Deno.exit(1); 88 + } else if (error instanceof StopCommandError) { 89 + console.error( 90 + `Failed to stop virtual machine ${chalk.greenBright(error.vmName)}.`, 91 + ); 92 + Deno.exit(error.exitCode); 93 + } else { 94 + console.error(`An error occurred: ${error}`); 95 + Deno.exit(1); 96 + } 97 + }); 98 99 + const stopEffect = (name: string) => 100 pipe( 101 + findVm(name), 102 + Effect.tap(logStopping), 103 + Effect.flatMap(killProcess), 104 + Effect.flatMap(updateToStopped), 105 + Effect.tap(logSuccess), 106 + Effect.catchAll(handleError), 107 ); 108 109 export default async function (name: string) { 110 + await Effect.runPromise(stopEffect(name)); 111 }