package daemon import ( "context" "log/slog" "time" ws "banger/internal/daemon/workspace" "banger/internal/model" "banger/internal/paths" "banger/internal/store" "banger/internal/system" ) // WorkspaceService owns workspace.prepare / workspace.export plus the // ssh-key + git-identity sync that runs as part of VM start's // prepare_work_disk capability hook. The workspaceLocks set lives here // so its scope (serialise concurrent tar imports on the same VM) is // obvious at the field definition. // // The inspect/import test seams are per-service fields so tests inject // fakes without mutating package-level state. type WorkspaceService struct { runner system.CommandRunner logger *slog.Logger config model.DaemonConfig layout paths.Layout store *store.Store // workspaceLocks serialises concurrent workspace.prepare / // workspace.export on the same VM. Separate from vmLocks so slow // guest I/O doesn't block lifecycle ops. workspaceLocks vmLockSet // Peer-service access via narrow function-typed dependencies. // WorkspaceService doesn't hold pointers to the full VMService or // HostNetwork; it only sees the exact operations it needs. vmResolver func(ctx context.Context, idOrName string) (model.VMRecord, error) aliveChecker func(vm model.VMRecord) bool waitGuestSSH func(ctx context.Context, address string, interval time.Duration) error dialGuest func(ctx context.Context, address string) (guestSSHClient, error) imageResolver func(ctx context.Context, idOrName string) (model.Image, error) imageWorkSeed func(ctx context.Context, image model.Image, fingerprint string) error withVMLockByRef func(ctx context.Context, idOrName string, fn func(model.VMRecord) (model.VMRecord, error)) (model.VMRecord, error) beginOperation func(name string, attrs ...any) *operationLog // Test seams. workspaceInspectRepo func(ctx context.Context, sourcePath, branchName, fromRef string, includeUntracked bool) (ws.RepoSpec, error) workspaceImport func(ctx context.Context, client ws.GuestClient, spec ws.RepoSpec, guestPath string, mode model.WorkspacePrepareMode) error } type workspaceServiceDeps struct { runner system.CommandRunner logger *slog.Logger config model.DaemonConfig layout paths.Layout store *store.Store vmResolver func(ctx context.Context, idOrName string) (model.VMRecord, error) aliveChecker func(vm model.VMRecord) bool waitGuestSSH func(ctx context.Context, address string, interval time.Duration) error dialGuest func(ctx context.Context, address string) (guestSSHClient, error) imageResolver func(ctx context.Context, idOrName string) (model.Image, error) imageWorkSeed func(ctx context.Context, image model.Image, fingerprint string) error withVMLockByRef func(ctx context.Context, idOrName string, fn func(model.VMRecord) (model.VMRecord, error)) (model.VMRecord, error) beginOperation func(name string, attrs ...any) *operationLog } func newWorkspaceService(deps workspaceServiceDeps) *WorkspaceService { return &WorkspaceService{ runner: deps.runner, logger: deps.logger, config: deps.config, layout: deps.layout, store: deps.store, vmResolver: deps.vmResolver, aliveChecker: deps.aliveChecker, waitGuestSSH: deps.waitGuestSSH, dialGuest: deps.dialGuest, imageResolver: deps.imageResolver, imageWorkSeed: deps.imageWorkSeed, withVMLockByRef: deps.withVMLockByRef, beginOperation: deps.beginOperation, } }