feat: Implement platform-specific hosts file path retrieval
- Created a new `platform` package to handle OS-specific hosts file paths. - Added implementations for macOS, Linux, and Windows to retrieve the hosts file path. - Introduced tests for the `GetHostsFilePath` function to ensure correct behavior across platforms. feat: Add DNS resolver for IP lookups - Implemented a `resolver` package for performing DNS lookups. - Created a `DNSResolver` struct with a `LookupIP` method to handle A-record lookups and CNAME resolution. - Added comprehensive tests for various DNS scenarios, including CNAME chains and error handling. chore: Refactor project structure and update dependencies - Restructured the project to follow Go's standard package layout. - Updated `go.mod` to Go 1.20 and added `golang.org/x/net` dependency. - Removed old source files and ensured a clean build with all tests passing.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
package lockfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lockFileName = ".dns-helper.lock"
|
||||
staleTimeout = 2 * time.Minute
|
||||
retryInterval = 200 * time.Millisecond
|
||||
maxWait = 2 * time.Second
|
||||
)
|
||||
|
||||
// Lock represents an acquired lock file that can be released.
|
||||
type Lock interface {
|
||||
Release() error
|
||||
}
|
||||
|
||||
type fileLock struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// Release deletes the lock file. It is safe to call more than once.
|
||||
func (l *fileLock) Release() error {
|
||||
err := os.Remove(l.path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Acquire creates a lock file in dir and returns a Lock that can release it.
|
||||
// It waits up to maxWait for a stale or otherwise held lock to clear.
|
||||
func Acquire(dir string) (Lock, error) {
|
||||
lockPath := filepath.Join(dir, lockFileName)
|
||||
deadline := time.Now().Add(maxWait)
|
||||
for {
|
||||
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
|
||||
if err == nil {
|
||||
// Write PID for debugging (not used for staleness detection)
|
||||
fmt.Fprintf(f, "%d\n", os.Getpid())
|
||||
f.Close()
|
||||
return &fileLock{path: lockPath}, nil
|
||||
}
|
||||
if !os.IsExist(err) {
|
||||
return nil, fmt.Errorf("creating lock file %s: %w", lockPath, err)
|
||||
}
|
||||
// Lock file exists — check staleness
|
||||
info, statErr := os.Stat(lockPath)
|
||||
if statErr == nil && time.Since(info.ModTime()) > staleTimeout {
|
||||
os.Remove(lockPath)
|
||||
continue // Retry after breaking stale lock
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return nil, fmt.Errorf("could not acquire lock file %s: another instance may be running", lockPath)
|
||||
}
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package lockfile_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"dns-helper/lockfile"
|
||||
)
|
||||
|
||||
// T006-1: Acquire succeeds and lock file exists on disk.
|
||||
func TestAcquireSucceeds(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lock, err := lockfile.Acquire(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Acquire returned unexpected error: %v", err)
|
||||
}
|
||||
defer lock.Release()
|
||||
|
||||
lockPath := filepath.Join(dir, ".dns-helper.lock")
|
||||
if _, err := os.Stat(lockPath); os.IsNotExist(err) {
|
||||
t.Error("lock file does not exist after Acquire")
|
||||
}
|
||||
}
|
||||
|
||||
// T006-2: Release deletes the lock file.
|
||||
func TestReleaseDeletesLockFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lock, err := lockfile.Acquire(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Acquire returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := lock.Release(); err != nil {
|
||||
t.Fatalf("Release returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
lockPath := filepath.Join(dir, ".dns-helper.lock")
|
||||
if _, err := os.Stat(lockPath); !os.IsNotExist(err) {
|
||||
t.Error("lock file still exists after Release")
|
||||
}
|
||||
}
|
||||
|
||||
// T006-3: Double acquire fails with timeout error.
|
||||
func TestDoubleAcquireFails(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lock, err := lockfile.Acquire(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("first Acquire returned unexpected error: %v", err)
|
||||
}
|
||||
defer lock.Release()
|
||||
|
||||
_, err = lockfile.Acquire(dir)
|
||||
if err == nil {
|
||||
t.Fatal("second Acquire expected to fail, but it succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
// T006-4: Stale lock file (>2 minutes old) is broken and Acquire succeeds.
|
||||
func TestStaleLockBroken(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lockPath := filepath.Join(dir, ".dns-helper.lock")
|
||||
|
||||
// Manually create a lock file with a stale modification time.
|
||||
if err := os.WriteFile(lockPath, []byte("99999\n"), 0644); err != nil {
|
||||
t.Fatalf("failed to create stale lock file: %v", err)
|
||||
}
|
||||
staleTime := time.Now().Add(-3 * time.Minute)
|
||||
if err := os.Chtimes(lockPath, staleTime, staleTime); err != nil {
|
||||
t.Fatalf("failed to set stale mtime: %v", err)
|
||||
}
|
||||
|
||||
lock, err := lockfile.Acquire(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Acquire over stale lock returned unexpected error: %v", err)
|
||||
}
|
||||
lock.Release()
|
||||
}
|
||||
|
||||
// T006-5: Release is idempotent — calling twice does not return an error.
|
||||
func TestReleaseIsIdempotent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
lock, err := lockfile.Acquire(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("Acquire returned unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := lock.Release(); err != nil {
|
||||
t.Fatalf("first Release returned unexpected error: %v", err)
|
||||
}
|
||||
if err := lock.Release(); err != nil {
|
||||
t.Fatalf("second Release returned unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user