Guarantee the database is closed on every fatal exit path (closes #4)
All checks were successful
check / check (push) Successful in 57s
All checks were successful
check / check (push) Successful in 57s
fatalf called os.Exit(1), which does not run deferred functions, so every defer db.Close() was dead on the fatal path: the SQLite WAL was left uncheckpointed and the -wal/-shm sidecars were left for the next process to recover. It also made those paths impossible to exercise in-process. fatalf is gone. runScan, runReport, runTrees, loadRecords and resolveRoots return their errors, so the deferred close always runs, and the only exit point is run() in main.go. Mapping errors to exit codes needs care: cobra prints the error and the command's usage text for anything RunE returns, and main mapped every Execute() error to exit 2. A runtime failure is not a usage problem, so the runE adapter silences both for the subcommands and marks their errors fatalError; run() reports a fatalError as "sfdupes: ..." on stderr and exits 1, and leaves everything else -- cobra's own argument, flag and unknown-command errors, which cobra has already reported with its usage text -- on exit 2. The bare "sfdupes" invocation still prints usage and exits 2. Exit codes and message text are unchanged: 0 on success even with per-file warnings, 1 fatal, 2 usage, per README section "Error handling and exit codes". Everything on stdout is still data only. main_test.go drives the CLI in-process and covers all three: a fatal error raised after the database is open (a database with no files table) closes it and leaves no -wal or -shm behind for scan, report and trees; a nonexistent PATH operand is fatal, not usage, and prints no usage text; the usage errors still exit 2; and a scan that skipped an unreadable file still exits 0.
This commit is contained in:
25
scan.go
25
scan.go
@@ -49,25 +49,30 @@ type fileMeta struct {
|
||||
// under the PATH operands. Only files whose size at least one other
|
||||
// file shares are ever hashed: a size-unique file cannot be a
|
||||
// duplicate. Flag parsing and the at-least-one-operand check are done
|
||||
// by cobra.
|
||||
func runScan(roots []string, workers int, oneFS bool) {
|
||||
// by cobra. Errors are returned rather than exiting, so that the
|
||||
// deferred close — which checkpoints the SQLite WAL — always runs.
|
||||
func runScan(roots []string, workers int, oneFS bool) error {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
|
||||
roots = resolveRoots(roots)
|
||||
roots, err := resolveRoots(roots)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbPath := databasePath()
|
||||
|
||||
db, err := openScanDatabase(dbPath)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() { _ = db.Close() }()
|
||||
|
||||
st, err := syncScan(db, roots, workers, oneFS)
|
||||
if err != nil {
|
||||
fatalf("update database %s: %v", dbPath, err)
|
||||
return fmt.Errorf("update database %s: %w", dbPath, err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr,
|
||||
@@ -75,31 +80,33 @@ func runScan(roots []string, workers int, oneFS bool) {
|
||||
"%d unchanged), %d skipped\n",
|
||||
st.added+st.updated+st.unchanged, st.added, st.updated,
|
||||
st.removed, st.unchanged, st.skipped)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveRoots converts each PATH operand to an absolute, lexically
|
||||
// cleaned path (symlinks are not resolved) and verifies that it
|
||||
// exists. Database records are keyed by absolute path, so scan results
|
||||
// must not depend on the working directory.
|
||||
func resolveRoots(roots []string) []string {
|
||||
func resolveRoots(roots []string) ([]string, error) {
|
||||
abs := make([]string, 0, len(roots))
|
||||
|
||||
for _, root := range roots {
|
||||
a, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
fatalf("resolve %s: %v", root, err)
|
||||
return nil, fmt.Errorf("resolve %s: %w", root, err)
|
||||
}
|
||||
|
||||
// A nonexistent operand is a fatal error before any scanning.
|
||||
_, err = os.Lstat(a)
|
||||
if err != nil {
|
||||
fatalf("%v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
abs = append(abs, a)
|
||||
}
|
||||
|
||||
return abs
|
||||
return abs, nil
|
||||
}
|
||||
|
||||
// pruneRoots drops operands already covered by another operand:
|
||||
|
||||
Reference in New Issue
Block a user