Replace Bash qvm scripts with Go CLI implementation

This commit is contained in:
Joshua Bell 2026-01-26 20:48:32 -06:00
parent ffb456707f
commit 2a6a333721
27 changed files with 2551 additions and 1702 deletions

View file

@ -0,0 +1,234 @@
package virtiofsd
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/samber/mo"
)
type Manager struct {
stateDir string
pids map[string]int
}
func NewManager(stateDir string) *Manager {
return &Manager{
stateDir: stateDir,
pids: make(map[string]int),
}
}
func findVirtiofsd() (string, error) {
// First try PATH
if path, err := exec.LookPath("virtiofsd"); err == nil {
return path, nil
}
// Fall back to nix
cmd := exec.Command("nix", "path-info", "nixpkgs#virtiofsd")
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("virtiofsd not found in PATH and nix lookup failed: %w", err)
}
storePath := strings.TrimSpace(string(output))
virtiofsdPath := filepath.Join(storePath, "bin", "virtiofsd")
if _, err := os.Stat(virtiofsdPath); err != nil {
return "", fmt.Errorf("virtiofsd binary not found at %s", virtiofsdPath)
}
return virtiofsdPath, nil
}
func (m *Manager) StartMount(mount Mount) mo.Result[int] {
if err := m.CleanStale([]Mount{mount}); err != nil {
return mo.Err[int](fmt.Errorf("failed to clean stale socket for %s: %w", mount.Tag, err))
}
if err := os.MkdirAll(mount.HostPath, 0755); err != nil {
return mo.Err[int](fmt.Errorf("failed to create host directory %s: %w", mount.HostPath, err))
}
virtiofsd, err := findVirtiofsd()
if err != nil {
return mo.Err[int](err)
}
cmd := exec.Command(virtiofsd,
"--socket-path="+mount.SocketPath,
"--shared-dir="+mount.HostPath,
"--cache=auto",
)
if err := cmd.Start(); err != nil {
return mo.Err[int](fmt.Errorf("failed to start virtiofsd for %s: %w", mount.Tag, err))
}
pid := cmd.Process.Pid
m.pids[mount.Tag] = pid
pidFile := m.pidFilePath(mount.Tag)
if err := os.WriteFile(pidFile, []byte(strconv.Itoa(pid)), 0644); err != nil {
_ = cmd.Process.Kill()
return mo.Err[int](fmt.Errorf("failed to write PID file for %s: %w", mount.Tag, err))
}
for i := 0; i < 50; i++ {
if _, err := os.Stat(mount.SocketPath); err == nil {
return mo.Ok(pid)
}
time.Sleep(100 * time.Millisecond)
}
_ = m.StopMount(mount)
return mo.Err[int](fmt.Errorf("virtiofsd socket for %s did not appear within 5 seconds", mount.Tag))
}
func (m *Manager) StopMount(mount Mount) mo.Result[struct{}] {
pidFile := m.pidFilePath(mount.Tag)
pidBytes, err := os.ReadFile(pidFile)
if err != nil {
if os.IsNotExist(err) {
return mo.Ok(struct{}{})
}
return mo.Err[struct{}](fmt.Errorf("failed to read PID file for %s: %w", mount.Tag, err))
}
pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
if err != nil {
return mo.Err[struct{}](fmt.Errorf("invalid PID in file for %s: %w", mount.Tag, err))
}
process, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(pidFile)
_ = os.Remove(mount.SocketPath)
return mo.Ok(struct{}{})
}
if err := process.Signal(syscall.SIGTERM); err != nil {
_ = os.Remove(pidFile)
_ = os.Remove(mount.SocketPath)
return mo.Ok(struct{}{})
}
done := make(chan bool, 1)
go func() {
_, _ = process.Wait()
done <- true
}()
select {
case <-done:
case <-time.After(5 * time.Second):
_ = process.Signal(syscall.SIGKILL)
<-done
}
_ = os.Remove(pidFile)
_ = os.Remove(mount.SocketPath)
delete(m.pids, mount.Tag)
return mo.Ok(struct{}{})
}
func (m *Manager) StartAll(mounts []Mount) mo.Result[struct{}] {
started := []Mount{}
for _, mount := range mounts {
result := m.StartMount(mount)
if result.IsError() {
for i := len(started) - 1; i >= 0; i-- {
_ = m.StopMount(started[i])
}
return mo.Err[struct{}](fmt.Errorf("failed to start mount %s: %w", mount.Tag, result.Error()))
}
started = append(started, mount)
}
return mo.Ok(struct{}{})
}
func (m *Manager) StopAll() mo.Result[struct{}] {
files, err := filepath.Glob(filepath.Join(m.stateDir, "virtiofsd-*.pid"))
if err != nil {
return mo.Err[struct{}](fmt.Errorf("failed to list PID files: %w", err))
}
for _, pidFile := range files {
pidBytes, err := os.ReadFile(pidFile)
if err != nil {
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
if err != nil {
continue
}
if process, err := os.FindProcess(pid); err == nil {
_ = process.Signal(syscall.SIGTERM)
time.Sleep(100 * time.Millisecond)
_ = process.Signal(syscall.SIGKILL)
}
_ = os.Remove(pidFile)
}
sockFiles, err := filepath.Glob(filepath.Join(m.stateDir, "*.sock"))
if err == nil {
for _, sockFile := range sockFiles {
_ = os.Remove(sockFile)
}
}
m.pids = make(map[string]int)
return mo.Ok(struct{}{})
}
func (m *Manager) CleanStale(mounts []Mount) error {
for _, mount := range mounts {
if _, err := os.Stat(mount.SocketPath); err == nil {
pidFile := m.pidFilePath(mount.Tag)
pidBytes, err := os.ReadFile(pidFile)
if err != nil {
_ = os.Remove(mount.SocketPath)
continue
}
pid, err := strconv.Atoi(strings.TrimSpace(string(pidBytes)))
if err != nil {
_ = os.Remove(mount.SocketPath)
_ = os.Remove(pidFile)
continue
}
process, err := os.FindProcess(pid)
if err != nil {
_ = os.Remove(mount.SocketPath)
_ = os.Remove(pidFile)
continue
}
if err := process.Signal(syscall.Signal(0)); err != nil {
_ = os.Remove(mount.SocketPath)
_ = os.Remove(pidFile)
}
}
}
return nil
}
func (m *Manager) pidFilePath(tag string) string {
return filepath.Join(m.stateDir, "virtiofsd-"+tag+".pid")
}

View file

@ -0,0 +1,51 @@
package virtiofsd
import (
"path/filepath"
"qvm/internal/config"
)
// Mount represents a single virtiofsd mount configuration
type Mount struct {
Tag string // Mount tag (e.g., "cargo_home", "ws_abc12345")
HostPath string // Path on host to share
SocketPath string // Path to virtiofsd socket
}
// DefaultCacheMounts returns the standard cache mounts for cargo, pnpm, and sccache.
// These are shared across all projects and mounted at VM start.
func DefaultCacheMounts() []Mount {
return []Mount{
{
Tag: "cargo_home",
HostPath: config.CargoHome,
SocketPath: filepath.Join(config.StateDir, "cargo_home.sock"),
},
{
Tag: "cargo_target",
HostPath: config.CargoTarget,
SocketPath: filepath.Join(config.StateDir, "cargo_target.sock"),
},
{
Tag: "pnpm_store",
HostPath: config.PnpmStore,
SocketPath: filepath.Join(config.StateDir, "pnpm_store.sock"),
},
{
Tag: "sccache",
HostPath: config.Sccache,
SocketPath: filepath.Join(config.StateDir, "sccache.sock"),
},
}
}
// WorkspaceMount creates a Mount configuration for a single workspace.
// mountTag should be the workspace's mount tag (e.g., "ws_abc12345")
// hostPath is the absolute path on the host to share
func WorkspaceMount(mountTag, hostPath string) Mount {
return Mount{
Tag: mountTag,
HostPath: hostPath,
SocketPath: filepath.Join(config.StateDir, mountTag+".sock"),
}
}