Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -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).
|
||||
Reference in New Issue
Block a user