Moves the stateless parts of the workspace subsystem into internal/daemon/workspace: - RepoSpec struct + InspectRepo for host-side git inspection - ImportRepoToGuest (taking a minimal GuestClient interface) with the full-copy and metadata-only / shallow-overlay paths - FinalizeScript, PrepareRepoCopy, ResolveSourcePath - ListSubmodules, ListOverlayPaths, ParsePrepareMode - Git helpers (GitOutput, GitTrimmedOutput, GitResolvedConfigValue, ParseNullSeparatedOutput, RunHostCommand, GitFileURL) and the HostCommandOutputFunc test seam - ShallowFetchDepth const The subpackage imports internal/daemon/session for ShellQuote and FormatStepError so both workspace and session pure helpers live in their own subpackages with a clean session→workspace direction of use. daemon/workspace.go shrinks from 481 → 156 LOC, keeping just the three orchestrator methods (Export, Prepare, prepareLocked) that still touch d.store, d.FindVM, d.dialGuest, d.waitForGuestSSH, and the VM lock set. guestSessionHostCommandOutputFunc is removed from guest_sessions.go (its only caller was workspace.go; the new package has its own copy). All tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.7 KiB
Go
142 lines
4.7 KiB
Go
package daemon
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"banger/internal/daemon/session"
|
|
"banger/internal/guest"
|
|
"banger/internal/model"
|
|
"banger/internal/system"
|
|
)
|
|
|
|
type guestSSHClient interface {
|
|
Close() error
|
|
RunScript(context.Context, string, io.Writer) error
|
|
RunScriptOutput(context.Context, string) ([]byte, error)
|
|
UploadFile(context.Context, string, os.FileMode, []byte, io.Writer) error
|
|
StreamTar(context.Context, string, string, io.Writer) error
|
|
StreamTarEntries(context.Context, string, []string, string, io.Writer) error
|
|
}
|
|
|
|
func (d *Daemon) waitForGuestSSH(ctx context.Context, address string, interval time.Duration) error {
|
|
if d != nil && d.guestWaitForSSH != nil {
|
|
return d.guestWaitForSSH(ctx, address, d.config.SSHKeyPath, interval)
|
|
}
|
|
return guest.WaitForSSH(ctx, address, d.config.SSHKeyPath, interval)
|
|
}
|
|
|
|
func (d *Daemon) dialGuest(ctx context.Context, address string) (guestSSHClient, error) {
|
|
if d != nil && d.guestDial != nil {
|
|
return d.guestDial(ctx, address, d.config.SSHKeyPath)
|
|
}
|
|
return guest.Dial(ctx, address, d.config.SSHKeyPath)
|
|
}
|
|
|
|
func (d *Daemon) waitForGuestSessionReadyHook(ctx context.Context, vm model.VMRecord, s model.GuestSession) (model.GuestSession, error) {
|
|
if d != nil && d.waitForGuestSessionReady != nil {
|
|
return d.waitForGuestSessionReady(ctx, vm, s)
|
|
}
|
|
return d.waitForGuestSessionReadyDefault(ctx, vm, s)
|
|
}
|
|
|
|
func (d *Daemon) waitForGuestSessionReadyDefault(ctx context.Context, vm model.VMRecord, s model.GuestSession) (model.GuestSession, error) {
|
|
for {
|
|
updated, err := d.refreshGuestSession(ctx, vm, s)
|
|
if err == nil {
|
|
s = updated
|
|
if s.GuestPID != 0 || s.ExitCode != nil || s.Status == model.GuestSessionStatusRunning || s.Status == model.GuestSessionStatusFailed || s.Status == model.GuestSessionStatusExited {
|
|
return s, nil
|
|
}
|
|
}
|
|
select {
|
|
case <-ctx.Done():
|
|
return s, ctx.Err()
|
|
case <-time.After(100 * time.Millisecond):
|
|
}
|
|
}
|
|
}
|
|
|
|
func (d *Daemon) refreshGuestSession(ctx context.Context, vm model.VMRecord, s model.GuestSession) (model.GuestSession, error) {
|
|
if s.Status != model.GuestSessionStatusStarting && s.Status != model.GuestSessionStatusRunning && s.Status != model.GuestSessionStatusStopping {
|
|
return s, nil
|
|
}
|
|
snapshot, err := d.inspectGuestSessionState(ctx, vm, s)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
original := s
|
|
session.ApplyStateSnapshot(&s, snapshot, vm.State == model.VMStateRunning && system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath))
|
|
if session.StateChanged(original, s) {
|
|
s.UpdatedAt = model.Now()
|
|
if err := d.store.UpsertGuestSession(ctx, s); err != nil {
|
|
return s, err
|
|
}
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (d *Daemon) inspectGuestSessionState(ctx context.Context, vm model.VMRecord, s model.GuestSession) (session.StateSnapshot, error) {
|
|
if vm.State == model.VMStateRunning && system.ProcessRunning(vm.Runtime.PID, vm.Runtime.APISockPath) {
|
|
client, err := guest.Dial(ctx, net.JoinHostPort(vm.Runtime.GuestIP, "22"), d.config.SSHKeyPath)
|
|
if err != nil {
|
|
return session.StateSnapshot{}, err
|
|
}
|
|
defer client.Close()
|
|
var output bytes.Buffer
|
|
if err := client.RunScript(ctx, session.InspectScript(s.ID), &output); err != nil {
|
|
return session.StateSnapshot{}, session.FormatStepError("inspect guest session state", err, output.String())
|
|
}
|
|
return session.ParseState(output.String())
|
|
}
|
|
return d.inspectGuestSessionStateFromWorkDisk(ctx, vm, s.ID)
|
|
}
|
|
|
|
func (d *Daemon) inspectGuestSessionStateFromWorkDisk(ctx context.Context, vm model.VMRecord, sessionID string) (session.StateSnapshot, error) {
|
|
runner := d.runner
|
|
if runner == nil {
|
|
runner = system.NewRunner()
|
|
}
|
|
workMount, cleanup, err := system.MountTempDir(ctx, runner, vm.Runtime.WorkDiskPath, false)
|
|
if err != nil {
|
|
return session.StateSnapshot{}, err
|
|
}
|
|
defer cleanup()
|
|
stateDir := filepath.Join(workMount, session.RelativeStateDir(sessionID))
|
|
return session.InspectStateFromDir(stateDir)
|
|
}
|
|
|
|
func (d *Daemon) findGuestSession(ctx context.Context, vmID, idOrName string) (model.GuestSession, error) {
|
|
if strings.TrimSpace(idOrName) == "" {
|
|
return model.GuestSession{}, errors.New("session id or name is required")
|
|
}
|
|
if s, err := d.store.GetGuestSession(ctx, vmID, idOrName); err == nil {
|
|
return s, nil
|
|
}
|
|
sessions, err := d.store.ListGuestSessionsByVM(ctx, vmID)
|
|
if err != nil {
|
|
return model.GuestSession{}, err
|
|
}
|
|
matches := make([]model.GuestSession, 0, 1)
|
|
for _, s := range sessions {
|
|
if strings.HasPrefix(s.ID, idOrName) || strings.HasPrefix(s.Name, idOrName) {
|
|
matches = append(matches, s)
|
|
}
|
|
}
|
|
switch len(matches) {
|
|
case 0:
|
|
return model.GuestSession{}, fmt.Errorf("session %q not found", idOrName)
|
|
case 1:
|
|
return matches[0], nil
|
|
default:
|
|
return model.GuestSession{}, fmt.Errorf("multiple sessions match %q", idOrName)
|
|
}
|
|
}
|