Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-03-03 17:43:04 -05:00
parent a405b38507
commit 5e28d0bd8c
14 changed files with 3535 additions and 8 deletions
@@ -0,0 +1,229 @@
# CLI Contract: dns-helper
**Feature Branch**: `001-safety-reliability-refactor`
**Date**: 2026-03-03
## Overview
`dns-helper` is a single-binary CLI tool that updates the local hosts file with DNS entries resolved from a user-specified DNS server. It exposes three subcommands: `add`, `delete`, and a usage/help display on invalid input.
## Binary Name
```
dns-helper[.exe]
```
## Global Behavior
- Prints banner on every invocation: `DNSHelper v1.0`, `Copyright (c) 2024 Emberkom LLC`, blank line.
- All error messages are printed to **stderr**.
- Informational output (banner, summaries, warnings) is printed to **stdout**.
- Exit code `0` on full success. Non-zero on any error or partial failure.
---
## Subcommand: `add` (aliases: `a`)
### Synopsis
```
dns-helper add -host <hostnames> -server <dns-server>
```
### Flags
| Flag | Required | Type | Description |
|------|----------|------|-------------|
| `-host` | Yes | string | Comma-separated list of hostnames to resolve and add |
| `-server` | Yes | string | DNS server to use for resolution (hostname or IP) |
### Behavior
1. Validates that both `-host` and `-server` are provided and non-empty.
2. Resolves each hostname against the specified DNS server (follows CNAME chains, deduplicates IPs).
3. Acquires lock file.
4. Reads the current hosts file and parses the managed block.
5. Removes any existing managed entries for the specified hostnames.
6. Adds new entries for all successfully resolved hostnames.
7. Writes the updated hosts file atomically (temp file + rename, with pre-write backup).
8. Releases lock file.
9. Prints summary of changes.
### Output — Success (exit 0)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
Added 3 entries for host1.example.com
Added 2 entries for host2.example.com
```
### Output — Partial Failure (exit 1)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
Added 3 entries for host1.example.com
Warning: failed to resolve host2.example.com: NXDOMAIN
```
### Output — Validation Error (exit 1)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
Error: -host flag is required for the add command
```
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | All hostnames resolved and written successfully |
| 1 | Any error: validation failure, DNS failure, file write failure, partial resolution |
---
## Subcommand: `delete` (aliases: `d`, `del`)
### Synopsis
```
dns-helper delete -host <hostnames>
dns-helper delete all
```
### Flags
| Flag | Required | Type | Description |
|------|----------|------|-------------|
| `-host` | Yes* | string | Comma-separated list of hostnames to remove (*not required when using `all`) |
### Behavior — Delete Specific Hosts
1. Validates that `-host` is provided and non-empty.
2. Acquires lock file.
3. Reads the current hosts file and parses the managed block.
4. Removes all managed entries matching the specified hostnames.
5. Writes the updated hosts file atomically.
6. Releases lock file.
7. Prints summary.
### Behavior — Delete All
1. Acquires lock file.
2. Reads the current hosts file and parses the managed block.
3. Removes the entire managed block (start marker through end marker).
4. Writes the updated hosts file atomically.
5. Releases lock file.
6. Prints summary.
### Output — Success (exit 0)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
Removed 3 entries for host1.example.com
```
### Output — No Entries Found (exit 0)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
No managed entries found for host1.example.com
```
### Output — Delete All (exit 0)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
Removed all managed entries (5 entries removed)
```
### Exit Codes
| Code | Meaning |
|------|---------|
| 0 | Deletion completed (or no entries to delete) |
| 1 | Error: validation failure, file write failure, corrupt managed block |
---
## Invalid / No Subcommand
### Synopsis
```
dns-helper
dns-helper <invalid-subcommand>
```
### Behavior
1. Prints usage information to stdout.
2. Does **not** modify the hosts file.
3. Exits with code 1.
### Output (exit 1)
```
DNSHelper v1.0
Copyright (c) 2024 Emberkom LLC
This utility will update the local hosts file with DNS entries obtained by the specified DNS server.
Usage: dns-helper [add|delete] -host hostname [-server dns.example.com]
Example:
dns-helper add -host xyz.acme.com -server dns.example.com
This will use dns.example.com to find and add all IP addresses for xyz.acme.com to the local hosts file.
dns-helper delete -host hostname.example.com
This will delete all entries for hostname.example.com from the local hosts file.
dns-helper delete all
This will delete all entries from the local hosts file that were added by this utility.
Note: Adding a hostname will first remove all entries in the hosts file that match the same hostname.
This utility will only remove entries from the hosts file that it added.
```
---
## Error Conditions
| Condition | Message (stderr) | Exit Code | Hosts File Modified? |
|-----------|-------------------|-----------|---------------------|
| No subcommand | Usage text (stdout) | 1 | No |
| Unknown subcommand | Usage text (stdout) | 1 | No |
| Missing `-host` (add) | `Error: -host flag is required for the add command` | 1 | No |
| Missing `-server` (add) | `Error: -server flag is required for the add command` | 1 | No |
| Empty `-host` value | `Error: -host flag is required for the add command` | 1 | No |
| DNS resolution failure (all hosts) | `Error: failed to resolve <host>: <reason>` | 1 | No |
| DNS resolution partial failure | Warning per host + entries for successful hosts written | 1 | Yes (partial) |
| Hosts file not found | `Error: file does not exist: <path>` | 1 | No |
| Permission denied | `Error: permission denied: <path>` | 1 | No |
| Corrupt managed block | `Error: managed block is corrupt: <details>` | 1 | No |
| Lock file unavailable | `Error: could not acquire lock file <path>: another instance may be running` | 1 | No |
| Write failure | `Error: writing hosts file: <details>` | 1 | No (atomic write) |
---
## Hosts File Managed Block Format
```
# DNSHelper <<-> START CONFIG
1.2.3.4 hostname1.example.com
5.6.7.8 hostname1.example.com
9.10.11.12 hostname2.example.com
# DNSHelper <->> END CONFIG
```
- Start marker: `# DNSHelper <<-> START CONFIG`
- End marker: `# DNSHelper <->> END CONFIG`
- Each entry: `<IPv4>\t<hostname>` (tab-separated)
- Entries are deduplicated by full line content.
- Block is omitted entirely when there are no managed entries.
@@ -0,0 +1,168 @@
# Package Contracts: dns-helper
**Feature Branch**: `001-safety-reliability-refactor`
**Date**: 2026-03-03
## Package: `resolver`
Handles DNS name resolution against a user-specified DNS server.
### Interface
```go
// Resolver performs DNS lookups against a specific server.
type Resolver interface {
// LookupIP resolves a hostname to a list of IPv4 addresses using the
// specified DNS server. Follows CNAME chains up to a maximum depth.
// Returns deduplicated IPs. Returns an error if the hostname cannot
// be resolved (NXDOMAIN, timeout, server unreachable, etc.).
LookupIP(hostname, server string) ([]string, error)
}
```
### Behavior Contract
- Queries are sent over UDP to `<server>:53`.
- The `hostname` is automatically converted to FQDN (trailing dot appended if missing).
- An A query is sent first. If A records are returned, they are collected.
- If only CNAME records are returned (no A records in the answer), the CNAME target is resolved recursively.
- CNAME chain depth is limited to 10 levels. Exceeding this returns an error.
- Results are deduplicated before return.
- Timeout: 5 seconds per query.
- On NXDOMAIN: returns `error` with descriptive message.
- On server unreachable/timeout: returns `error` with descriptive message.
- Response ID must match query ID; mismatches are treated as errors.
- Truncated responses (TC flag set) are treated as errors (no TCP fallback for simplicity).
---
## Package: `hostfile`
Manages reading, parsing, modifying, and atomically writing the hosts file.
### Interface
```go
// Manager handles hosts file operations.
type Manager interface {
// Read reads and parses the hosts file at the given path.
// Returns an error if the file cannot be read or the managed block is corrupt.
Read(path string) (*HostsFile, error)
// Write atomically writes the hosts file content to the given path.
// Creates a backup before writing. Uses temp-file-and-rename pattern.
// backupDir is the directory for the backup file (typically the exe directory).
Write(hf *HostsFile, backupDir string) error
}
// HostsFile represents the parsed content of a hosts file.
type HostsFile struct {
Path string
OriginalContent []string
PrefixContent []string
ManagedContent []string
PostfixContent []string
HasManagedBlock bool
}
```
### Sub-package: Managed Block Operations
```go
// AddEntries adds DNS entries to the managed content, removing any
// existing entries for the same hostnames first. Returns updated content.
func AddEntries(existing []string, entries []DNSEntry) []string
// RemoveByHostname removes all managed entries matching any of the
// specified hostnames. Returns updated content.
func RemoveByHostname(existing []string, hostnames []string) []string
// RemoveAll returns an empty slice (clears all managed entries).
func RemoveAll() []string
```
### Behavior Contract — Read
- Opens the file at `path` and reads all lines.
- Scans for start marker (`# DNSHelper <<-> START CONFIG`) and end marker (`# DNSHelper <->> END CONFIG`).
- If neither marker found: `HasManagedBlock = false`, all content goes to `PrefixContent`.
- If both markers found in correct order: splits content into Prefix, Managed, Postfix.
- **Corruption detection** (returns error):
- Start marker without end marker.
- End marker without start marker.
- End marker before start marker.
- Duplicate start or end markers.
- Uses sentinel values (not zero-index) for marker detection.
### Behavior Contract — Write
1. Creates backup: copies current hosts file to `<backupDir>/hosts.bak.<YYYYMMDD>-<4hex>`.
2. Assembles full file content: prefix + (managed block if non-empty) + postfix.
3. Compares assembled content with original — skips write if identical.
4. Creates temp file in same directory as hosts file: `.dns-helper-tmp-*`.
5. Writes content to temp file.
6. Sets temp file permissions to match original hosts file permissions.
7. Calls `Sync()` on temp file.
8. Closes temp file.
9. Calls `os.Rename(temp, hostsPath)`.
10. On rename success: deletes backup, returns nil.
11. On rename failure (including Windows retry 2-3x with 200ms delay): cleans up temp file, returns error. Backup is cleaned up after confirming original is intact.
---
## Package: `platform`
Provides platform-specific hosts file location.
### Interface
```go
// GetHostsFilePath returns the absolute path to the system hosts file.
// Returns an error if the file does not exist or cannot be accessed.
func GetHostsFilePath() (string, error)
```
### Platform Implementations
| Platform | Path | Detection |
|----------|------|-----------|
| Windows | `%SystemRoot%\System32\drivers\etc\hosts` (fallback: `C:\Windows\...`) | `os.LookupEnv("SystemRoot")` |
| Linux | `/etc/hosts` | Hardcoded |
| macOS | `/etc/hosts` | Hardcoded |
### Behavior Contract
- Returns error to caller on file-not-found or access error.
- **Must NOT** call `os.Exit()`, `log.Fatal()`, or any process-terminating function.
- Error messages include the path that was checked.
---
## Package: `lockfile`
Serializes concurrent access to the hosts file.
### Interface
```go
// Lock represents an acquired lock.
type Lock interface {
// Release deletes the lock file. Safe to call multiple times.
Release() error
}
// Acquire attempts to acquire a lock file in the specified directory.
// Retries for up to maxWait on contention. Breaks stale locks older
// than staleTimeout. Returns an error if the lock cannot be acquired.
func Acquire(dir string) (Lock, error)
```
### Behavior Contract
- Lock file path: `<dir>/.dns-helper.lock`.
- Created atomically via `os.OpenFile` with `O_CREATE|O_EXCL`.
- PID written to lock file for debugging (not used for staleness).
- Staleness: lock file with `ModTime` older than 2 minutes is considered stale and removed.
- Retry: up to 2 seconds total, 200ms between attempts.
- Release: `os.Remove(lockPath)`. Idempotent (no error if already deleted).
- Lock acquired **after** DNS resolution, released **after** file write (or on error).