detached c47a795

fix(logging): improve log completeness and cleanup functionality

Logging improvements:
- Add log archiving with timestamps on each installer run
- Add Sync() calls to flush log output immediately in exec.go
- Add comprehensive package list logging in repos phase
- Log which extra packages are being installed and any failures

Cleanup improvements:
- Add cleanupMountPoint() to wipe /mnt contents after unmount
- Add cleanupOldLogs() to remove archived log files
- Ensure --cleanup provides complete fresh start for next install
- Update cleanup documentation to reflect all 8 steps
commit
c47a79515d829032613907b396dcc16e551b685d
author
bnema · · 2025-10-26 21:32 UTC
parent
48f527b
signature
unsigned
4 files · +134 -3
M internal/cleanup/cleanup.go +91 -3
@@ -3,6 +3,7 @@ package cleanup
33 import (
44 "os"
55 "os/exec"
6+ "path/filepath"
67 "strings"
78 "time"
89
@@ -17,8 +18,10 @@ import (
1718 // 2. Close LUKS/dm-crypt mappings (cryptroot and all others)
1819 // 3. Disable swap
1920 // 4. Kill stuck pacstrap/arch-chroot processes
20-// 5. Remove installation config file
21-// 6. Sync filesystems
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
2225 func Run(log *logger.Logger) error {
2326 log.Info("Starting comprehensive cleanup")
2427
@@ -26,10 +29,12 @@ func Run(log *logger.Logger) error {
2629 cleanupEncryption(log)
2730 cleanupSwap(log)
2831 cleanupProcesses(log)
32+ cleanupMountPoint(log)
2933 cleanupConfig(log)
34+ cleanupOldLogs(log)
3035 cleanupSync(log)
3136
32- log.Info("Cleanup complete")
37+ log.Info("Cleanup complete - system ready for fresh install")
3338 return nil
3439 }
3540
@@ -178,6 +183,50 @@ func cleanupProcesses(log *logger.Logger) {
178183 }
179184 }
180185
186+// cleanupMountPoint wipes /mnt directory for a fresh install
187+func 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+
181230 // cleanupConfig removes the installation config file.
182231 func cleanupConfig(log *logger.Logger) {
183232 log.Info("Removing installation config file")
@@ -192,6 +241,45 @@ func cleanupConfig(log *logger.Logger) {
192241 }
193242 }
194243
244+// cleanupOldLogs removes archived log files (*.log.TIMESTAMP pattern).
245+func 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+
195283 // cleanupSync syncs all filesystems.
196284 func cleanupSync(log *logger.Logger) {
197285 log.Info("Syncing filesystems")
@@ -3,6 +3,7 @@ package cleanup
3 import (3 import (
4 "os"4 "os"
5 "os/exec"5 "os/exec"
6+ "path/filepath"
6 "strings"7 "strings"
7 "time"8 "time"
8 9
@@ -17,8 +18,10 @@ import (
17 // 2. Close LUKS/dm-crypt mappings (cryptroot and all others)18 // 2. Close LUKS/dm-crypt mappings (cryptroot and all others)
18 // 3. Disable swap19 // 3. Disable swap
19 // 4. Kill stuck pacstrap/arch-chroot processes20 // 4. Kill stuck pacstrap/arch-chroot processes
20-// 5. Remove installation config file21+// 5. Wipe /mnt directory contents (fresh start for next install)
21-// 6. Sync filesystems22+// 6. Remove installation config file
23+// 7. Remove archived log files (*.log.TIMESTAMP)
24+// 8. Sync filesystems
22 func Run(log *logger.Logger) error {25 func Run(log *logger.Logger) error {
23 log.Info("Starting comprehensive cleanup")26 log.Info("Starting comprehensive cleanup")
24 27
@@ -26,10 +29,12 @@ func Run(log *logger.Logger) error {
26 cleanupEncryption(log)29 cleanupEncryption(log)
27 cleanupSwap(log)30 cleanupSwap(log)
28 cleanupProcesses(log)31 cleanupProcesses(log)
32+ cleanupMountPoint(log)
29 cleanupConfig(log)33 cleanupConfig(log)
34+ cleanupOldLogs(log)
30 cleanupSync(log)35 cleanupSync(log)
31 36
32- log.Info("Cleanup complete")37+ log.Info("Cleanup complete - system ready for fresh install")
33 return nil38 return nil
34 }39 }
35 40
@@ -178,6 +183,50 @@ func cleanupProcesses(log *logger.Logger) {
178 }183 }
179 }184 }
180 185
186+// cleanupMountPoint wipes /mnt directory for a fresh install
187+func 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+
181 // cleanupConfig removes the installation config file.230 // cleanupConfig removes the installation config file.
182 func cleanupConfig(log *logger.Logger) {231 func cleanupConfig(log *logger.Logger) {
183 log.Info("Removing installation config file")232 log.Info("Removing installation config file")
@@ -192,6 +241,45 @@ func cleanupConfig(log *logger.Logger) {
192 }241 }
193 }242 }
194 243
244+// cleanupOldLogs removes archived log files (*.log.TIMESTAMP pattern).
245+func 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+
195 // cleanupSync syncs all filesystems.283 // cleanupSync syncs all filesystems.
196 func cleanupSync(log *logger.Logger) {284 func cleanupSync(log *logger.Logger) {
197 log.Info("Syncing filesystems")285 log.Info("Syncing filesystems")
M internal/logger/logger.go +37 -0
@@ -6,6 +6,7 @@ import (
66 "log/slog"
77 "os"
88 "strings"
9+ "time"
910
1011 "github.com/bnema/archup/internal/system"
1112 )
@@ -39,8 +40,44 @@ func (sw *syncWriter) Write(p []byte) (n int, err error) {
3940 return n, err
4041 }
4142
43+// archiveOldLog archives the existing log file if it exists and is not empty
44+// Creates a timestamped backup like: /var/log/archup-install.log.2025-10-26_203045
45+func archiveOldLog(logPath string) error {
46+ // Check if log file exists
47+ info, err := os.Stat(logPath)
48+ if err != nil {
49+ if os.IsNotExist(err) {
50+ // No log file to archive
51+ return nil
52+ }
53+ return fmt.Errorf("failed to stat log file: %w", err)
54+ }
55+
56+ // If log file is empty, just truncate it
57+ if info.Size() == 0 {
58+ return nil
59+ }
60+
61+ // Create archive filename with timestamp
62+ timestamp := time.Now().Format("2006-01-02_150405")
63+ archivePath := fmt.Sprintf("%s.%s", logPath, timestamp)
64+
65+ // Rename old log to archive
66+ if err := os.Rename(logPath, archivePath); err != nil {
67+ return fmt.Errorf("failed to rename log to archive: %w", err)
68+ }
69+
70+ return nil
71+}
72+
4273 // New creates a new Logger with optional file logging and dry-run mode
4374 func New(logPath string, dryRun bool) (*Logger, error) {
75+ // Archive old log if it exists and is not empty
76+ if err := archiveOldLog(logPath); err != nil {
77+ // Non-fatal error - just warn and continue
78+ fmt.Fprintf(os.Stderr, "Warning: failed to archive old log: %v\n", err)
79+ }
80+
4481 // Open log file for writing with sync flag for immediate writes
4582 logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0644)
4683 if err != nil {
@@ -6,6 +6,7 @@ import (
6 "log/slog"6 "log/slog"
7 "os"7 "os"
8 "strings"8 "strings"
9+ "time"
9 10
10 "github.com/bnema/archup/internal/system"11 "github.com/bnema/archup/internal/system"
11 )12 )
@@ -39,8 +40,44 @@ func (sw *syncWriter) Write(p []byte) (n int, err error) {
39 return n, err40 return n, err
40 }41 }
41 42
43+// archiveOldLog archives the existing log file if it exists and is not empty
44+// Creates a timestamped backup like: /var/log/archup-install.log.2025-10-26_203045
45+func archiveOldLog(logPath string) error {
46+ // Check if log file exists
47+ info, err := os.Stat(logPath)
48+ if err != nil {
49+ if os.IsNotExist(err) {
50+ // No log file to archive
51+ return nil
52+ }
53+ return fmt.Errorf("failed to stat log file: %w", err)
54+ }
55+
56+ // If log file is empty, just truncate it
57+ if info.Size() == 0 {
58+ return nil
59+ }
60+
61+ // Create archive filename with timestamp
62+ timestamp := time.Now().Format("2006-01-02_150405")
63+ archivePath := fmt.Sprintf("%s.%s", logPath, timestamp)
64+
65+ // Rename old log to archive
66+ if err := os.Rename(logPath, archivePath); err != nil {
67+ return fmt.Errorf("failed to rename log to archive: %w", err)
68+ }
69+
70+ return nil
71+}
72+
42 // New creates a new Logger with optional file logging and dry-run mode73 // New creates a new Logger with optional file logging and dry-run mode
43 func New(logPath string, dryRun bool) (*Logger, error) {74 func New(logPath string, dryRun bool) (*Logger, error) {
75+ // Archive old log if it exists and is not empty
76+ if err := archiveOldLog(logPath); err != nil {
77+ // Non-fatal error - just warn and continue
78+ fmt.Fprintf(os.Stderr, "Warning: failed to archive old log: %v\n", err)
79+ }
80+
44 // Open log file for writing with sync flag for immediate writes81 // Open log file for writing with sync flag for immediate writes
45 logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0644)82 logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND|os.O_SYNC, 0644)
46 if err != nil {83 if err != nil {
M internal/phases/repos.go +4 -0
@@ -67,6 +67,7 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
6767 switch err := p.installExtraPackages(progressChan); {
6868 case err != nil:
6969 // Don't fail the phase, just warn
70+ p.logger.Warn("Extra packages installation failed", "error", err)
7071 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))
7172 }
7273
@@ -283,15 +284,18 @@ func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) er
283284 return nil
284285 }
285286
287+ p.logger.Info("Installing extra packages", "count", len(packages), "packages", strings.Join(packages, ", "))
286288 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))
287289
288290 // Install packages
289291 installCmd := fmt.Sprintf("pacman -S --noconfirm %s", strings.Join(packages, " "))
290292 switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, installCmd); {
291293 case err != nil:
294+ p.logger.Error("Failed to install extra packages", "error", err, "packages", strings.Join(packages, ", "))
292295 return fmt.Errorf("failed to install extra packages: %w", err)
293296 }
294297
298+ p.logger.Info("Extra packages installed successfully", "count", len(packages))
295299 p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))
296300
297301 return nil
@@ -67,6 +67,7 @@ func (p *ReposPhase) Execute(progressChan chan<- ProgressUpdate) PhaseResult {
67 switch err := p.installExtraPackages(progressChan); {67 switch err := p.installExtraPackages(progressChan); {
68 case err != nil:68 case err != nil:
69 // Don't fail the phase, just warn69 // Don't fail the phase, just warn
70+ p.logger.Warn("Extra packages installation failed", "error", err)
70 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))71 p.SendOutput(progressChan, fmt.Sprintf("[WARN] Some extra packages failed: %v", err))
71 }72 }
72 73
@@ -283,15 +284,18 @@ func (p *ReposPhase) installExtraPackages(progressChan chan<- ProgressUpdate) er
283 return nil284 return nil
284 }285 }
285 286
287+ p.logger.Info("Installing extra packages", "count", len(packages), "packages", strings.Join(packages, ", "))
286 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))288 p.SendOutput(progressChan, fmt.Sprintf("Installing %d extra packages...", len(packages)))
287 289
288 // Install packages290 // Install packages
289 installCmd := fmt.Sprintf("pacman -S --noconfirm %s", strings.Join(packages, " "))291 installCmd := fmt.Sprintf("pacman -S --noconfirm %s", strings.Join(packages, " "))
290 switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, installCmd); {292 switch err := system.ChrootExec(p.logger.LogPath(),config.PathMnt, installCmd); {
291 case err != nil:293 case err != nil:
294+ p.logger.Error("Failed to install extra packages", "error", err, "packages", strings.Join(packages, ", "))
292 return fmt.Errorf("failed to install extra packages: %w", err)295 return fmt.Errorf("failed to install extra packages: %w", err)
293 }296 }
294 297
298+ p.logger.Info("Extra packages installed successfully", "count", len(packages))
295 p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))299 p.SendOutput(progressChan, fmt.Sprintf("[OK] Installed %d extra packages", len(packages)))
296 300
297 return nil301 return nil
M internal/system/exec.go +2 -0
@@ -89,6 +89,7 @@ func Run(cfg RunConfig) CommandResult {
8989 output.WriteString(line + "\n")
9090 if logFile != nil {
9191 fmt.Fprintln(logFile, line)
92+ logFile.Sync() // Flush to disk immediately
9293 }
9394 }
9495 }()
@@ -103,6 +104,7 @@ func Run(cfg RunConfig) CommandResult {
103104 output.WriteString(line + "\n")
104105 if logFile != nil {
105106 fmt.Fprintln(logFile, line)
107+ logFile.Sync() // Flush to disk immediately
106108 }
107109 }
108110 }()
@@ -89,6 +89,7 @@ func Run(cfg RunConfig) CommandResult {
89 output.WriteString(line + "\n")89 output.WriteString(line + "\n")
90 if logFile != nil {90 if logFile != nil {
91 fmt.Fprintln(logFile, line)91 fmt.Fprintln(logFile, line)
92+ logFile.Sync() // Flush to disk immediately
92 }93 }
93 }94 }
94 }()95 }()
@@ -103,6 +104,7 @@ func Run(cfg RunConfig) CommandResult {
103 output.WriteString(line + "\n")104 output.WriteString(line + "\n")
104 if logFile != nil {105 if logFile != nil {
105 fmt.Fprintln(logFile, line)106 fmt.Fprintln(logFile, line)
107+ logFile.Sync() // Flush to disk immediately
106 }108 }
107 }109 }
108 }()110 }()

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