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

@ -10,6 +10,7 @@ import (
"time"
"banger/internal/api"
sess "banger/internal/daemon/session"
"banger/internal/guest"
"banger/internal/model"
"banger/internal/system"
@ -50,34 +51,34 @@ func (d *Daemon) startGuestSessionLocked(ctx context.Context, vm model.VMRecord,
session := model.GuestSession{
ID: id,
VMID: vm.ID,
Name: defaultGuestSessionName(id, params.Command, params.Name),
Backend: guestSessionBackendSSH,
Name: sess.DefaultName(id, params.Command, params.Name),
Backend: sess.BackendSSH,
Command: params.Command,
Args: append([]string(nil), params.Args...),
CWD: strings.TrimSpace(params.CWD),
Env: cloneStringMap(params.Env),
Env: sess.CloneStringMap(params.Env),
StdinMode: stdinMode,
Status: model.GuestSessionStatusStarting,
GuestStateDir: guestSessionStateDir(id),
StdoutLogPath: guestSessionStdoutLogPath(id),
StderrLogPath: guestSessionStderrLogPath(id),
Tags: cloneStringMap(params.Tags),
GuestStateDir: sess.StateDir(id),
StdoutLogPath: sess.StdoutLogPath(id),
StderrLogPath: sess.StderrLogPath(id),
Tags: sess.CloneStringMap(params.Tags),
Attachable: stdinMode == model.GuestSessionStdinPipe,
Reattachable: stdinMode == model.GuestSessionStdinPipe,
CreatedAt: now,
UpdatedAt: now,
}
if session.Attachable {
session.AttachBackend = guestSessionAttachBackendSSHBridge
session.AttachMode = guestSessionAttachModeExclusive
session.AttachBackend = sess.AttachBackendSSHBridge
session.AttachMode = sess.AttachModeExclusive
} else {
session.AttachBackend = guestSessionAttachBackendNone
session.AttachBackend = sess.AttachBackendNone
}
if err := d.store.UpsertGuestSession(ctx, session); err != nil {
return model.GuestSession{}, err
}
fail := func(stage, message, rawLog string) (model.GuestSession, error) {
session = failGuestSessionLaunch(session, stage, message, rawLog)
session = sess.FailLaunch(session, stage, message, rawLog)
if err := d.store.UpsertGuestSession(ctx, session); err != nil {
return model.GuestSession{}, err
}
@ -93,20 +94,20 @@ func (d *Daemon) startGuestSessionLocked(ctx context.Context, vm model.VMRecord,
}
defer client.Close()
var preflightLog bytes.Buffer
if err := client.RunScript(ctx, guestSessionCWDPreflightScript(session.CWD), &preflightLog); err != nil {
return fail("preflight_cwd", fmt.Sprintf("guest working directory is unavailable: %s", defaultGuestSessionCWD(session.CWD)), preflightLog.String())
if err := client.RunScript(ctx, sess.CWDPreflightScript(session.CWD), &preflightLog); err != nil {
return fail("preflight_cwd", fmt.Sprintf("guest working directory is unavailable: %s", sess.DefaultCWD(session.CWD)), preflightLog.String())
}
preflightLog.Reset()
requiredCommands := normalizeGuestSessionRequiredCommands(params.Command, params.RequiredCommands)
if err := client.RunScript(ctx, guestSessionCommandPreflightScript(requiredCommands), &preflightLog); err != nil {
requiredCommands := sess.NormalizeRequiredCommands(params.Command, params.RequiredCommands)
if err := client.RunScript(ctx, sess.CommandPreflightScript(requiredCommands), &preflightLog); err != nil {
return fail("preflight_command", fmt.Sprintf("required guest command is unavailable: %s", strings.TrimSpace(preflightLog.String())), preflightLog.String())
}
var uploadLog bytes.Buffer
if err := client.UploadFile(ctx, guestSessionScriptPath(id), 0o755, []byte(guestSessionScript(session)), &uploadLog); err != nil {
if err := client.UploadFile(ctx, sess.ScriptPath(id), 0o755, []byte(sess.Script(session)), &uploadLog); err != nil {
return fail("upload_script", "upload guest session script failed", uploadLog.String())
}
var launchLog bytes.Buffer
launchScript := fmt.Sprintf("set -euo pipefail\nnohup bash %s >/dev/null 2>&1 </dev/null &\ndisown || true\n", guestShellQuote(guestSessionScriptPath(id)))
launchScript := fmt.Sprintf("set -euo pipefail\nnohup bash %s >/dev/null 2>&1 </dev/null &\ndisown || true\n", sess.ShellQuote(sess.ScriptPath(id)))
if err := client.RunScript(ctx, launchScript, &launchLog); err != nil {
return fail("launch", "launch guest session failed", launchLog.String())
}
@ -201,8 +202,8 @@ func (d *Daemon) signalGuestSession(ctx context.Context, params api.GuestSession
}
defer client.Close()
var log bytes.Buffer
if err := client.RunScript(ctx, guestSessionSignalScript(session.ID, signal), &log); err != nil {
return model.GuestSession{}, formatGuestSessionStepError("signal guest session", err, log.String())
if err := client.RunScript(ctx, sess.SignalScript(session.ID, signal), &log); err != nil {
return model.GuestSession{}, sess.FormatStepError("signal guest session", err, log.String())
}
session.Status = model.GuestSessionStatusStopping
session.UpdatedAt = model.Now()