fix(runtime): make Copy to RAM switch live backing

This commit is contained in:
Mikhail Chusavitin
2026-09-03 10:47:15 +03:00
parent 347bc8310a
commit b1ab866f58
7 changed files with 150 additions and 111 deletions
+74 -98
View File
@@ -155,41 +155,33 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret
log("Already running from RAM — installation media can be safely disconnected.")
return nil
}
originalStatus := state.LiveBootSource
squashfsFiles, sourceAvailable := ensureLiveMediumAvailable(log)
dstDir := installToRAMDir
loopsSwitched := false
defer func() {
if retErr == nil || loopsSwitched {
return
}
_ = os.RemoveAll(dstDir)
log("Removed incomplete RAM copy.")
}()
// If the source medium is unavailable, check whether a previous run already
// produced a complete copy in RAM. If so, skip the copy phase and proceed
// directly to the loop-rebind / bind-mount steps.
// A worker or virtual-CD disconnect may have interrupted the task after a
// complete squashfs copy was fsynced. That copy is useful, but it is not
// success by itself: the active loop device still has to accept it as its
// backing file and the live-medium mount must move to tmpfs below.
if !sourceAvailable {
copiedFiles, _ := filepath.Glob(filepath.Join(dstDir, "*.squashfs"))
if len(copiedFiles) > 0 {
log("Source medium not available, but a previous RAM copy was found — resuming from existing copy.")
// Proceed to rebind with the already-copied files.
for _, dst := range copiedFiles {
base := filepath.Base(dst)
// Re-associate the loop device that was originally backed by the
// source file (now gone); find it by the old source path pattern.
srcGuess := "/run/live/medium/live/" + base
loopDev, lerr := findLoopForFile(srcGuess)
if lerr != nil {
log(fmt.Sprintf("Loop device for %s not found (%v) — skipping re-association.", base, lerr))
continue
}
if rerr := reassociateLoopDevice(loopDev, dst); rerr != nil {
log(fmt.Sprintf("Warning: could not re-associate %s → %s: %v", loopDev, dst, rerr))
} else {
log(fmt.Sprintf("Loop device %s now backed by RAM copy.", loopDev))
}
}
goto bindMedium
if len(copiedFiles) == 0 {
return fmt.Errorf("no squashfs files found in /run/live/medium/live/ and no prior RAM copy in %s — reconnect the installation medium and retry (or run bee-remount-medium as root)", dstDir)
}
return fmt.Errorf("no squashfs files found in /run/live/medium/live/ and no prior RAM copy in %s — reconnect the installation medium and retry (or run bee-remount-medium as root)", dstDir)
log("Source medium not available, but a previous RAM copy was found — verifying and resuming it.")
squashfsFiles = copiedFiles
}
{
if sourceAvailable {
free := freeMemBytes()
var needed int64
for _, sf := range squashfsFiles {
@@ -204,72 +196,78 @@ func (s *System) RunInstallToRAM(ctx context.Context, logFunc func(string)) (ret
return fmt.Errorf("insufficient RAM: need %s, available %s",
humanBytes(needed+headroom), humanBytes(free))
}
}
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
if state.CopyPresent {
log("Removing stale partial RAM copy before retry...")
}
_ = os.RemoveAll(dstDir)
log("Removed incomplete RAM copy.")
}()
if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("create tmpfs dir: %v", err)
}
for _, sf := range squashfsFiles {
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))
}
log("Copying remaining medium files...")
if err := cpDir(ctx, "/run/live/medium", dstDir, log); err != nil {
log(fmt.Sprintf("Warning: partial metadata copy: %v", err))
}
if err := ctx.Err(); err != nil {
return err
}
}
switchedCount := 0
for _, sf := range squashfsFiles {
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)
oldBacking := sf
if !sourceAvailable {
oldBacking = filepath.Join("/run/live/medium/live", base)
}
log(fmt.Sprintf("Copied %s.", base))
loopDev, err := findLoopForFile(sf)
loopDev, err := findLoopForFile(oldBacking)
if err != nil {
log(fmt.Sprintf("Loop device for %s not found (%v) — skipping re-association.", base, err))
continue
return fmt.Errorf("find active loop device for %s: %v", base, err)
}
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))
return fmt.Errorf("switch %s to RAM copy %s: %v", loopDev, dst, err)
}
loopsSwitched = true
newLoop, err := findLoopForFile(dst)
if err != nil || newLoop != loopDev {
return fmt.Errorf("verify %s RAM backing: active backing file did not change to %s", loopDev, dst)
}
switchedCount++
log(fmt.Sprintf("Loop device %s now backed by RAM copy.", loopDev))
}
if strings.TrimSpace(mountSource("/run/live/medium")) != "" {
log("Unmounting original live medium...")
if err := umountLiveMedium(); err != nil {
return fmt.Errorf("unmount original /run/live/medium: %v", err)
}
}
bindMedium:
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
return fmt.Errorf("bind RAM copy on /run/live/medium: %v", err)
}
log("Verifying live medium now served from RAM...")
status := s.LiveBootSource()
if err := verifyInstallToRAMStatus(status, dstDir, mediumRebound, log); err != nil {
if err := verifyInstallToRAMStatus(status, switchedCount); err != nil {
return err
}
if status.InRAM {
log(fmt.Sprintf("Verification passed: live medium now served from %s.", describeLiveBootSource(status)))
}
detachInstallMedium(status, log)
log(fmt.Sprintf("Verification passed: %d loop device(s) and live medium now served from RAM.", switchedCount))
detachInstallMedium(originalStatus, log)
log("Done. Squashfs files are in RAM. Installation media has been detached when possible.")
return nil
}
@@ -324,12 +322,6 @@ func detachInstallMedium(status LiveBootSource, log func(string)) {
}
log("Detaching original installation medium...")
if err := umountLiveMedium(); err != nil {
log(fmt.Sprintf("Warning: could not unmount /run/live/medium: %v", err))
} else {
log("Unmounted /run/live/medium.")
}
device := strings.TrimSpace(status.Device)
if device == "" {
device = strings.TrimSpace(status.Source)
@@ -346,27 +338,14 @@ func detachInstallMedium(status LiveBootSource, log func(string)) {
log(fmt.Sprintf("Ejected %s.", device))
}
func verifyInstallToRAMStatus(status LiveBootSource, dstDir string, mediumRebound bool, log func(string)) error {
if status.InRAM {
return nil
func verifyInstallToRAMStatus(status LiveBootSource, switchedLoops int) error {
if switchedLoops <= 0 {
return fmt.Errorf("install to RAM verification failed: no active squashfs loop device was switched to RAM")
}
// 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
if !status.InRAM {
return fmt.Errorf("install to RAM verification failed: live medium still mounted from %s", describeLiveBootSource(status))
}
return fmt.Errorf("install to RAM verification failed: live medium still mounted from %s and no squashfs found in %s", describeLiveBootSource(status), dstDir)
return nil
}
func describeLiveBootSource(status LiveBootSource) string {
@@ -519,8 +498,5 @@ func reassociateLoopDevice(loopDev, newFile string) error {
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)
}