Factor the service + capability wiring out of Daemon.Open() into
wireServices(d), an idempotent helper that constructs HostNetwork,
ImageService, WorkspaceService, and VMService from whatever
infrastructure (runner, store, config, layout, logger, closing) is
already set on d. Open() calls it once after filling the composition
root; tests that build &Daemon{...} literals call it to get a working
service graph, preinstalling stubs on the fields they want to fake.
Drops the four lazy-init getters on *Daemon — d.hostNet(),
d.imageSvc(), d.workspaceSvc(), d.vmSvc() — whose sole purpose was
keeping test literals working. Every production call site now reads
d.net / d.img / d.ws / d.vm directly; the services are guaranteed
non-nil once Open returns. No behavior change.
Mechanical: all existing `d.xxxSvc()` calls (production + tests)
rewritten to field access; each `d := &Daemon{...}` in tests gets a
trailing wireServices(d) so the literal + wiring are side-by-side.
Tests that override a pre-built service (e.g. d.img = &ImageService{
bundleFetch: stub}) now set the override before wireServices so the
replacement propagates into VMService's peer pointer.
Also nil-guards HostNetwork.stopVMDNS and d.store in Close() so
partially-initialised daemons (pre-reconcile open failure) still
tear down cleanly — same contract the old lazy getters provided.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
85 lines
3.5 KiB
Go
85 lines
3.5 KiB
Go
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) (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,
|
|
}
|
|
}
|