Extract session subpackage with pure guest-session helpers

Moves the stateless parts of the guest-session subsystem into
internal/daemon/session:

- consts (BackendSSH, attach/transport kinds, StateRoot, LogTailLineDefault)
- StateSnapshot plus ParseState / InspectStateFromDir / ApplyStateSnapshot / StateChanged
- 10 on-guest path helpers (StateDir, StdoutLogPath, StdinPipePath, …)
- 3 bash script generators (Script, InspectScript, SignalScript)
- small utilities (ShellQuote, ExitCode, CloneStringMap, TailFileContent,
  ProcessAlive + syscallKill test seam, FormatStepError)
- launch helpers (DefaultName, DefaultCWD, FailLaunch,
  NormalizeRequiredCommands, CWDPreflightScript, CommandPreflightScript,
  AttachInputCommand, AttachTailCommand, EnvLines)

Callers inside the daemon package import the new package under the
alias "sess" to avoid colliding with the local `session model.GuestSession`
variables threaded through the orchestrator code. guest_sessions.go
shrinks from 616 → 156 LOC; session_stream.go, session_attach.go,
session_lifecycle.go, workspace.go, and guest_sessions_test.go rewire to
the exported names.

The orchestrator methods (StartGuestSession, BeginGuestSessionAttach,
SendToGuestSession, GuestSessionLogs, refresh/inspect, sessionRegistry,
guestSessionController) stay on *Daemon. Full Manager-style extraction
would need prerequisite phases (operation protocol, workdisk helpers),
mirroring Phase 4a's trade-off.

All tests green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-04-15 16:33:12 -03:00
parent c13c8b11af
commit 37e02b1576
No known key found for this signature in database
GPG key ID: 33112E6833C34679
8 changed files with 612 additions and 566 deletions

View file

