package daemon import ( "bufio" "context" "encoding/json" "errors" "fmt" "net" "os" "path/filepath" "strings" "sync" "time" "banger/internal/api" "banger/internal/config" "banger/internal/model" "banger/internal/paths" "banger/internal/rpc" "banger/internal/store" "banger/internal/system" ) type Daemon struct { layout paths.Layout config model.DaemonConfig store *store.Store runner system.CommandRunner mu sync.Mutex closing chan struct{} once sync.Once pid int listener net.Listener } func Open(ctx context.Context) (*Daemon, error) { layout, err := paths.Resolve() if err != nil { return nil, err } if err := paths.Ensure(layout); err != nil { return nil, err } cfg, err := config.Load(layout) if err != nil { return nil, err } db, err := store.Open(layout.DBPath) if err != nil { return nil, err } d := &Daemon{ layout: layout, config: cfg, store: db, runner: system.NewRunner(), closing: make(chan struct{}), pid: os.Getpid(), } if err := d.ensureDefaultImage(ctx); err != nil { return nil, err } if err := d.reconcile(ctx); err != nil { return nil, err } return d, nil } func (d *Daemon) Close() error { var err error d.once.Do(func() { close(d.closing) if d.listener != nil { _ = d.listener.Close() } err = d.store.Close() }) return err } func (d *Daemon) Serve(ctx context.Context) error { _ = os.Remove(d.layout.SocketPath) listener, err := net.Listen("unix", d.layout.SocketPath) if err != nil { return err } d.listener = listener defer listener.Close() defer os.Remove(d.layout.SocketPath) if err := os.Chmod(d.layout.SocketPath, 0o600); err != nil { return err } go d.backgroundLoop() for { conn, err := listener.Accept() if err != nil { select { case <-ctx.Done(): return nil case <-d.closing: return nil default: } if ne, ok := err.(net.Error); ok && ne.Temporary() { time.Sleep(100 * time.Millisecond) continue } return err } go d.handleConn(conn) } } func (d *Daemon) handleConn(conn net.Conn) { defer conn.Close() var req rpc.Request if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&req); err != nil { _ = json.NewEncoder(conn).Encode(rpc.NewError("bad_request", err.Error())) return } resp := d.dispatch(req) _ = json.NewEncoder(conn).Encode(resp) } func (d *Daemon) dispatch(req rpc.Request) rpc.Response { if req.Version != rpc.Version { return rpc.NewError("bad_version", fmt.Sprintf("unsupported version %d", req.Version)) } ctx := context.Background() switch req.Method { case "ping": result, _ := rpc.NewResult(api.PingResult{Status: "ok", PID: d.pid}) return result case "shutdown": go d.Close() result, _ := rpc.NewResult(api.ShutdownResult{Status: "stopping"}) return result case "vm.create": params, err := rpc.DecodeParams[api.VMCreateParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.CreateVM(ctx, params) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.list": vms, err := d.store.ListVMs(ctx) return marshalResultOrError(api.VMListResult{VMs: vms}, err) case "vm.show": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.FindVM(ctx, params.IDOrName) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.start": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.StartVM(ctx, params.IDOrName) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.stop": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.StopVM(ctx, params.IDOrName) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.restart": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.RestartVM(ctx, params.IDOrName) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.delete": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.DeleteVM(ctx, params.IDOrName) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.set": params, err := rpc.DecodeParams[api.VMSetParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.SetVM(ctx, params) return marshalResultOrError(api.VMShowResult{VM: vm}, err) case "vm.stats": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, stats, err := d.GetVMStats(ctx, params.IDOrName) return marshalResultOrError(api.VMStatsResult{VM: vm, Stats: stats}, err) case "vm.logs": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.FindVM(ctx, params.IDOrName) if err != nil { return rpc.NewError("not_found", err.Error()) } return marshalResultOrError(api.VMLogsResult{LogPath: vm.Runtime.LogPath}, nil) case "vm.ssh": params, err := rpc.DecodeParams[api.VMRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } vm, err := d.TouchVM(ctx, params.IDOrName) if err != nil { return rpc.NewError("not_found", err.Error()) } if vm.State != model.VMStateRunning || !system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) { return rpc.NewError("not_running", fmt.Sprintf("vm %s is not running", vm.Name)) } return marshalResultOrError(api.VMSSHResult{Name: vm.Name, GuestIP: vm.Runtime.GuestIP}, nil) case "image.list": images, err := d.store.ListImages(ctx) return marshalResultOrError(api.ImageListResult{Images: images}, err) case "image.show": params, err := rpc.DecodeParams[api.ImageRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } image, err := d.FindImage(ctx, params.IDOrName) return marshalResultOrError(api.ImageShowResult{Image: image}, err) case "image.build": params, err := rpc.DecodeParams[api.ImageBuildParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } image, err := d.BuildImage(ctx, params) return marshalResultOrError(api.ImageShowResult{Image: image}, err) case "image.delete": params, err := rpc.DecodeParams[api.ImageRefParams](req) if err != nil { return rpc.NewError("bad_request", err.Error()) } image, err := d.DeleteImage(ctx, params.IDOrName) return marshalResultOrError(api.ImageShowResult{Image: image}, err) default: return rpc.NewError("unknown_method", req.Method) } } func (d *Daemon) backgroundLoop() { statsTicker := time.NewTicker(d.config.StatsPollInterval) staleTicker := time.NewTicker(model.DefaultStaleSweepInterval) defer statsTicker.Stop() defer staleTicker.Stop() for { select { case <-d.closing: return case <-statsTicker.C: _ = d.pollStats(context.Background()) case <-staleTicker.C: _ = d.stopStaleVMs(context.Background()) } } } func (d *Daemon) ensureDefaultImage(ctx context.Context) error { if d.config.DefaultImageName == "" || d.config.RepoRoot == "" { return nil } if _, err := d.store.GetImageByName(ctx, d.config.DefaultImageName); err == nil { return nil } rootfs := filepath.Join(d.config.RepoRoot, "rootfs-docker.ext4") kernel := d.config.DefaultKernel initrd := d.config.DefaultInitrd if !exists(rootfs) || !exists(kernel) { return nil } id, err := model.NewID() if err != nil { return err } now := model.Now() image := model.Image{ ID: id, Name: d.config.DefaultImageName, Managed: false, RootfsPath: rootfs, KernelPath: kernel, InitrdPath: initrd, ModulesDir: d.config.DefaultModulesDir, PackagesPath: d.config.DefaultPackagesFile, Docker: strings.Contains(filepath.Base(rootfs), "docker"), CreatedAt: now, UpdatedAt: now, } return d.store.UpsertImage(ctx, image) } func (d *Daemon) reconcile(ctx context.Context) error { vms, err := d.store.ListVMs(ctx) if err != nil { return err } for _, vm := range vms { if vm.State != model.VMStateRunning { continue } if system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) { continue } _ = d.cleanupRuntime(ctx, vm, true) vm.State = model.VMStateStopped vm.Runtime.State = model.VMStateStopped vm.Runtime.PID = 0 vm.Runtime.TapDevice = "" vm.Runtime.APISockPath = "" vm.Runtime.BaseLoop = "" vm.Runtime.COWLoop = "" vm.Runtime.DMName = "" vm.Runtime.DMDev = "" vm.UpdatedAt = model.Now() if err := d.store.UpsertVM(ctx, vm); err != nil { return err } } return nil } func (d *Daemon) FindVM(ctx context.Context, idOrName string) (model.VMRecord, error) { if idOrName == "" { return model.VMRecord{}, errors.New("vm id or name is required") } if vm, err := d.store.GetVM(ctx, idOrName); err == nil { return vm, nil } vms, err := d.store.ListVMs(ctx) if err != nil { return model.VMRecord{}, err } matchCount := 0 var match model.VMRecord for _, vm := range vms { if strings.HasPrefix(vm.ID, idOrName) || strings.HasPrefix(vm.Name, idOrName) { match = vm matchCount++ } } if matchCount == 1 { return match, nil } if matchCount > 1 { return model.VMRecord{}, fmt.Errorf("multiple VMs match %q", idOrName) } return model.VMRecord{}, fmt.Errorf("vm %q not found", idOrName) } func (d *Daemon) FindImage(ctx context.Context, idOrName string) (model.Image, error) { if idOrName == "" { return model.Image{}, errors.New("image id or name is required") } if image, err := d.store.GetImageByName(ctx, idOrName); err == nil { return image, nil } if image, err := d.store.GetImageByID(ctx, idOrName); err == nil { return image, nil } images, err := d.store.ListImages(ctx) if err != nil { return model.Image{}, err } matchCount := 0 var match model.Image for _, image := range images { if strings.HasPrefix(image.ID, idOrName) || strings.HasPrefix(image.Name, idOrName) { match = image matchCount++ } } if matchCount == 1 { return match, nil } if matchCount > 1 { return model.Image{}, fmt.Errorf("multiple images match %q", idOrName) } return model.Image{}, fmt.Errorf("image %q not found", idOrName) } func (d *Daemon) TouchVM(ctx context.Context, idOrName string) (model.VMRecord, error) { d.mu.Lock() defer d.mu.Unlock() vm, err := d.FindVM(ctx, idOrName) if err != nil { return model.VMRecord{}, err } system.TouchNow(&vm) if err := d.store.UpsertVM(ctx, vm); err != nil { return model.VMRecord{}, err } return vm, nil } func marshalResultOrError(v any, err error) rpc.Response { if err != nil { return rpc.NewError("operation_failed", err.Error()) } resp, marshalErr := rpc.NewResult(v) if marshalErr != nil { return rpc.NewError("marshal_failed", marshalErr.Error()) } return resp } func exists(path string) bool { _, err := os.Stat(path) return err == nil }