Add guest sessions and agent VM defaults
Add daemon-backed workspace and guest-session primitives so host orchestrators can prepare /root/repo, launch long-lived guest commands, and attach to pipe-mode sessions over the local stdio mux bridge. Persist richer session metadata and launch diagnostics, preflight guest cwd/command requirements, make pipe-mode attach rehydratable from guest state after daemon restart, and allow submodules when workspace prepare runs in full_copy mode. At the same time, stop vm run from auto-attaching opencode, make it print next-step commands instead, and make glibc guest images more agent-ready by installing node, opencode, claude, and pi while syncing opencode/claude/pi auth files into work disks on VM start. Validation: - GOCACHE=/tmp/banger-gocache go test ./... - make build - banger vm workspace prepare --help - banger vm session --help - banger vm session start --help - banger vm session attach --help
This commit is contained in:
parent
497e6dca3d
commit
37c4c091ec
18 changed files with 3212 additions and 405 deletions
|
|
@ -15,6 +15,7 @@ import (
|
|||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
|
@ -24,6 +25,16 @@ type Client struct {
|
|||
client *ssh.Client
|
||||
}
|
||||
|
||||
type StreamSession struct {
|
||||
client *Client
|
||||
session *ssh.Session
|
||||
stdin io.WriteCloser
|
||||
stdout io.Reader
|
||||
stderr io.Reader
|
||||
waitCh chan error
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func WaitForSSH(ctx context.Context, address, privateKeyPath string, interval time.Duration) error {
|
||||
if interval <= 0 {
|
||||
interval = time.Second
|
||||
|
|
@ -109,6 +120,116 @@ func (c *Client) StreamTarEntries(ctx context.Context, sourceDir string, entries
|
|||
return errors.Join(runErr, tarErr)
|
||||
}
|
||||
|
||||
func (c *Client) StartCommand(ctx context.Context, command string) (*StreamSession, error) {
|
||||
if c == nil || c.client == nil {
|
||||
return nil, fmt.Errorf("ssh client is not connected")
|
||||
}
|
||||
session, err := c.client.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
_ = session.Close()
|
||||
return nil, err
|
||||
}
|
||||
stdout, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
_ = session.Close()
|
||||
return nil, err
|
||||
}
|
||||
stderr, err := session.StderrPipe()
|
||||
if err != nil {
|
||||
_ = session.Close()
|
||||
return nil, err
|
||||
}
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = session.Close()
|
||||
_ = c.client.Close()
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
if err := session.Start(command); err != nil {
|
||||
close(done)
|
||||
_ = session.Close()
|
||||
return nil, err
|
||||
}
|
||||
stream := &StreamSession{
|
||||
client: c,
|
||||
session: session,
|
||||
stdin: stdin,
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
waitCh: make(chan error, 1),
|
||||
}
|
||||
go func() {
|
||||
err := session.Wait()
|
||||
close(done)
|
||||
stream.waitCh <- err
|
||||
close(stream.waitCh)
|
||||
}()
|
||||
return stream, nil
|
||||
}
|
||||
|
||||
func (s *StreamSession) Stdin() io.WriteCloser {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.stdin
|
||||
}
|
||||
|
||||
func (s *StreamSession) Stdout() io.Reader {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.stdout
|
||||
}
|
||||
|
||||
func (s *StreamSession) Stderr() io.Reader {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
return s.stderr
|
||||
}
|
||||
|
||||
func (s *StreamSession) Wait() error {
|
||||
if s == nil || s.waitCh == nil {
|
||||
return nil
|
||||
}
|
||||
err, ok := <-s.waitCh
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *StreamSession) Close() error {
|
||||
if s == nil {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
s.closeOnce.Do(func() {
|
||||
err = errors.Join(
|
||||
func() error {
|
||||
if s.session != nil {
|
||||
return s.session.Close()
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
func() error {
|
||||
if s.client != nil {
|
||||
return s.client.Close()
|
||||
}
|
||||
return nil
|
||||
}(),
|
||||
)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) runSession(ctx context.Context, command string, stdin io.Reader, logWriter io.Writer) error {
|
||||
if c == nil || c.client == nil {
|
||||
return fmt.Errorf("ssh client is not connected")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue