Add vsock-backed SSH session reminders

Remind users when a VM is still running after 	hanger vm ssh exits instead of silently dropping them back to the host shell.\n\nAttach a Firecracker vsock device to each VM, persist the host vsock path/CID,\nadd a new guest-side banger-vsock-pingd responder to the runtime bundle and both\nimage-build paths, and expose a vm.ping RPC that the CLI and TUI call after SSH\nreturns. Doctor and start/build preflight now validate the helper plus\n/dev/vhost-vsock so the feature fails early and clearly.\n\nValidated with go mod tidy, bash -n customize.sh, git diff --check, make build,\nand GOCACHE=/tmp/banger-gocache go test ./... outside the sandbox because the\ndaemon tests need real Unix/UDP sockets. Rebuild the image/rootfs used for new\nVMs so the guest ping service is present.
This commit is contained in:
Thales Maciel 2026-03-18 20:14:51 -03:00
parent 4930d82cb9
commit 08ef706e3f
No known key found for this signature in database
GPG key ID: 33112E6833C34679
31 changed files with 912 additions and 75 deletions

View file

@ -16,6 +16,7 @@ import (
"banger/internal/paths"
"banger/internal/rpc"
"banger/internal/system"
"banger/internal/vsockping"
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
@ -104,6 +105,7 @@ type externalPreparedMsg struct {
action actionRequest
command *exec.Cmd
doneStatus string
done func(error) tea.Msg
refresh bool
err error
}
@ -716,10 +718,14 @@ func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
break
}
cmds = append(cmds, tea.ExecProcess(msg.command, func(err error) tea.Msg {
err = normalizeExecError(err)
if msg.done != nil {
return msg.done(err)
}
return actionResultMsg{
action: msg.action,
status: msg.doneStatus,
err: normalizeExecError(err),
err: err,
refresh: msg.refresh,
focusID: m.selectedID,
}
@ -1439,14 +1445,55 @@ func prepareSSHCmd(layout paths.Layout, cfg model.DaemonConfig, action actionReq
return externalPreparedMsg{action: action, err: err}
}
return externalPreparedMsg{
action: action,
command: exec.Command("ssh", args...),
doneStatus: fmt.Sprintf("ssh session ended for %s", result.Name),
refresh: true,
action: action,
command: exec.Command("ssh", args...),
done: func(execErr error) tea.Msg {
return sshDoneMsg(layout, action, result.Name, execErr)
},
refresh: true,
}
}
}
func sshDoneMsg(layout paths.Layout, action actionRequest, name string, execErr error) tea.Msg {
if execErr != nil {
return actionResultMsg{
action: action,
err: execErr,
refresh: true,
focusID: action.id,
}
}
pingCtx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ping, err := vmPingFunc(pingCtx, layout.SocketPath, name)
if err != nil {
return actionResultMsg{
action: action,
status: vsockping.WarningMessage(name, err),
refresh: true,
focusID: action.id,
}
}
if ping.Alive {
if strings.TrimSpace(ping.Name) != "" {
name = ping.Name
}
return actionResultMsg{
action: action,
status: vsockping.ReminderMessage(name),
refresh: true,
focusID: action.id,
}
}
return actionResultMsg{
action: action,
status: fmt.Sprintf("ssh session ended for %s", name),
refresh: true,
focusID: action.id,
}
}
func prepareLogsCmd(layout paths.Layout, action actionRequest) tea.Cmd {
return func() tea.Msg {
result, err := rpc.Call[api.VMLogsResult](context.Background(), layout.SocketPath, "vm.logs", api.VMRefParams{IDOrName: action.id})