package opencode import ( "context" "fmt" "log/slog" "strings" "time" "banger/internal/vsockagent" ) const ( Port = 4096 Host = "0.0.0.0" GuestBinaryPath = "/usr/local/bin/opencode" ShimPath = "/root/.local/share/mise/shims/opencode" ServiceName = "banger-opencode.service" RunitServiceName = "banger-opencode" ReadyTimeout = 45 * time.Second pollInterval = 200 * time.Millisecond ) func ServiceUnit() string { return fmt.Sprintf(`[Unit] Description=Banger opencode server After=network.target RequiresMountsFor=/root [Service] Type=simple Environment=HOME=/root WorkingDirectory=/root ExecStart=%s serve --hostname %s --port %d Restart=on-failure RestartSec=1 [Install] WantedBy=multi-user.target `, GuestBinaryPath, Host, Port) } func RunitRunScript() string { return fmt.Sprintf(`#!/bin/sh set -e export HOME=/root cd /root exec %s serve --hostname %s --port %d `, GuestBinaryPath, Host, Port) } func Ready(listeners []vsockagent.PortListener) bool { for _, listener := range listeners { if strings.ToLower(strings.TrimSpace(listener.Proto)) != "tcp" { continue } if listener.Port == Port { return true } } return false } func WaitReady(ctx context.Context, logger *slog.Logger, socketPath string, report func(stage, detail string)) error { return waitReady(ctx, logger, socketPath, ReadyTimeout, report) } func waitReady(ctx context.Context, logger *slog.Logger, socketPath string, timeout time.Duration, report func(stage, detail string)) error { waitCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ticker := time.NewTicker(pollInterval) defer ticker.Stop() var lastErr error for { portsCtx, portsCancel := context.WithTimeout(waitCtx, 3*time.Second) listeners, err := vsockagent.Ports(portsCtx, logger, socketPath) portsCancel() if err == nil { if Ready(listeners) { return nil } if report != nil { report("wait_opencode", fmt.Sprintf("waiting for opencode on guest port %d", Port)) } lastErr = fmt.Errorf("guest port %d is not listening yet", Port) } else { if report != nil { report("wait_vsock_agent", "waiting for guest vsock agent") } lastErr = err } select { case <-waitCtx.Done(): if lastErr != nil { return fmt.Errorf("opencode server did not become ready on guest port %d: %w", Port, lastErr) } return fmt.Errorf("opencode server did not become ready on guest port %d before timeout", Port) case <-ticker.C: } } }