Fix guest session cwd preflight scripts

Guest session cwd and command preflight helpers were emitting literal
`\\n` separators, so the guest shell saw malformed one-line scripts and
could fail `preflight_cwd` even when `/root/repo` already existed.

Replace those builders with real newlines, and fix the nearby attach
helper commands that were making the same mistake.

Add a small daemon guest-SSH seam so workspace preparation and session
start can share a fake backend in tests, then cover the regression with
an end-to-end daemon test for `PrepareVMWorkspace` followed by
`StartGuestSession` on `/root/repo`.

Validation: `GOCACHE=/tmp/banger-gocache go test ./internal/daemon` and
`GOCACHE=/tmp/banger-gocache go test ./...`.
This commit is contained in:
Thales Maciel 2026-04-13 18:26:19 -03:00
parent 37c4c091ec
commit 5e26fd7544
No known key found for this signature in database
GPG key ID: 33112E6833C34679
4 changed files with 296 additions and 49 deletions

View file

@ -50,6 +50,35 @@ var guestSessionHostCommandOutputFunc = func(ctx context.Context, name string, a
return output, fmt.Errorf("%s: %w: %s", command, err, detail)
}
type guestSSHClient interface {
Close() error
RunScript(context.Context, string, io.Writer) 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, session model.GuestSession) (model.GuestSession, error) {
if d != nil && d.waitForGuestSessionReady != nil {
return d.waitForGuestSessionReady(ctx, vm, session)
}
return d.waitForGuestSessionReadyDefault(ctx, vm, session)
}
type guestSessionController struct {
stream *guest.StreamSession
streams []*guest.StreamSession
@ -215,10 +244,10 @@ func (d *Daemon) startGuestSessionLocked(ctx context.Context, vm model.VMRecord,
return session, nil
}
address := net.JoinHostPort(vm.Runtime.GuestIP, "22")
if err := guest.WaitForSSH(ctx, address, d.config.SSHKeyPath, 250*time.Millisecond); err != nil {
if err := d.waitForGuestSSH(ctx, address, 250*time.Millisecond); err != nil {
return fail("ssh_unavailable", fmt.Sprintf("guest ssh unavailable: %v", err), "")
}
client, err := guest.Dial(ctx, address, d.config.SSHKeyPath)
client, err := d.dialGuest(ctx, address)
if err != nil {
return fail("dial_guest", fmt.Sprintf("dial guest ssh: %v", err), "")
}
@ -243,7 +272,7 @@ func (d *Daemon) startGuestSessionLocked(ctx context.Context, vm model.VMRecord,
}
readyCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
updated, err := d.waitForGuestSessionReady(readyCtx, vm, session)
updated, err := d.waitForGuestSessionReadyHook(readyCtx, vm, session)
if err != nil {
return fail("ready_wait", "guest session did not report ready state", err.Error())
}
@ -628,7 +657,7 @@ func (d *Daemon) watchGuestSessionAttach(id string, controller *guestSessionCont
}
}
func (d *Daemon) waitForGuestSessionReady(ctx context.Context, vm model.VMRecord, session model.GuestSession) (model.GuestSession, error) {
func (d *Daemon) waitForGuestSessionReadyDefault(ctx context.Context, vm model.VMRecord, session model.GuestSession) (model.GuestSession, error) {
for {
updated, err := d.refreshGuestSession(ctx, vm, session)
if err == nil {
@ -1037,35 +1066,35 @@ func normalizeGuestSessionRequiredCommands(command string, extras []string) []st
func guestSessionCWDPreflightScript(cwd string) string {
var script strings.Builder
script.WriteString("set -euo pipefail\\n")
fmt.Fprintf(&script, "DIR=%s\\n", guestShellQuote(defaultGuestSessionCWD(cwd)))
script.WriteString("if [ ! -d \"$DIR\" ]; then echo \"missing cwd: $DIR\"; exit 1; fi\\n")
script.WriteString("set -euo pipefail\n")
fmt.Fprintf(&script, "DIR=%s\n", guestShellQuote(defaultGuestSessionCWD(cwd)))
script.WriteString("if [ ! -d \"$DIR\" ]; then echo \"missing cwd: $DIR\"; exit 1; fi\n")
return script.String()
}
func guestSessionCommandPreflightScript(commands []string) string {
var script strings.Builder
script.WriteString("set -euo pipefail\\n")
script.WriteString("check_command() {\\n")
script.WriteString(" cmd=\\\"$1\\\"\\n")
script.WriteString(" case \\\"$cmd\\\" in\\n")
script.WriteString(" */*) [ -x \\\"$cmd\\\" ] || { echo \\\"missing command: $cmd\\\"; exit 1; } ;;\\n")
script.WriteString(" *) command -v \\\"$cmd\\\" >/dev/null 2>&1 || { echo \\\"missing command: $cmd\\\"; exit 1; } ;;\\n")
script.WriteString(" esac\\n")
script.WriteString("}\\n")
script.WriteString("set -euo pipefail\n")
script.WriteString("check_command() {\n")
script.WriteString(" cmd=\"$1\"\n")
script.WriteString(" case \"$cmd\" in\n")
script.WriteString(" */*) [ -x \"$cmd\" ] || { echo \"missing command: $cmd\"; exit 1; } ;;\n")
script.WriteString(" *) command -v \"$cmd\" >/dev/null 2>&1 || { echo \"missing command: $cmd\"; exit 1; } ;;\n")
script.WriteString(" esac\n")
script.WriteString("}\n")
for _, command := range commands {
fmt.Fprintf(&script, "check_command %s\\n", guestShellQuote(command))
fmt.Fprintf(&script, "check_command %s\n", guestShellQuote(command))
}
return script.String()
}
func guestSessionAttachInputCommand(sessionID string) string {
path := guestSessionStdinPipePath(sessionID)
return "bash -lc " + guestShellQuote(fmt.Sprintf("set -euo pipefail\\n[ -p %s ] || mkfifo -m 600 %s\\nexec cat > %s\\n", guestShellQuote(path), guestShellQuote(path), guestShellQuote(path)))
return "bash -lc " + guestShellQuote(fmt.Sprintf("set -euo pipefail\n[ -p %s ] || mkfifo -m 600 %s\nexec cat > %s\n", guestShellQuote(path), guestShellQuote(path), guestShellQuote(path)))
}
func guestSessionAttachTailCommand(path string) string {
return "bash -lc " + guestShellQuote(fmt.Sprintf("set -euo pipefail\\ntouch %s\\nexec tail -n 0 -F %s 2>/dev/null\\n", guestShellQuote(path), guestShellQuote(path)))
return "bash -lc " + guestShellQuote(fmt.Sprintf("set -euo pipefail\ntouch %s\nexec tail -n 0 -F %s 2>/dev/null\n", guestShellQuote(path), guestShellQuote(path)))
}
func guestSessionEnvLines(values map[string]string) []string {