Make vm create wait for the guest-side vsock /healthz endpoint instead of only waiting for the host socket path, so the wait_vsock_agent stage reflects actual guest readiness. Start banger-vsock-agent earlier in the Alpine OpenRC graph and report later /ports failures as guest-service waits rather than vsock-agent waits, which makes the progress output match what the guest is really doing. Validate with go test ./..., a rebuilt managed alpine image, and a fresh vm create --image alpine --name alp --nat that now progresses through wait_vsock_agent -> wait_guest_ready -> wait_opencode -> ready.
104 lines
2.5 KiB
Go
104 lines
2.5 KiB
Go
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_guest_ready", "waiting for guest services")
|
|
}
|
|
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:
|
|
}
|
|
}
|
|
}
|