@ -14,6 +14,7 @@ import (
"time"
"banger/internal/api"
sess "banger/internal/daemon/session"
"banger/internal/model"
"banger/internal/system"
)
@ -68,8 +69,8 @@ func (d *Daemon) ExportVMWorkspace(ctx context.Context, params api.WorkspaceExpo
// past diffRef) and any additional uncommitted changes on top.
patchScript := fmt.Sprintf(
"set -euo pipefail\ncd %s\ngit add -A\ngit diff --cached %s --binary\n",
guestShellQuote(guestPath),
guestShellQuote(diffRef),
sess.ShellQuote(guestPath),
sess.ShellQuote(diffRef),
)
patch, err := client.RunScriptOutput(ctx, patchScript)
if err != nil {
@ -79,8 +80,8 @@ func (d *Daemon) ExportVMWorkspace(ctx context.Context, params api.WorkspaceExpo
// Enumerate changed paths (index already staged; this is a cheap read).
namesScript := fmt.Sprintf(
"set -euo pipefail\ncd %s\ngit diff --cached %s --name-only\n",
guestShellQuote(guestPath),
guestShellQuote(diffRef),
sess.ShellQuote(guestPath),
sess.ShellQuote(diffRef),
)
namesOut, _ := client.RunScriptOutput(ctx, namesScript)
var changed []string
@ -153,9 +154,9 @@ func (d *Daemon) prepareVMWorkspaceLocked(ctx context.Context, vm model.VMRecord
}
if readOnly {
var chmodLog bytes.Buffer
chmodScript := fmt.Sprintf("set -euo pipefail\nchmod -R a-w %s\n", guestShellQuote(guestPath))
chmodScript := fmt.Sprintf("set -euo pipefail\nchmod -R a-w %s\n", sess.ShellQuote(guestPath))
if err := client.RunScript(ctx, chmodScript, &chmodLog); err != nil {
return model.WorkspacePrepareResult{}, formatGuestSessionStepError("set workspace readonly", err, chmodLog.String())
return model.WorkspacePrepareResult{}, sess.FormatStepError("set workspace readonly", err, chmodLog.String())
}
}
return model.WorkspacePrepareResult{
@ -246,13 +247,13 @@ func importWorkspaceRepoToGuest(ctx context.Context, client guestSSHClient, spec
switch mode {
case model.WorkspacePrepareModeFullCopy:
var copyLog bytes.Buffer
command := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", guestShellQuote(guestPath), guestShellQuote(guestPath), guestShellQuote(guestPath))
command := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", sess.ShellQuote(guestPath), sess.ShellQuote(guestPath), sess.ShellQuote(guestPath))
if err := client.StreamTar(ctx, spec.RepoRoot, command, &copyLog); err != nil {
return formatGuestSessionStepError("copy full workspace", err, copyLog.String())
return sess.FormatStepError("copy full workspace", err, copyLog.String())
}
var finalizeLog bytes.Buffer
if err := client.RunScript(ctx, workspaceFinalizeScript(spec, guestPath, mode), &finalizeLog); err != nil {
return formatGuestSessionStepError("finalize full workspace", err, finalizeLog.String())
return sess.FormatStepError("finalize full workspace", err, finalizeLog.String())
}
return nil
case model.WorkspacePrepareModeMetadataOnly, model.WorkspacePrepareModeShallowOverlay:
@ -262,21 +263,21 @@ func importWorkspaceRepoToGuest(ctx context.Context, client guestSSHClient, spec
}
defer cleanup()
var copyLog bytes.Buffer
command := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", guestShellQuote(guestPath), guestShellQuote(guestPath), guestShellQuote(guestPath))
command := fmt.Sprintf("rm -rf %s && mkdir -p %s && tar -o -C %s --strip-components=1 -xf -", sess.ShellQuote(guestPath), sess.ShellQuote(guestPath), sess.ShellQuote(guestPath))
if err := client.StreamTar(ctx, repoCopyDir, command, &copyLog); err != nil {
return formatGuestSessionStepError("copy guest git metadata", err, copyLog.String())
return sess.FormatStepError("copy guest git metadata", err, copyLog.String())
}
var scriptLog bytes.Buffer
if err := client.RunScript(ctx, workspaceFinalizeScript(spec, guestPath, mode), &scriptLog); err != nil {
return formatGuestSessionStepError("prepare guest checkout", err, scriptLog.String())
return sess.FormatStepError("prepare guest checkout", err, scriptLog.String())
}
if mode == model.WorkspacePrepareModeMetadataOnly {
return nil
}
var overlayLog bytes.Buffer
command = fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", guestShellQuote(guestPath))
command = fmt.Sprintf("tar -o -C %s --strip-components=1 -xf -", sess.ShellQuote(guestPath))
if err := client.StreamTarEntries(ctx, spec.RepoRoot, spec.OverlayPaths, command, &overlayLog); err != nil {
return formatGuestSessionStepError("overlay workspace working tree", err, overlayLog.String())
return sess.FormatStepError("overlay workspace working tree", err, overlayLog.String())
}
return nil
default:
@ -287,22 +288,22 @@ func importWorkspaceRepoToGuest(ctx context.Context, client guestSSHClient, spec
func workspaceFinalizeScript(spec workspaceRepoSpec, guestPath string, mode model.WorkspacePrepareMode) string {
var script strings.Builder
script.WriteString("set -euo pipefail\n")
fmt.Fprintf(&script, "DIR=%s\n", guestShellQuote(guestPath))
fmt.Fprintf(&script, "DIR=%s\n", sess.ShellQuote(guestPath))
script.WriteString("git config --global --add safe.directory \"$DIR\"\n")
if mode != model.WorkspacePrepareModeFullCopy {
script.WriteString("find \"$DIR\" -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +\n")
}
switch {
case strings.TrimSpace(spec.BranchName) != "":
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", guestShellQuote(spec.BranchName), guestShellQuote(spec.BaseCommit))
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", sess.ShellQuote(spec.BranchName), sess.ShellQuote(spec.BaseCommit))
case strings.TrimSpace(spec.CurrentBranch) != "":
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", guestShellQuote(spec.CurrentBranch), guestShellQuote(spec.HeadCommit))
fmt.Fprintf(&script, "git -C \"$DIR\" checkout -B %s %s\n", sess.ShellQuote(spec.CurrentBranch), sess.ShellQuote(spec.HeadCommit))
default:
fmt.Fprintf(&script, "git -C \"$DIR\" checkout --detach %s\n", guestShellQuote(spec.HeadCommit))
fmt.Fprintf(&script, "git -C \"$DIR\" checkout --detach %s\n", sess.ShellQuote(spec.HeadCommit))
}
if strings.TrimSpace(spec.GitUserName) != "" && strings.TrimSpace(spec.GitUserEmail) != "" {
fmt.Fprintf(&script, "git -C \"$DIR\" config user.name %s\n", guestShellQuote(spec.GitUserName))
fmt.Fprintf(&script, "git -C \"$DIR\" config user.email %s\n", guestShellQuote(spec.GitUserEmail))
fmt.Fprintf(&script, "git -C \"$DIR\" config user.name %s\n", sess.ShellQuote(spec.GitUserName))
fmt.Fprintf(&script, "git -C \"$DIR\" config user.email %s\n", sess.ShellQuote(spec.GitUserEmail))
}
return script.String()
}