package env
import (
"bytes"
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"net"
"os"
"os/exec"
"strings"
"time"
)
type ContextKey string
const DebugKey ContextKey = "debug"
type Process struct {
cmd *exec.Cmd
sandbox *Sandbox
LogBuffer *bytes.Buffer
DebugMode bool
}
func generateAccessToken() string {
bytes := make([]byte, 16)
if _, err := rand.Read(bytes); err != nil {
return "test-token-fallback-1234567890abcdef"
}
return hex.EncodeToString(bytes)
}
func (s *Sandbox) StartVane(ctx context.Context, debugMode bool) (*Process, error) {
return s.startVaneInternal(ctx, debugMode, true)
}
func (s *Sandbox) StartVaneWithoutToken(ctx context.Context, debugMode bool) (*Process, error) {
return s.startVaneInternal(ctx, debugMode, false)
}
func (s *Sandbox) startVaneInternal(ctx context.Context, debugMode bool, withToken bool) (*Process, error) {
cmd := exec.CommandContext(ctx, "vane")
logLevel := "info"
if debugMode {
logLevel = "debug"
}
envVars := []string{
fmt.Sprintf("CONFIG_DIR=%s", s.ConfigDir),
fmt.Sprintf("SOCKET_DIR=%s", s.SocketDir),
fmt.Sprintf("PORT=%d", s.ConsolePort),
fmt.Sprintf("LOG_LEVEL=%s", logLevel),
"DETECT_PUBLIC_NETWORK=false",
"CONSOLE_LISTEN_IPV6=false",
"DEV_PROJECT_DIR=/tmp/void",
}
if withToken {
if _, ok := s.Env["ACCESS_TOKEN"]; !ok {
token := generateAccessToken()
envVars = append(envVars, fmt.Sprintf("ACCESS_TOKEN=%s", token))
}
}
for k, v := range s.Env {
envVars = append(envVars, fmt.Sprintf("%s=%s", k, v))
}
var baseEnv []string
for _, e := range os.Environ() {
if !strings.HasPrefix(e, "ACCESS_TOKEN=") {
baseEnv = append(baseEnv, e)
}
}
cmd.Env = append(baseEnv, envVars...)
cmd.Dir = s.RootDir
logBuf := &bytes.Buffer{}
if debugMode {
cmd.Stdout = io.MultiWriter(os.Stdout, logBuf)
cmd.Stderr = io.MultiWriter(os.Stderr, logBuf)
} else {
cmd.Stdout = logBuf
cmd.Stderr = logBuf
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start vane binary: %w", err)
}
proc := &Process{
cmd: cmd,
sandbox: s,
LogBuffer: logBuf,
DebugMode: debugMode,
}
if withToken {
if err := proc.WaitForReady(5 * time.Second); err != nil {
proc.Stop()
if !debugMode {
return nil, fmt.Errorf("vane startup failed: %w\nLogs:\n%s", err, logBuf.String())
}
return nil, fmt.Errorf("vane startup failed: %w", err)
}
} else {
if err := proc.WaitForLog("Access token not set, management API disabled", 3*time.Second); err != nil {
proc.Stop()
if !debugMode {
return nil, fmt.Errorf("vane startup failed: %w\nLogs:\n%s", err, logBuf.String())
}
return nil, fmt.Errorf("vane startup failed: %w", err)
}
}
return proc, nil
}
func (p *Process) WaitForReady(timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
target := fmt.Sprintf("127.0.0.1:%d", p.sandbox.ConsolePort)
for {
select {
case <-ticker.C:
conn, err := net.DialTimeout("tcp", target, 50*time.Millisecond)
if err == nil {
conn.Close()
return nil
}
if p.cmd.ProcessState != nil && p.cmd.ProcessState.Exited() {
return fmt.Errorf("process exited unexpectedly")
}
case <-time.After(time.Until(deadline)):
return fmt.Errorf("timeout waiting for port %d", p.sandbox.ConsolePort)
}
}
}
func (p *Process) WaitForNoConsole(timeout time.Duration) error {
target := fmt.Sprintf("127.0.0.1:%d", p.sandbox.ConsolePort)
if err := p.WaitForLog("Access token not set, management API disabled", timeout); err != nil {
return fmt.Errorf("expected 'Access token not set, management API disabled' log message: %w", err)
}
for i := 0; i < 5; i++ {
conn, err := net.DialTimeout("tcp", target, 50*time.Millisecond)
if err == nil {
conn.Close()
return fmt.Errorf("console port %d should NOT be listening (no ACCESS_TOKEN)", p.sandbox.ConsolePort)
}
if p.cmd.ProcessState != nil && p.cmd.ProcessState.Exited() {
return fmt.Errorf("process exited unexpectedly")
}
time.Sleep(200 * time.Millisecond)
}
return nil
}
func (p *Process) WaitForLog(snippet string, timeout time.Duration) error {
deadline := time.Now().Add(timeout)
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
logs := p.LogBuffer.String()
if strings.Contains(logs, snippet) {
return nil
}
if p.cmd.ProcessState != nil && p.cmd.ProcessState.Exited() {
return fmt.Errorf("process exited while waiting for log: %s", snippet)
}
case <-time.After(time.Until(deadline)):
return fmt.Errorf("timeout waiting for log snippet: '%s'", snippet)
}
}
}
func (p *Process) WaitForTcpPort(port int, timeout time.Duration) error {
return p.WaitForLog(fmt.Sprintf("PORT %d TCP UP", port), timeout)
}
func (p *Process) WaitForUdpPort(port int, timeout time.Duration) error {
return p.WaitForLog(fmt.Sprintf("PORT %d UDP UP", port), timeout)
}
func (p *Process) Stop() error {
if p.cmd.Process == nil {
return nil
}
if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
return p.cmd.Process.Kill()
}
return p.cmd.Wait()
}
func (p *Process) DumpLogs() string {
if p.LogBuffer != nil {
return p.LogBuffer.String()
}
return "(No logs captured)"
}