package platform import ( "context" "encoding/json" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) const installToRAMDir = "/dev/shm/bee-live" func (s *System) IsLiveMediaInRAM() bool { return s.LiveMediaRAMState().InRAM } func (s *System) LiveBootSource() LiveBootSource { fsType := mountFSType("/run/live/medium") source := mountSource("/run/live/medium") device := findLiveBootDevice() status := LiveBootSource{ InRAM: strings.EqualFold(fsType, "tmpfs"), Source: source, Device: device, } if fsType == "" && source == "" && device == "" { if toramActive() { status.InRAM = true status.Kind = "ram" status.Source = "tmpfs" return status } status.Kind = "unknown" return status } status.Kind = inferLiveBootKind(fsType, source, blockDeviceType(device), blockDeviceTransport(device)) if status.Kind == "" { status.Kind = "unknown" } if status.InRAM && strings.TrimSpace(status.Source) == "" { status.Source = "tmpfs" } return status } func (s *System) LiveMediaRAMState() LiveMediaRAMState { return evaluateLiveMediaRAMState( s.LiveBootSource(), toramActive(), globPaths("/run/live/medium/live/*.squashfs"), globPaths(filepath.Join(installToRAMDir, "*.squashfs")), ) } func evaluateLiveMediaRAMState(status LiveBootSource, toram bool, sourceSquashfs, copiedSquashfs []string) LiveMediaRAMState { state := LiveMediaRAMState{ LiveBootSource: status, ToramActive: toram, CopyPresent: len(copiedSquashfs) > 0, } if status.InRAM { state.State = "in_ram" state.Status = "ok" state.CopyComplete = true state.Message = "Running from RAM — installation media can be safely disconnected." return state } expected := pathBaseSet(sourceSquashfs) copied := pathBaseSet(copiedSquashfs) state.CopyComplete = len(expected) > 0 && setContainsAll(copied, expected) switch { case state.CopyComplete: state.State = "partial" state.Status = "partial" state.CanStartCopy = true state.Message = "Live media files were copied to RAM, but the system is still mounted from the original boot source." case state.CopyPresent: state.State = "partial" state.Status = "partial" state.CanStartCopy = true state.Message = "Partial RAM copy detected. A previous Copy to RAM run was interrupted or cancelled." case toram: state.State = "toram_failed" state.Status = "failed" state.CanStartCopy = true state.Message = "toram boot parameter is set but the live medium is not mounted from RAM." default: state.State = "not_in_ram" state.Status = "warning" state.CanStartCopy = true state.Message = "ISO not copied to RAM. Use Copy to RAM to free the boot drive and improve performance." } return state } func globPaths(pattern string) []string { matches, _ := filepath.Glob(pattern) return matches } func pathBaseSet(paths []string) map[string]struct{} { out := make(map[string]struct{}, len(paths)) for _, path := range paths { base := strings.TrimSpace(filepath.Base(path)) if base != "" { out[base] = struct{}{} } } return out } func setContainsAll(have, want map[string]struct{}) bool { if len(want) == 0 { return false } for name := range want { if _, ok := have[name]; !ok { return false } } return true } func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (retErr error) { log := func(msg string) { if logFunc != nil { logFunc(msg) } } state := s.LiveMediaRAMState() if state.InRAM { 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 := installToRAMDir if state.CopyPresent { log("Removing stale partial RAM copy before retry...") } _ = os.RemoveAll(dstDir) if err := os.MkdirAll(dstDir, 0755); err != nil { return fmt.Errorf("create tmpfs dir: %v", err) } defer func() { if retErr == nil { return } _ = os.RemoveAll(dstDir) log("Removed incomplete RAM copy.") }() for _, sf := range squashfsFiles { if err := ctx.Err(); err != nil { return err } base := filepath.Base(sf) dst := filepath.Join(dstDir, base) log(fmt.Sprintf("Copying %s to RAM...", base)) if err := copyFileLarge(ctx, 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(ctx, "/run/live/medium", dstDir, log); err != nil { log(fmt.Sprintf("Warning: partial copy: %v", err)) } if err := ctx.Err(); err != nil { return err } mediumRebound := false if err := bindMount(dstDir, "/run/live/medium"); err != nil { log(fmt.Sprintf("Warning: rebind /run/live/medium → %s failed: %v", dstDir, err)) } else { mediumRebound = true } log("Verifying live medium now served from RAM...") status := s.LiveBootSource() if err := verifyInstallToRAMStatus(status, dstDir, mediumRebound, log); err != nil { return err } if status.InRAM { log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status))) } log("Done. Squashfs files are in RAM. Installation media can be safely disconnected.") return nil } func verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error { if status.InRAM { return nil } // The live medium mount was not redirected to RAM. This is expected when // booting from an ISO/CD-ROM: the squashfs loop device has a non-zero // offset and LOOP_CHANGE_FD cannot be used; the bind mount also fails // because the CD-ROM mount is in use. Check whether files were at least // copied to the tmpfs directory — that is sufficient for safe disconnection // once the kernel has paged in all actively-used data. files, _ := filepath.Glob(filepath.Join(dstDir, "*.squashfs")) if len(files) > 0 { if !mediumRebound { log(fmt.Sprintf("Note: squashfs copied to RAM (%s) but /run/live/medium still shows the original source.", dstDir)) log("This is normal for CD-ROM boots. For a fully transparent RAM boot, add 'toram' to the kernel parameters.") } return nil } return fmt.Errorf("install to RAM verification failed: live medium still mounted from %s and no squashfs found in %s", describeLiveBootSource(status), dstDir) } func describeLiveBootSource(status LiveBootSource) string { source := strings.TrimSpace(status.Device) if source == "" { source = strings.TrimSpace(status.Source) } if source == "" { source = "unknown source" } switch strings.TrimSpace(status.Kind) { case "ram": return "RAM" case "usb": return "USB (" + source + ")" case "cdrom": return "CD-ROM (" + source + ")" case "disk": return "disk (" + source + ")" default: return source } } func copyFileLarge(ctx context.Context, 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 { if err := ctx.Err(); err != nil { return err } 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(ctx context.Context, src, dst string, logFunc func(string)) error { return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error { if ctx.Err() != nil { return ctx.Err() } 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(ctx, 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) } // loopDeviceOffset returns the byte offset configured for the loop device, // or -1 if it cannot be determined. func loopDeviceOffset(loopDev string) int64 { out, err := exec.Command("losetup", "--json", loopDev).Output() if err != nil { return -1 } var result struct { Loopdevices []struct { Offset int64 `json:"offset"` } `json:"loopdevices"` } if err := json.Unmarshal(out, &result); err != nil || len(result.Loopdevices) == 0 { return -1 } return result.Loopdevices[0].Offset } func reassociateLoopDevice(loopDev, newFile string) error { // LOOP_CHANGE_FD requires lo_offset == 0. ISO/CD-ROM loop devices are // typically set up with a non-zero offset (squashfs lives inside the ISO), // so the ioctl returns EINVAL. Detect this early for a clear error message. if off := loopDeviceOffset(loopDev); off > 0 { return fmt.Errorf("loop device has non-zero offset (%d bytes, typical for ISO/CD-ROM) — LOOP_CHANGE_FD not supported; use 'toram' kernel parameter for RAM boot", off) } if err := exec.Command("losetup", "--replace", loopDev, newFile).Run(); err == nil { return nil } return loopChangeFD(loopDev, newFile) }