package daemon import ( "context" "fmt" "strings" "sync" "time" "banger/internal/api" "banger/internal/model" ) func (op *vmCreateOperationState) ID() string { return op.snapshot().ID } func (op *vmCreateOperationState) IsDone() bool { return op.snapshot().Done } func (op *vmCreateOperationState) UpdatedAt() time.Time { return op.snapshot().UpdatedAt } func (op *vmCreateOperationState) Cancel() { op.cancelOperation() } type vmCreateProgressKey struct{} type vmCreateOperationState struct { mu sync.Mutex cancel context.CancelFunc op api.VMCreateOperation } func newVMCreateOperationState() (*vmCreateOperationState, error) { id, err := model.NewID() if err != nil { return nil, err } now := model.Now() return &vmCreateOperationState{ op: api.VMCreateOperation{ ID: id, Stage: "queued", Detail: "waiting to start", StartedAt: now, UpdatedAt: now, }, }, nil } func withVMCreateProgress(ctx context.Context, op *vmCreateOperationState) context.Context { if op == nil { return ctx } return context.WithValue(ctx, vmCreateProgressKey{}, op) } func vmCreateProgressFromContext(ctx context.Context) *vmCreateOperationState { if ctx == nil { return nil } op, _ := ctx.Value(vmCreateProgressKey{}).(*vmCreateOperationState) return op } func vmCreateStage(ctx context.Context, stage, detail string) { if op := vmCreateProgressFromContext(ctx); op != nil { op.stage(stage, detail) } } func vmCreateBindVM(ctx context.Context, vm model.VMRecord) { if op := vmCreateProgressFromContext(ctx); op != nil { op.bindVM(vm) } } func (op *vmCreateOperationState) setCancel(cancel context.CancelFunc) { op.mu.Lock() defer op.mu.Unlock() op.cancel = cancel } func (op *vmCreateOperationState) bindVM(vm model.VMRecord) { op.mu.Lock() defer op.mu.Unlock() op.op.VMID = vm.ID op.op.VMName = vm.Name } func (op *vmCreateOperationState) stage(stage, detail string) { op.mu.Lock() defer op.mu.Unlock() stage = strings.TrimSpace(stage) detail = strings.TrimSpace(detail) if stage == "" { stage = op.op.Stage } if stage == op.op.Stage && detail == op.op.Detail { return } op.op.Stage = stage op.op.Detail = detail op.op.UpdatedAt = model.Now() } func (op *vmCreateOperationState) done(vm model.VMRecord) { op.mu.Lock() defer op.mu.Unlock() vmCopy := vm op.op.VMID = vm.ID op.op.VMName = vm.Name op.op.Stage = "ready" op.op.Detail = "vm is ready" op.op.Done = true op.op.Success = true op.op.Error = "" op.op.VM = &vmCopy op.op.UpdatedAt = model.Now() } func (op *vmCreateOperationState) fail(err error) { op.mu.Lock() defer op.mu.Unlock() op.op.Done = true op.op.Success = false if err != nil { op.op.Error = err.Error() } if strings.TrimSpace(op.op.Detail) == "" { op.op.Detail = "vm create failed" } op.op.UpdatedAt = model.Now() } func (op *vmCreateOperationState) snapshot() api.VMCreateOperation { op.mu.Lock() defer op.mu.Unlock() snapshot := op.op if snapshot.VM != nil { vmCopy := *snapshot.VM snapshot.VM = &vmCopy } return snapshot } func (op *vmCreateOperationState) cancelOperation() { op.mu.Lock() cancel := op.cancel op.mu.Unlock() if cancel != nil { cancel() } } func (d *Daemon) BeginVMCreate(_ context.Context, params api.VMCreateParams) (api.VMCreateOperation, error) { op, err := newVMCreateOperationState() if err != nil { return api.VMCreateOperation{}, err } createCtx, cancel := context.WithCancel(context.Background()) op.setCancel(cancel) d.createOps.Insert(op) go d.runVMCreateOperation(withVMCreateProgress(createCtx, op), op, params) return op.snapshot(), nil } func (d *Daemon) runVMCreateOperation(ctx context.Context, op *vmCreateOperationState, params api.VMCreateParams) { vm, err := d.CreateVM(ctx, params) if err != nil { op.fail(err) return } op.done(vm) } func (d *Daemon) VMCreateStatus(_ context.Context, id string) (api.VMCreateOperation, error) { op, ok := d.createOps.Get(strings.TrimSpace(id)) if !ok { return api.VMCreateOperation{}, fmt.Errorf("vm create operation not found: %s", id) } return op.snapshot(), nil } func (d *Daemon) CancelVMCreate(_ context.Context, id string) error { op, ok := d.createOps.Get(strings.TrimSpace(id)) if !ok { return fmt.Errorf("vm create operation not found: %s", id) } op.cancelOperation() return nil } func (d *Daemon) pruneVMCreateOperations(olderThan time.Time) { d.createOps.Prune(olderThan) }