272 lines
6.9 KiB
Go
272 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// mount tracker
|
|
|
|
type MountTracker struct {
|
|
mounts []string
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (mt *MountTracker) Add(path string) {
|
|
mt.mu.Lock()
|
|
defer mt.mu.Unlock()
|
|
mt.mounts = append(mt.mounts, path)
|
|
}
|
|
|
|
func (mt *MountTracker) GetAll() []string {
|
|
mt.mu.Lock()
|
|
defer mt.mu.Unlock()
|
|
result := make([]string, len(mt.mounts))
|
|
copy(result, mt.mounts)
|
|
sort.SliceStable(result, func(i, j int) bool {
|
|
return len(result[i]) > len(result[j])
|
|
})
|
|
return result
|
|
}
|
|
|
|
func (mt *MountTracker) Reset() {
|
|
mt.mu.Lock()
|
|
defer mt.mu.Unlock()
|
|
mt.mounts = nil
|
|
}
|
|
|
|
// mount helpers
|
|
|
|
func findActiveMountCount(path string) int {
|
|
if path == "" {
|
|
return 0
|
|
}
|
|
cmd := exec.Command("findmnt", "-R", "-n", "-o", "TARGET", path)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
|
|
if len(lines) == 1 && lines[0] == "" {
|
|
return 0
|
|
}
|
|
return len(lines)
|
|
}
|
|
|
|
func hasActiveMounts(path string) bool {
|
|
return findActiveMountCount(path) > 0
|
|
}
|
|
|
|
func retryMount(source, target, fstype string, flags uintptr, data string, maxRetries int) error {
|
|
var lastErr error
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
time.Sleep(retryDelay)
|
|
}
|
|
err := syscall.Mount(source, target, fstype, flags, data)
|
|
if err == nil {
|
|
debugf("Mounted %s -> %s", source, target)
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
debugf("Mount attempt %d failed for %s: %v", attempt+1, target, err)
|
|
}
|
|
return fmt.Errorf("mount %s failed after %d attempts: %w", target, maxRetries, lastErr)
|
|
}
|
|
|
|
func retryUnmount(target string, flags int, maxRetries int) error {
|
|
var lastErr error
|
|
for attempt := 0; attempt < maxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
time.Sleep(retryDelay)
|
|
}
|
|
err := syscall.Unmount(target, flags)
|
|
if err == nil || err == syscall.EINVAL || err == syscall.ENOENT {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
debugf("Unmount attempt %d failed for %s: %v", attempt+1, target, err)
|
|
}
|
|
return fmt.Errorf("unmount %s failed after %d attempts: %w", target, maxRetries, lastErr)
|
|
}
|
|
|
|
// chroot setup
|
|
|
|
func setupChroot(tmpDir, uuid string, needDev bool, tracker *MountTracker) error {
|
|
base := filepath.Join(tmpDir, uuid)
|
|
rootfs := filepath.Join(base, "rootfs")
|
|
|
|
dirs := []string{
|
|
rootfs,
|
|
filepath.Join(base, "run"),
|
|
filepath.Join(base, "var", "cache", "zypp"),
|
|
filepath.Join(base, "var", "lib", "ca-certificates"),
|
|
}
|
|
for _, d := range dirs {
|
|
if err := os.MkdirAll(d, 0755); err != nil {
|
|
return fmt.Errorf("mkdir %s: %w", d, err)
|
|
}
|
|
}
|
|
|
|
resolvPath := "/etc/resolv.conf"
|
|
if linkTarget, err := os.Readlink(resolvPath); err == nil {
|
|
if !filepath.IsAbs(linkTarget) {
|
|
linkTarget = filepath.Join("/etc", linkTarget)
|
|
}
|
|
target := filepath.Join(base, linkTarget)
|
|
os.MkdirAll(filepath.Dir(target), 0755)
|
|
if data, err := os.ReadFile(linkTarget); err == nil {
|
|
os.WriteFile(target, data, 0644)
|
|
}
|
|
} else if data, err := os.ReadFile(resolvPath); err == nil {
|
|
target := filepath.Join(base, resolvPath)
|
|
os.MkdirAll(filepath.Dir(target), 0755)
|
|
os.WriteFile(target, data, 0644)
|
|
}
|
|
|
|
if err := retryMount("/", rootfs, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
|
|
return fmt.Errorf("mount rootfs: %w", err)
|
|
}
|
|
tracker.Add(rootfs)
|
|
|
|
if needDev {
|
|
devPath := filepath.Join(rootfs, "dev")
|
|
if err := retryMount("/dev", devPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
|
return fmt.Errorf("mount /dev bind: %w", err)
|
|
}
|
|
tracker.Add(devPath)
|
|
|
|
tmpPath := filepath.Join(rootfs, "tmp")
|
|
if err := retryMount("tmpfs", tmpPath, "tmpfs", 0, "", maxRetries); err != nil {
|
|
return fmt.Errorf("mount tmpfs: %w", err)
|
|
}
|
|
tracker.Add(tmpPath)
|
|
}
|
|
|
|
runPath := filepath.Join(rootfs, "run")
|
|
if err := retryMount(filepath.Join(base, "run"), runPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
|
return fmt.Errorf("mount run: %w", err)
|
|
}
|
|
tracker.Add(runPath)
|
|
|
|
varPath := filepath.Join(rootfs, "var")
|
|
if err := retryMount(filepath.Join(base, "var"), varPath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
|
return fmt.Errorf("mount var: %w", err)
|
|
}
|
|
tracker.Add(varPath)
|
|
|
|
cachePath := filepath.Join(rootfs, "var", "cache", "zypp")
|
|
if err := retryMount("/var/cache/zypp", cachePath, "", syscall.MS_BIND, "", maxRetries); err != nil {
|
|
debugf("Warning: failed to mount cache (non-critical): %v", err)
|
|
} else {
|
|
tracker.Add(cachePath)
|
|
}
|
|
|
|
certPath := filepath.Join(rootfs, "var", "lib", "ca-certificates")
|
|
if err := retryMount("/var/lib/ca-certificates", certPath, "", syscall.MS_BIND|syscall.MS_RDONLY, "", maxRetries); err != nil {
|
|
debugf("Warning: failed to mount ca-certs (non-critical): %v", err)
|
|
} else {
|
|
tracker.Add(certPath)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unmount
|
|
|
|
func unmountAll(tmpDir string, tracker *MountTracker) error {
|
|
if tmpDir == "" {
|
|
return nil
|
|
}
|
|
|
|
mounts := tracker.GetAll()
|
|
var errs []string
|
|
|
|
for _, mountPath := range mounts {
|
|
cmd := exec.Command("findmnt", "-n", "-o", "TARGET", mountPath)
|
|
if err := cmd.Run(); err != nil {
|
|
continue
|
|
}
|
|
|
|
if err := retryUnmount(mountPath, syscall.MNT_DETACH, maxRetries); err != nil {
|
|
debugf("Failed to unmount %s: %v", mountPath, err)
|
|
if err := syscall.Unmount(mountPath, syscall.MNT_DETACH|syscall.MNT_FORCE); err != nil {
|
|
errs = append(errs, fmt.Sprintf("%s: %v", mountPath, err))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
warningf("Some unmounts failed: %s", strings.Join(errs, "; "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// cleanup
|
|
|
|
func vlcroCleanup(tmpDir string, tracker *MountTracker) {
|
|
releaseZyppLock()
|
|
|
|
if tmpDir == "" {
|
|
return
|
|
}
|
|
|
|
if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
|
|
return
|
|
}
|
|
|
|
absPath, err := filepath.Abs(tmpDir)
|
|
if err != nil || !strings.HasPrefix(absPath, "/tmp/vlcro_") {
|
|
errorf("Refusing to cleanup unsafe temp directory: %s", tmpDir)
|
|
return
|
|
}
|
|
|
|
infof("%s", colorText(colorInfo, "Cleaning up temp mounts..."))
|
|
unmountAll(tmpDir, tracker)
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
for attempt := 0; attempt < 5; attempt++ {
|
|
if !hasActiveMounts(tmpDir) {
|
|
break
|
|
}
|
|
warningf("Active mounts still detected under %s, retrying unmount...", tmpDir)
|
|
unmountAll(tmpDir, tracker)
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
if hasActiveMounts(tmpDir) {
|
|
warningf("Could not unmount all filesystems under %s. Skipping temp directory removal.", tmpDir)
|
|
return
|
|
}
|
|
|
|
infof("%s", colorText(colorInfo, "Cleaning up temp directory..."))
|
|
for attempt := 0; attempt < 3; attempt++ {
|
|
if err := os.RemoveAll(tmpDir); err == nil {
|
|
return
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
|
|
warningf("Could not fully remove temp directory %s (will clean up at next boot)", tmpDir)
|
|
}
|
|
|
|
// uuid
|
|
|
|
func randomUUID() string {
|
|
b := make([]byte, 16)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
|
|
}
|
|
b[6] = (b[6] & 0x0f) | 0x40
|
|
b[8] = (b[8] & 0x3f) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
|
}
|