package platform import ( "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) func (s *System) IsLiveMediaInRAM() bool { out, err := exec.Command("findmnt", "-n", "-o", "FSTYPE", "/run/live/medium").Output() if err != nil { return toramActive() } return strings.TrimSpace(string(out)) == "tmpfs" } func (s *System) RunInstallToRAM(logFunc func(string)) error { log := func(msg string) { if logFunc != nil { logFunc(msg) } } if s.IsLiveMediaInRAM() { log("Already running from RAM — installation media can be safely disconnected.") return nil } squashfsFiles, err := filepath.Glob("/run/live/medium/live/*.squashfs") if err != nil || len(squashfsFiles) == 0 { return fmt.Errorf("no squashfs files found in /run/live/medium/live/") } free := freeMemBytes() var needed int64 for _, sf := range squashfsFiles { fi, err2 := os.Stat(sf) if err2 != nil { return fmt.Errorf("stat %s: %v", sf, err2) } needed += fi.Size() } const headroom = 256 * 1024 * 1024 if free > 0 && needed+headroom > free { return fmt.Errorf("insufficient RAM: need %s, available %s", humanBytes(needed+headroom), humanBytes(free)) } dstDir := "/dev/shm/bee-live" if err := os.MkdirAll(dstDir, 0755); err != nil { return fmt.Errorf("create tmpfs dir: %v", err) } for _, sf := range squashfsFiles { base := filepath.Base(sf) dst := filepath.Join(dstDir, base) log(fmt.Sprintf("Copying %s to RAM...", base)) if err := copyFileLarge(sf, dst, log); err != nil { return fmt.Errorf("copy %s: %v", base, err) } log(fmt.Sprintf("Copied %s.", base)) loopDev, err := findLoopForFile(sf) if err != nil { log(fmt.Sprintf("Loop device for %s not found (%v) — skipping re-association.", base, err)) continue } if err := reassociateLoopDevice(loopDev, dst); err != nil { log(fmt.Sprintf("Warning: could not re-associate %s → %s: %v", loopDev, dst, err)) } else { log(fmt.Sprintf("Loop device %s now backed by RAM copy.", loopDev)) } } log("Copying remaining medium files...") if err := cpDir("/run/live/medium", dstDir, log); err != nil { log(fmt.Sprintf("Warning: partial copy: %v", err)) } if err := exec.Command("mount", "--bind", dstDir, "/run/live/medium").Run(); err != nil { log(fmt.Sprintf("Warning: rebind /run/live/medium failed: %v", err)) } log("Done. Installation media can be safely disconnected.") return nil } func copyFileLarge(src, dst string, logFunc func(string)) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() fi, err := in.Stat() if err != nil { return err } out, err := os.Create(dst) if err != nil { return err } defer out.Close() total := fi.Size() var copied int64 buf := make([]byte, 4*1024*1024) for { n, err := in.Read(buf) if n > 0 { if _, werr := out.Write(buf[:n]); werr != nil { return werr } copied += int64(n) if logFunc != nil && total > 0 { pct := int(float64(copied) / float64(total) * 100) logFunc(fmt.Sprintf(" %s / %s (%d%%)", humanBytes(copied), humanBytes(total), pct)) } } if err == io.EOF { break } if err != nil { return err } } return out.Sync() } func cpDir(src, dst string, logFunc func(string)) error { return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error { if err != nil { return nil } rel, _ := filepath.Rel(src, path) target := filepath.Join(dst, rel) if fi.IsDir() { return os.MkdirAll(target, fi.Mode()) } if strings.HasSuffix(path, ".squashfs") { return nil } if _, err := os.Stat(target); err == nil { return nil } return copyFileLarge(path, target, nil) }) } func findLoopForFile(backingFile string) (string, error) { out, err := exec.Command("losetup", "--list", "--json").Output() if err != nil { return "", err } var result struct { Loopdevices []struct { Name string `json:"name"` BackFile string `json:"back-file"` } `json:"loopdevices"` } if err := json.Unmarshal(out, &result); err != nil { return "", err } for _, dev := range result.Loopdevices { if dev.BackFile == backingFile { return dev.Name, nil } } return "", fmt.Errorf("no loop device found for %s", backingFile) } func reassociateLoopDevice(loopDev, newFile string) error { if err := exec.Command("losetup", "--replace", loopDev, newFile).Run(); err == nil { return nil } return loopChangeFD(loopDev, newFile) }