package daemon import ( "bytes" "context" "errors" "fmt" "net" "strings" "time" "banger/internal/api" sess "banger/internal/daemon/session" "banger/internal/guest" "banger/internal/model" ) func (d *Daemon) StartGuestSession(ctx context.Context, params api.GuestSessionStartParams) (model.GuestSession, error) { stdinMode := model.GuestSessionStdinMode(strings.TrimSpace(params.StdinMode)) if stdinMode == "" { stdinMode = model.GuestSessionStdinClosed } if stdinMode != model.GuestSessionStdinClosed && stdinMode != model.GuestSessionStdinPipe { return model.GuestSession{}, fmt.Errorf("unsupported stdin mode %q", params.StdinMode) } if strings.TrimSpace(params.Command) == "" { return model.GuestSession{}, errors.New("session command is required") } var created model.GuestSession _, err := d.withVMLockByRef(ctx, params.VMIDOrName, func(vm model.VMRecord) (model.VMRecord, error) { if !d.vmAlive(vm) { return model.VMRecord{}, fmt.Errorf("vm %q is not running", vm.Name) } session, err := d.startGuestSessionLocked(ctx, vm, params, stdinMode) if err != nil { return model.VMRecord{}, err } created = session return vm, nil }) return created, err } func (d *Daemon) startGuestSessionLocked(ctx context.Context, vm model.VMRecord, params api.GuestSessionStartParams, stdinMode model.GuestSessionStdinMode) (model.GuestSession, error) { id, err := model.NewID() if err != nil { return model.GuestSession{}, err } now := model.Now() session := model.GuestSession{ ID: id, VMID: vm.ID, 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: sess.CloneStringMap(params.Env), StdinMode: stdinMode, Status: model.GuestSessionStatusStarting, 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 = sess.AttachBackendSSHBridge session.AttachMode = sess.AttachModeExclusive } else { 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 = sess.FailLaunch(session, stage, message, rawLog) if err := d.store.UpsertGuestSession(ctx, session); err != nil { return model.GuestSession{}, err } return session, nil } address := net.JoinHostPort(vm.Runtime.GuestIP, "22") if err := d.waitForGuestSSH(ctx, address, 250*time.Millisecond); err != nil { return fail("ssh_unavailable", fmt.Sprintf("guest ssh unavailable: %v", err), "") } client, err := d.dialGuest(ctx, address) if err != nil { return fail("dial_guest", fmt.Sprintf("dial guest ssh: %v", err), "") } defer client.Close() var preflightLog bytes.Buffer 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 := 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, 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