main 6af0a61

cleanup.go RawHistoryFolder

1package cleanup
2
3import (
4	"os"
5	"os/exec"
6	"path/filepath"
7	"strings"
8	"time"
9
10	"github.com/bnema/archup/internal/config"
11	"github.com/bnema/archup/internal/logger"
12)
13
14// Run performs a comprehensive cleanup of installation artifacts.
15// This function is idempotent and safe to call multiple times.
16// It performs the following operations:
17// 1. Unmount /mnt hierarchy (recursive, force if needed)
18// 2. Close LUKS/dm-crypt mappings (cryptroot and all others)
19// 3. Disable swap
20// 4. Kill stuck pacstrap/arch-chroot processes
21// 5. Wipe /mnt directory contents (fresh start for next install)
22// 6. Remove installation config file
23// 7. Remove archived log files (*.log.TIMESTAMP)
24// 8. Sync filesystems
25func Run(log *logger.Logger) error {
26	log.Info("Starting comprehensive cleanup")
27
28	cleanupMounts(log)
29	cleanupEncryption(log)
30	cleanupSwap(log)
31	cleanupProcesses(log)
32	cleanupMountPoint(log)
33	cleanupConfig(log)
34	cleanupOldLogs(log)
35	cleanupSync(log)
36
37	log.Info("Cleanup complete - system ready for fresh install")
38	return nil
39}
40
41// cleanupMounts unmounts the /mnt hierarchy in reverse order.
42func cleanupMounts(log *logger.Logger) {
43	log.Info("Cleaning up mounted filesystems")
44
45	// Unmount in reverse order (most specific first)
46	mountsToClean := []string{
47		"/mnt/boot",
48		"/mnt/home",
49		"/mnt",
50	}
51
52	for _, mountPoint := range mountsToClean {
53		// Check if mount point is mounted
54		if !isMounted(mountPoint) {
55			continue
56		}
57
58		log.Info("Unmounting", "mount_point", mountPoint)
59
60		// Try normal unmount first
61		cmd := exec.Command("umount", "-R", mountPoint)
62		if err := cmd.Run(); err == nil {
63			log.Info("Unmounted successfully", "mount_point", mountPoint)
64			continue
65		}
66
67		// Force unmount if normal unmount fails
68		log.Info("Force unmounting", "mount_point", mountPoint)
69		cmd = exec.Command("umount", "-R", "-l", mountPoint)
70		if err := cmd.Run(); err == nil {
71			log.Info("Force unmounted successfully", "mount_point", mountPoint)
72		} else {
73			log.Warn("Failed to unmount (may retry later)", "mount_point", mountPoint, "error", err)
74		}
75	}
76
77	// Wait for any lingering unmount operations
78	time.Sleep(1 * time.Second)
79}
80
81// cleanupEncryption closes LUKS/dm-crypt mappings.
82func cleanupEncryption(log *logger.Logger) {
83	log.Info("Cleaning up LUKS/dm-crypt mappings")
84
85	// Close cryptroot specifically
86	if _, err := os.Stat("/dev/mapper/cryptroot"); err == nil {
87		log.Info("Closing LUKS container", "device", "cryptroot")
88		cmd := exec.Command("cryptsetup", "close", "cryptroot")
89		if err := cmd.Run(); err == nil {
90			log.Info("Closed LUKS container successfully", "device", "cryptroot")
91		} else {
92			log.Warn("Failed to close LUKS container", "device", "cryptroot", "error", err)
93		}
94	}
95
96	// Close ALL dm-crypt mappings
97	cmd := exec.Command("dmsetup", "ls", "--target", "crypt")
98	output, err := cmd.Output()
99	if err == nil && len(output) > 0 {
100		lines := strings.Split(string(output), "\n")
101		for _, line := range lines {
102			if line == "" {
103				continue
104			}
105			// Extract mapping name (first field)
106			fields := strings.Fields(line)
107			if len(fields) == 0 {
108				continue
109			}
110			mapping := fields[0]
111
112			log.Info("Closing dm-crypt mapping", "device", mapping)
113			closeCmd := exec.Command("cryptsetup", "close", mapping)
114			if err := closeCmd.Run(); err == nil {
115				log.Info("Closed dm-crypt mapping successfully", "device", mapping)
116			} else {
117				log.Warn("Failed to close dm-crypt mapping", "device", mapping, "error", err)
118			}
119		}
120	}
121
122	// Ensure all device mapper entries are removed
123	cmd = exec.Command("dmsetup", "table")
124	output, err = cmd.Output()
125	if err == nil && strings.Contains(string(output), "crypt") {
126		log.Info("Removing lingering device mapper entries")
127		removeCmd := exec.Command("dmsetup", "remove_all")
128		_ = removeCmd.Run() // Ignore errors
129		log.Info("Device mapper cleanup attempted")
130	}
131}
132
133// cleanupSwap disables all active swap.
134func cleanupSwap(log *logger.Logger) {
135	log.Info("Disabling swap")
136
137	// Check if swap is active
138	cmd := exec.Command("swapon", "--show")
139	output, err := cmd.Output()
140	if err == nil && len(output) > 0 {
141		log.Info("Found active swap, disabling")
142		swapoffCmd := exec.Command("swapoff", "-a")
143		if err := swapoffCmd.Run(); err == nil {
144			log.Info("Swap disabled successfully")
145		} else {
146			log.Warn("Failed to disable swap (non-critical)", "error", err)
147		}
148	}
149}
150
151// cleanupProcesses kills stuck pacstrap/arch-chroot processes.
152func cleanupProcesses(log *logger.Logger) {
153	log.Info("Checking for stuck pacstrap/chroot processes")
154
155	// Find stuck processes
156	cmd := exec.Command("pgrep", "-f", "pacstrap|arch-chroot")
157	output, err := cmd.Output()
158	if err != nil {
159		// pgrep returns 1 if no processes found, which is expected/normal
160		return
161	}
162
163	if len(output) > 0 {
164		pids := strings.TrimSpace(string(output))
165		log.Info("Killing stuck processes", "pids", pids)
166
167		// Try SIGTERM first
168		pidList := strings.Fields(pids)
169		for _, pid := range pidList {
170			killCmd := exec.Command("kill", pid)
171			_ = killCmd.Run() // Ignore errors
172		}
173
174		time.Sleep(1 * time.Second)
175
176		// Try SIGKILL if still running
177		for _, pid := range pidList {
178			killCmd := exec.Command("kill", "-9", pid)
179			_ = killCmd.Run() // Ignore errors
180		}
181
182		log.Info("Cleaned up stuck processes")
183	}
184}
185
186// cleanupMountPoint wipes /mnt directory for a fresh install
187func cleanupMountPoint(log *logger.Logger) {
188	log.Info("Wiping /mnt directory for fresh install")
189
190	mountPoint := config.PathMnt
191
192	// Verify /mnt is not mounted before wiping
193	if isMounted(mountPoint) {
194		log.Warn("Mount point still mounted, skipping wipe", "path", mountPoint)
195		return
196	}
197
198	// Remove all contents of /mnt
199	entries, err := os.ReadDir(mountPoint)
200	if err != nil {
201		if os.IsNotExist(err) {
202			log.Info("Mount point doesn't exist, nothing to clean", "path", mountPoint)
203			return
204		}
205		log.Warn("Failed to read mount point", "path", mountPoint, "error", err)
206		return
207	}
208
209	if len(entries) == 0 {
210		log.Info("Mount point already empty", "path", mountPoint)
211		return
212	}
213
214	removedCount := 0
215	for _, entry := range entries {
216		entryPath := filepath.Join(mountPoint, entry.Name())
217		if err := os.RemoveAll(entryPath); err == nil {
218			removedCount++
219			log.Info("Removed", "path", entryPath)
220		} else {
221			log.Warn("Failed to remove", "path", entryPath, "error", err)
222		}
223	}
224
225	if removedCount > 0 {
226		log.Info("Wiped mount point", "removed_items", removedCount)
227	}
228}
229
230// cleanupConfig removes the installation config file.
231func cleanupConfig(log *logger.Logger) {
232	log.Info("Removing installation config file")
233
234	configPath := config.DefaultConfigPath
235	if _, err := os.Stat(configPath); err == nil {
236		if err := os.Remove(configPath); err == nil {
237			log.Info("Config file removed", "path", configPath)
238		} else {
239			log.Warn("Failed to remove config file", "path", configPath, "error", err)
240		}
241	}
242}
243
244// cleanupOldLogs removes archived log files (*.log.TIMESTAMP pattern).
245func cleanupOldLogs(log *logger.Logger) {
246	log.Info("Removing archived log files")
247
248	logDir := filepath.Dir(config.DefaultLogPath)
249	logBase := filepath.Base(config.DefaultLogPath)
250
251	// Find all archived logs matching pattern: archup-install.log.2025-10-26_203045
252	pattern := filepath.Join(logDir, logBase+".*")
253	matches, err := filepath.Glob(pattern)
254	if err != nil {
255		log.Warn("Failed to find archived logs", "error", err)
256		return
257	}
258
259	if len(matches) == 0 {
260		log.Info("No archived logs to clean up")
261		return
262	}
263
264	removedCount := 0
265	for _, archiveFile := range matches {
266		// Skip the current log file itself
267		if archiveFile == config.DefaultLogPath {
268			continue
269		}
270
271		if err := os.Remove(archiveFile); err == nil {
272			removedCount++
273		} else {
274			log.Warn("Failed to remove archived log", "file", archiveFile, "error", err)
275		}
276	}
277
278	if removedCount > 0 {
279		log.Info("Removed archived logs", "count", removedCount)
280	}
281}
282
283// cleanupSync syncs all filesystems.
284func cleanupSync(log *logger.Logger) {
285	log.Info("Syncing filesystems")
286
287	cmd := exec.Command("sync")
288	if err := cmd.Run(); err == nil {
289		log.Info("Filesystems synced successfully")
290	} else {
291		log.Warn("Failed to sync filesystems (non-critical)", "error", err)
292	}
293}
294
295// isMounted checks if a mount point is currently mounted.
296func isMounted(mountPoint string) bool {
297	cmd := exec.Command("mountpoint", "-q", mountPoint)
298	return cmd.Run() == nil
299}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close