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}