banger/internal/opencode/opencode.go
Thales Maciel a166068fab
Add an experimental Alpine image flow
Stage a complete Alpine x86_64 image stack so \	--image alpineworks like the existing manual Void path instead of relying on Debian-oriented image builds.\n\nAdd make targets plus kernel/rootfs/register helpers that download pinned Alpine artifacts, extract a Firecracker-compatible vmlinux, build a matching mkinitfs initramfs, seed OpenRC services, and register/promote a managed image named alpine.\n\nFold in the bring-up fixes discovered during boot validation: use rootfstype=ext4 in shared boot args, install libgcc/libstdc++ for the opencode binary, and give opencode more time to become ready on cold boots.\n\nValidate with go test ./..., the Alpine helper builds, image promotion, and banger vm create --image alpine --name alp --nat plus guest service and port checks.
2026-03-21 20:25:55 -03:00

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_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:
}
}
}