okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
61c43ca
main
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 { 21host ,controlPath ,notice string 22here bool 23} 24 25// chatHostAuthMsg is the probe of a host a blocked turn may be waiting on. 26type chatHostAuthMsg struct { 27SessionID string 28Generation int64 29Status daemon.HostStatus 30} 31 32type chatSignedInMsg struct { 33SessionID string 34Err 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 { 41host ,_ := daemon .SplitLocation (m .Workspace ) 42if host == "" || blocked == "" || m .hostAuth != nil { 43return nil 44 } 45client ,ctx ,id ,generation := m .client ,m .streamCtx ,m .SessionID ,m .Generation 46return func () tea.Msg { 47status ,err := client .WarmHost (ctx ,host ) 48if err != nil || status .State != "needs_auth" { 49return nil 50 } 51return 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 ) { 58if msg .SessionID != m .SessionID || msg .Generation != m .Generation || m .hostAuth != nil { 59return 60 } 61host := msg .Status .Host 62auth := & hostSignIn {host :host ,controlPath :msg .Status .ControlPath ,here :m .client .LocalDaemon ()} 63auth .notice = "Run `ssh " + host + "` on the machine albedo runs on; the waiting message goes after." 64if auth .here { 65auth .notice = "Press ctrl+l to sign in to " + cmp .Or (m .Host ,host )+ " here; the waiting message goes after." 66 } 67m .hostAuth = auth 68m .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 { 75auth ,client ,id := m .hostAuth ,m .client ,m .SessionID 76return tea .ExecProcess (signInCommand (auth .host ,auth .controlPath ),func (err error ) tea.Msg { 77if err == nil { 78var status daemon.HostStatus 79status ,err = client .WarmHost (context .Background (),auth .host ) 80if err == nil && status .State != "ready" { 81err = errors .New (cmp .Or (status .Detail ,status .State )) 82 } 83 } 84return 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 { 90return exec .Command ("ssh" ,"-M" ,"-fN" ,"-o" ,"ControlPath=" + controlPath ,"-o" ,"ControlPersist=600" ,host ) 91} 92 93func (m * ChatModel )signedIn (msg chatSignedInMsg ) { 94auth := m .hostAuth 95if msg .SessionID != m .SessionID || auth == nil { 96return 97 } 98if msg .Err != nil { 99m .AddError ("Signing in to " + auth .host + " failed: " + msg .Err .Error ()) 100return 101 } 102m .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 () { 108if auth := m .hostAuth ;auth != nil { 109m .hostAuth = nil 110m .Notices = slices .DeleteFunc (m .Notices ,func (n Notice )bool {return n .Message == auth .notice }) 111m .syncViewportHeight () 112 } 113}