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") }