okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffauffeat(api)!: migrate daemon and clients to protocol 361c43ca

main
3.7 KiB113 linesraw
1package tui
2
3import (
4	"cmp"
5	"context"
6	"errors"
7	"os/exec"
8	"slices"
9
10	tea "charm.land/bubbletea/v2"
11
12	"albedo/cli/internal/daemon"
13)
14
15// hostSignIn is the session's host needing a person to sign in over ssh
16// before a waiting turn can start. ctrl+l opens the daemon's ControlMaster
17// in this terminal when the daemon shares this machine, so ssh can ask for
18// the passphrase or code here; then the host is probed again and the
19// waiting turn starts on the daemon's next try.
20type hostSignIn struct {
21	host, controlPath, notice string
22	here                      bool
23}
24
25// chatHostAuthMsg is the probe of a host a blocked turn may be waiting on.
26type chatHostAuthMsg struct {
27	SessionID  string
28	Generation int64
29	Status     daemon.HostStatus
30}
31
32type chatSignedInMsg struct {
33	SessionID string
34	Err       error
35}
36
37// hostAuthCmd asks the host of a remote session whose waiting turn is
38// blocked whether it needs a sign-in: the daemon's reason is ssh's words,
39// and the probe carries the control path the sign-in opens.
40func (m *ChatModel) hostAuthCmd(blocked string) tea.Cmd {
41	host, _ := daemon.SplitLocation(m.Workspace)
42	if host == "" || blocked == "" || m.hostAuth != nil {
43		return nil
44	}
45	client, ctx, id, generation := m.client, m.streamCtx, m.SessionID, m.Generation
46	return func() tea.Msg {
47		status, err := client.WarmHost(ctx, host)
48		if err != nil || status.State != "needs_auth" {
49			return nil
50		}
51		return chatHostAuthMsg{SessionID: id, Generation: generation, Status: status}
52	}
53}
54
55// offerSignIn says how to sign in: here when the daemon shares this machine
56// (its master socket is ours to open), otherwise on the daemon's machine.
57func (m *ChatModel) offerSignIn(msg chatHostAuthMsg) {
58	if msg.SessionID != m.SessionID || msg.Generation != m.Generation || m.hostAuth != nil {
59		return
60	}
61	host := msg.Status.Host
62	auth := &hostSignIn{host: host, controlPath: msg.Status.ControlPath, here: m.client.LocalDaemon()}
63	auth.notice = "Run `ssh " + host + "` on the machine albedo runs on; the waiting message goes after."
64	if auth.here {
65		auth.notice = "Press ctrl+l to sign in to " + cmp.Or(m.Host, host) + " here; the waiting message goes after."
66	}
67	m.hostAuth = auth
68	m.AddError(auth.notice)
69}
70
71// signInCmd runs `ssh -M -fN` on the daemon's control path: ssh prompts in
72// the terminal, forks the master into the background once signed in, and the
73// daemon's BatchMode connections ride it from then on.
74func (m *ChatModel) signInCmd() tea.Cmd {
75	auth, client, id := m.hostAuth, m.client, m.SessionID
76	return tea.ExecProcess(signInCommand(auth.host, auth.controlPath), func(err error) tea.Msg {
77		if err == nil {
78			var status daemon.HostStatus
79			status, err = client.WarmHost(context.Background(), auth.host)
80			if err == nil && status.State != "ready" {
81				err = errors.New(cmp.Or(status.Detail, status.State))
82			}
83		}
84		return chatSignedInMsg{SessionID: id, Err: err}
85	})
86}
87
88// signInCommand opens host's master on controlPath, asking in the terminal.
89func signInCommand(host, controlPath string) *exec.Cmd {
90	return exec.Command("ssh", "-M", "-fN", "-o", "ControlPath="+controlPath, "-o", "ControlPersist=600", host)
91}
92
93func (m *ChatModel) signedIn(msg chatSignedInMsg) {
94	auth := m.hostAuth
95	if msg.SessionID != m.SessionID || auth == nil {
96		return
97	}
98	if msg.Err != nil {
99		m.AddError("Signing in to " + auth.host + " failed: " + msg.Err.Error())
100		return
101	}
102	m.dropSignIn()
103}
104
105// dropSignIn forgets the offer once nothing waits on it: signed in, or the
106// turn it was for started or went away.
107func (m *ChatModel) dropSignIn() {
108	if auth := m.hostAuth; auth != nil {
109		m.hostAuth = nil
110		m.Notices = slices.DeleteFunc(m.Notices, func(n Notice) bool { return n.Message == auth.notice })
111		m.syncViewportHeight()
112	}
113}