init for public push
This commit is contained in:
commit
c2d1ac9af5
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Binaries for programs and plugins
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
|
||||||
|
# Test binary, built with `go test -c`
|
||||||
|
*.test
|
||||||
|
|
||||||
|
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||||
|
*.out
|
||||||
|
|
||||||
|
# Dependency directories (remove the comment below to include it)
|
||||||
|
# vendor/
|
||||||
|
|
||||||
|
# Go workspace file
|
||||||
|
go.work
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Project specific
|
||||||
|
/dist/
|
||||||
|
/dist-*/
|
||||||
|
/build/
|
||||||
|
/bin/
|
||||||
|
/tmp/
|
||||||
|
/logs/
|
||||||
|
*.log
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Config files (if they contain sensitive data)
|
||||||
|
config.local.json
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Test coverage
|
||||||
|
coverage.html
|
||||||
|
coverage.out
|
||||||
|
profile.out
|
||||||
|
|
||||||
|
# Air live reload
|
||||||
|
tmp/
|
152
.goreleaser.yaml
Normal file
152
.goreleaser.yaml
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
project_name: hostsync
|
||||||
|
|
||||||
|
# 构建前的准备工作
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- go mod tidy
|
||||||
|
- go generate ./...
|
||||||
|
|
||||||
|
builds:
|
||||||
|
- id: hostsync
|
||||||
|
binary: "{{ .ProjectName }}"
|
||||||
|
main: .
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
goos:
|
||||||
|
- linux
|
||||||
|
- windows
|
||||||
|
- darwin
|
||||||
|
goarch:
|
||||||
|
- "386"
|
||||||
|
- amd64
|
||||||
|
- arm
|
||||||
|
- arm64
|
||||||
|
goarm:
|
||||||
|
- "6"
|
||||||
|
- "7"
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
- goos: darwin
|
||||||
|
goarch: arm
|
||||||
|
- goos: windows
|
||||||
|
goarch: arm
|
||||||
|
ldflags:
|
||||||
|
- -s -w
|
||||||
|
- -X main.version={{ .Version }}
|
||||||
|
- -X main.commit={{ .Commit }}
|
||||||
|
- -X main.date={{ .Date }}
|
||||||
|
- -X main.builtBy=goreleaser
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
|
||||||
|
upx:
|
||||||
|
- enabled: true
|
||||||
|
# 仅在需要时启用 UPX 压缩
|
||||||
|
# 注意:UPX 压缩可能会导致某些平台不兼容
|
||||||
|
# 需要确保 UPX 已安装并在 PATH 中
|
||||||
|
compress: best
|
||||||
|
# 压缩级别,best 为最高压缩率
|
||||||
|
lzma: true
|
||||||
|
|
||||||
|
# 打包配置
|
||||||
|
archives:
|
||||||
|
- id: hostsync-archive
|
||||||
|
name_template: "{{ .ProjectName }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}-{{ . }}{{ end }}{{ if not (eq .Amd64 \"v1\") }}{{ .Amd64 }}{{ end }}"
|
||||||
|
formats: [binary]
|
||||||
|
builds_info:
|
||||||
|
mode: 0755 # 设置可执行文件权限
|
||||||
|
files:
|
||||||
|
- LICENSE
|
||||||
|
- readme.md
|
||||||
|
- install.sh
|
||||||
|
- install.ps1
|
||||||
|
|
||||||
|
checksum:
|
||||||
|
name_template: "SHA256SUMS"
|
||||||
|
algorithm: sha256
|
||||||
|
|
||||||
|
changelog:
|
||||||
|
sort: asc
|
||||||
|
filters:
|
||||||
|
exclude:
|
||||||
|
- "^docs:"
|
||||||
|
- "^test:"
|
||||||
|
- "^ci:"
|
||||||
|
- "^chore:"
|
||||||
|
- "^style:"
|
||||||
|
groups:
|
||||||
|
- title: "🚀 新功能"
|
||||||
|
regexp: "^.*feat[(\\w)]*:+.*$"
|
||||||
|
order: 0
|
||||||
|
- title: "🐛 Bug修复"
|
||||||
|
regexp: "^.*fix[(\\w)]*:+.*$"
|
||||||
|
order: 1
|
||||||
|
- title: "⚡ 性能优化"
|
||||||
|
regexp: "^.*perf[(\\w)]*:+.*$"
|
||||||
|
order: 2
|
||||||
|
- title: "📚 文档更新"
|
||||||
|
regexp: "^.*docs[(\\w)]*:+.*$"
|
||||||
|
order: 3
|
||||||
|
- title: "🔧 其他更新"
|
||||||
|
order: 999
|
||||||
|
|
||||||
|
release:
|
||||||
|
# 发布配置 - 强大的 Hosts 文件管理工具
|
||||||
|
name_template: "hostSync {{ .Tag }}"
|
||||||
|
mode: replace
|
||||||
|
header: |
|
||||||
|
## 快速安装
|
||||||
|
|
||||||
|
- **Linux/macOS**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.sh | bash
|
||||||
|
```
|
||||||
|
- **Windows**:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
irm "https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.ps1" | iex
|
||||||
|
```
|
||||||
|
|
||||||
|
- 📖 [完整文档](https://git.xykqyy.com/ljp/hostSync#readme)
|
||||||
|
- 🐛 [问题反馈](https://git.xykqyy.com/ljp/hostSync/issues)
|
||||||
|
- 💬 [讨论区](https://git.xykqyy.com/ljp/hostSync/discussions)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **权限提醒**: 由于需要修改系统 hosts 文件,请确保以适当的权限运行程序。
|
||||||
|
>
|
||||||
|
> - **Windows**: 以管理员身份运行
|
||||||
|
> - **Linux/macOS**: 使用 `sudo` 命令
|
||||||
|
>
|
||||||
|
> 在 Linux/macOS 上,初始化后会将二进制文件安装到 `~/.hostsync/` 目录,并自动设置 PATH 环境变量。
|
||||||
|
> 在 Windows 上,初始化后会将二进制文件安装到 `%USERPROFILE%\.hostsync\` 目录,并自动设置 PATH 环境变量。
|
||||||
|
>
|
||||||
|
> 如果需要卸载 HostSync,请手动删除 `~/.hostsync/` 或 `%USERPROFILE%\.hostsync\` 目录,并从 PATH 中移除相关路径。
|
||||||
|
prerelease: auto
|
||||||
|
footer: |
|
||||||
|
|
||||||
|
## 📈 校验和信息
|
||||||
|
|
||||||
|
下载文件后,可以使用以下命令验证文件完整性:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 下载校验和文件
|
||||||
|
curl -L "https://git.xykqyy.com/ljp/hostSync/releases/download/{{ .Tag }}/SHA256SUMS" -o "SHA256SUMS"
|
||||||
|
|
||||||
|
# 验证文件 (Linux/macOS)
|
||||||
|
sha256sum -c SHA256SUMS
|
||||||
|
|
||||||
|
# 验证文件 (Windows PowerShell)
|
||||||
|
Get-FileHash .\{{ .ProjectName }}.exe -Algorithm SHA256
|
||||||
|
```
|
||||||
|
|
||||||
|
**感谢使用 HostSync!** 🎉
|
||||||
|
|
||||||
|
如果您觉得这个工具有用,请给我们一个 ⭐ Star!
|
||||||
|
|
||||||
|
gitea_urls:
|
||||||
|
api: https://git.xykqyy.com/api/v1
|
||||||
|
download: https://git.xykqyy.com
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 evil7@deepwn
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
6
banner.txt
Normal file
6
banner.txt
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
_ _
|
||||||
|
| |__ ___ ___| |_ ___ _ _ _ __ ___
|
||||||
|
| '_ \ / _ \/ __| __/ __| | | | '_ \ / __|
|
||||||
|
| | | | (_) \__ \ |_\__ \ |_| | | | | (__
|
||||||
|
|_| |_|\___/|___/\__|___/\__, |_| |_|\___|
|
||||||
|
by evil7@deepwn |___/
|
120
build.ps1
Normal file
120
build.ps1
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
# .\build.ps1 [name] [version]
|
||||||
|
# 手动编译脚本,与 GoReleaser 配置保持一致
|
||||||
|
$binName = $args[0]
|
||||||
|
if (-Not $binName) {
|
||||||
|
$binName = 'hostsync' # 项目默认名称
|
||||||
|
}
|
||||||
|
|
||||||
|
# version by args or git tag
|
||||||
|
$version = $args[1]
|
||||||
|
if (-Not $version) {
|
||||||
|
try {
|
||||||
|
$version = git describe --tags --abbrev=0 2>&1 | Where-Object { $_.GetType().Name -eq 'String' }
|
||||||
|
# only use version number \d+\.\d+\.\d+ format
|
||||||
|
if ($version -match 'v(\d+\.\d+\.\d+)') {
|
||||||
|
$version = "$($matches[1])"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$version = $null # 如果没有匹配到版本号,则设置为 null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$version = $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# use short commit hash if no tag
|
||||||
|
if (-Not $version) {
|
||||||
|
$version = "$(git rev-parse --short HEAD)"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Building $binName $version" -ForegroundColor Green
|
||||||
|
|
||||||
|
# output dir
|
||||||
|
$outputDir = "dist-${version}"
|
||||||
|
Remove-Item -Recurse -Force $outputDir -ErrorAction SilentlyContinue | Out-Null
|
||||||
|
New-Item -ItemType Directory -Path $outputDir -ErrorAction Stop | Out-Null
|
||||||
|
|
||||||
|
# check if upx is available
|
||||||
|
$upxAvailable = $false
|
||||||
|
try {
|
||||||
|
$null = Get-Command upx -ErrorAction Stop
|
||||||
|
$upxAvailable = $true
|
||||||
|
Write-Host 'UPX detected, will compress binaries after build'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host 'UPX not found, binaries will not be compressed'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 与 GoReleaser 配置完全一致的平台组合
|
||||||
|
$targets = @(
|
||||||
|
# Darwin (macOS) - 忽略 386 和 arm
|
||||||
|
@{ GOOS = 'darwin'; GOARCH = 'amd64'; GOARM = ''; EXT = ''; Name = 'darwin-amd64' },
|
||||||
|
@{ GOOS = 'darwin'; GOARCH = 'arm64'; GOARM = ''; EXT = ''; Name = 'darwin-arm64' },
|
||||||
|
|
||||||
|
# Linux - 支持所有架构
|
||||||
|
@{ GOOS = 'linux'; GOARCH = '386'; GOARM = ''; EXT = ''; Name = 'linux-386' },
|
||||||
|
@{ GOOS = 'linux'; GOARCH = 'amd64'; GOARM = ''; EXT = ''; Name = 'linux-amd64' },
|
||||||
|
@{ GOOS = 'linux'; GOARCH = 'arm'; GOARM = '6'; EXT = ''; Name = 'linux-armv6' },
|
||||||
|
@{ GOOS = 'linux'; GOARCH = 'arm'; GOARM = '7'; EXT = ''; Name = 'linux-armv7' },
|
||||||
|
@{ GOOS = 'linux'; GOARCH = 'arm64'; GOARM = ''; EXT = ''; Name = 'linux-arm64' },
|
||||||
|
|
||||||
|
# Windows - 忽略 arm (32位)
|
||||||
|
@{ GOOS = 'windows'; GOARCH = '386'; GOARM = ''; EXT = '.exe'; Name = 'windows-386' },
|
||||||
|
@{ GOOS = 'windows'; GOARCH = 'amd64'; GOARM = ''; EXT = '.exe'; Name = 'windows-amd64' },
|
||||||
|
@{ GOOS = 'windows'; GOARCH = 'arm64'; GOARM = ''; EXT = '.exe'; Name = 'windows-arm64' }
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($target in $targets) {
|
||||||
|
$index = $targets.IndexOf($target)
|
||||||
|
$total = $targets.Count
|
||||||
|
$prg = '{0:P0}' -f ($index / $total)
|
||||||
|
|
||||||
|
# 构建符合 GoReleaser 规范的文件名
|
||||||
|
$output = "$outputDir\$binName-$version-$($target.Name)$($target.EXT)"
|
||||||
|
$time = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
|
||||||
|
Write-Host -NoNewline ("`r[$prg] Building $output ...").PadRight($host.ui.rawui.windowsize.width / 2, ' ')$time
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
$env:GOOS = $target.GOOS
|
||||||
|
$env:GOARCH = $target.GOARCH
|
||||||
|
if ($target.GOARM) {
|
||||||
|
$env:GOARM = $target.GOARM
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$env:GOARM = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
# 构建
|
||||||
|
try {
|
||||||
|
$commitHash = git rev-parse --short HEAD 2>&1 | Where-Object { $_.GetType().Name -eq 'String' }
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$commitHash = 'unknown'
|
||||||
|
}
|
||||||
|
$ldflags = "-s -w -X main.version=$version -X main.commit=$commitHash -X main.date=$(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ') -X main.builtBy=manual"
|
||||||
|
go build -ldflags="$ldflags" -trimpath -o $output # compress with upx if available
|
||||||
|
if ($upxAvailable) {
|
||||||
|
$time = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
Write-Host -NoNewline ("`r[$prg] Compressing $output ...").PadRight($host.ui.rawui.windowsize.width / 2, ' ')$time
|
||||||
|
upx --best --lzma -q $output | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$time = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||||
|
Write-Host -NoNewline ("`r[100%] All done.").padright($host.ui.rawui.windowsize.width / 2, ' ')$time
|
||||||
|
|
||||||
|
# reset env
|
||||||
|
$env:GOOS = $env:GOARCH = $env:GOARM = ''
|
||||||
|
|
||||||
|
# 生成校验和文件,文件名与 GoReleaser 一致
|
||||||
|
Write-Host "`n📋 Generating checksums..." -ForegroundColor Yellow
|
||||||
|
$checksumFile = "$outputDir\SHA256SUMS"
|
||||||
|
$hash = Get-FileHash -Algorithm SHA256 -Path $outputDir\* | Where-Object { $_.Path -notlike '*SHA256SUMS' }
|
||||||
|
$hash | ForEach-Object {
|
||||||
|
$filename = Split-Path $_.Path -Leaf
|
||||||
|
$_.Hash.ToLower() + ' ' + $filename
|
||||||
|
} | Out-File -FilePath $checksumFile -Encoding utf8
|
||||||
|
|
||||||
|
Write-Host "✅ Build completed. Files generated in $outputDir" -ForegroundColor Green
|
||||||
|
Write-Host "📁 Total files: $($hash.Count + 1) (including checksum)" -ForegroundColor Cyan
|
127
build.sh
Normal file
127
build.sh
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# ./build.sh [name] [version]
|
||||||
|
# 手动编译脚本,与 GoReleaser 配置保持一致
|
||||||
|
|
||||||
|
BIN_NAME=$1
|
||||||
|
if [ -z "$BIN_NAME" ]; then
|
||||||
|
BIN_NAME="hostsync" # 项目默认名称
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$2
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||||
|
# only use tags that start with 'v' and match semantic versioning
|
||||||
|
# then use the number part as version
|
||||||
|
# e.g. v1.2.3 => 1.2.3
|
||||||
|
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
VERSION=""
|
||||||
|
else
|
||||||
|
VERSION="${VERSION#v}" # 去掉 'v' 前缀
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# use short commit hash if no tag
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION="$(git rev-parse --short HEAD)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building $BIN_NAME $VERSION"
|
||||||
|
|
||||||
|
OUTPUT_DIR="dist-$VERSION"
|
||||||
|
|
||||||
|
# output dir
|
||||||
|
rm -rf "$OUTPUT_DIR" >/dev/null 2>&1
|
||||||
|
mkdir -p "$OUTPUT_DIR" >/dev/null 2>&1
|
||||||
|
|
||||||
|
# check if upx is available
|
||||||
|
UPX_AVAILABLE=false
|
||||||
|
if command -v upx >/dev/null 2>&1; then
|
||||||
|
UPX_AVAILABLE=true
|
||||||
|
echo "UPX detected, will compress binaries after build"
|
||||||
|
else
|
||||||
|
echo "UPX not found, binaries will not be compressed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 与 GoReleaser 配置完全一致的平台组合
|
||||||
|
declare -A targets=(
|
||||||
|
# Darwin (macOS) - 忽略 386 和 arm
|
||||||
|
["darwin-amd64"]=""
|
||||||
|
["darwin-arm64"]=""
|
||||||
|
|
||||||
|
# Linux - 支持所有架构,包括 ARM 版本
|
||||||
|
["linux-386"]=""
|
||||||
|
["linux-amd64"]=""
|
||||||
|
["linux-arm-v6"]="" # goarm=6
|
||||||
|
["linux-arm-v7"]="" # goarm=7
|
||||||
|
["linux-arm64"]=""
|
||||||
|
|
||||||
|
# Windows - 忽略 arm (32位)
|
||||||
|
["windows-386"]=".exe"
|
||||||
|
["windows-amd64"]=".exe"
|
||||||
|
["windows-arm64"]=".exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
# build
|
||||||
|
k=1
|
||||||
|
total=${#targets[@]}
|
||||||
|
for target_key in "${!targets[@]}"; do
|
||||||
|
time=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# 解析目标平台信息
|
||||||
|
if [[ "$target_key" == *"-arm-"* ]]; then
|
||||||
|
# 处理 ARM 版本,如 linux-arm-v6, linux-arm-v7
|
||||||
|
IFS='-' read -r GOOS GOARCH GOARM_VER <<<"$target_key"
|
||||||
|
GOARM=${GOARM_VER#v} # 移除 'v' 前缀
|
||||||
|
NAME_SUFFIX="armv$GOARM"
|
||||||
|
else
|
||||||
|
# 普通平台,如 linux-amd64, windows-386
|
||||||
|
IFS='-' read -r GOOS GOARCH <<<"$target_key"
|
||||||
|
GOARM=""
|
||||||
|
NAME_SUFFIX="$GOARCH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
EXT="${targets[$target_key]}"
|
||||||
|
|
||||||
|
# 构建符合 GoReleaser 规范的文件名
|
||||||
|
OUTPUT="$OUTPUT_DIR/${BIN_NAME}-${VERSION}-${GOOS}-${NAME_SUFFIX}${EXT}"
|
||||||
|
|
||||||
|
# padding with spaces half of the terminal width
|
||||||
|
COLUMNS=$(tput cols 2>/dev/null || echo 80)
|
||||||
|
PADWIDTH=$((COLUMNS / 2))
|
||||||
|
OUTPUT_MSG=$(printf "%-${PADWIDTH}s" "Building $OUTPUT ...")
|
||||||
|
prg=$(echo "scale=2; ($k * 100) / $total" | bc 2>/dev/null || echo "$k")
|
||||||
|
printf "\r[%3.0f%%] %s%s" "$prg" "$OUTPUT_MSG" "$time"
|
||||||
|
|
||||||
|
# 设置环境变量并构建
|
||||||
|
COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
LDFLAGS="-s -w -X main.version=$VERSION -X main.commit=$COMMIT -X main.date=$DATE -X main.builtBy=manual"
|
||||||
|
|
||||||
|
env GOOS=$GOOS GOARCH=$GOARCH GOARM=$GOARM go build -ldflags="$LDFLAGS" -trimpath -o "$OUTPUT" >/dev/null 2>&1
|
||||||
|
|
||||||
|
# compress with upx if available
|
||||||
|
if [ "$UPX_AVAILABLE" = true ]; then
|
||||||
|
OUTPUT_MSG=$(printf "%-${PADWIDTH}s" "Compressing $OUTPUT ...")
|
||||||
|
time=$(date "+%Y-%m-%d %H:%M:%S")
|
||||||
|
printf "\r[%3.0f%%] %s%s" "$prg" "$OUTPUT_MSG" "$time"
|
||||||
|
upx --best --lzma -q "$OUTPUT" >/dev/null 2>&1 || echo -e "\nWarning: Failed to compress $OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
k=$((k + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "\r%-${PADWIDTH}s%s\n" "[100%] All done." "$(date "+%Y-%m-%d %H:%M:%S")"
|
||||||
|
|
||||||
|
# reset env
|
||||||
|
unset BIN_NAME VERSION OUTPUT_DIR targets time GOOS GOARCH GOARM EXT UPX_AVAILABLE
|
||||||
|
unset COMMIT DATE LDFLAGS OUTPUT_MSG PADWIDTH COLUMNS prg k total target_key GOARM_VER NAME_SUFFIX
|
||||||
|
|
||||||
|
# 生成校验和文件,文件名与 GoReleaser 一致
|
||||||
|
echo ""
|
||||||
|
echo "📋 Generating checksums..."
|
||||||
|
CHECKSUM_FILE="$OUTPUT_DIR/SHA256SUMS"
|
||||||
|
cd "$OUTPUT_DIR" && sha256sum * | grep -v SHA256SUMS >"$(basename "$CHECKSUM_FILE")" && cd ..
|
||||||
|
|
||||||
|
echo "✅ Build completed. Files generated in $OUTPUT_DIR"
|
||||||
|
echo "📁 Total files: $(ls -1 "$OUTPUT_DIR" | wc -l) (including checksum)"
|
66
cmd/add.go
Normal file
66
cmd/add.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// addCmd 添加命令
|
||||||
|
var addCmd = &cobra.Command{
|
||||||
|
Use: "add <blockName> <domain> [ip]",
|
||||||
|
Short: "添加域名记录",
|
||||||
|
Long: `添加域名记录到指定块。如果不存在该块则会自动创建。
|
||||||
|
如果不指定IP地址,将自动解析域名。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync add github github.com 140.82.114.3 # 指定IP添加
|
||||||
|
hostsync add github api.github.com # 自动解析IP添加`,
|
||||||
|
Args: cobra.RangeArgs(2, 3),
|
||||||
|
Run: runAdd,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runAdd(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
blockName := args[0]
|
||||||
|
domain := args[1]
|
||||||
|
var ip string
|
||||||
|
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 3 {
|
||||||
|
// 使用指定的IP
|
||||||
|
ip = args[2]
|
||||||
|
} else {
|
||||||
|
// 自动解析IP
|
||||||
|
utils.LogInfo("正在解析域名: %s", domain)
|
||||||
|
resolver := core.NewDNSResolver()
|
||||||
|
var err error
|
||||||
|
ip, err = resolver.ResolveDomain(domain, "", "")
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("解析域名失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
utils.LogSuccess("解析成功: %s -> %s", domain, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hm.AddEntry(blockName, domain, ip); err != nil {
|
||||||
|
utils.LogError("添加记录失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已添加记录: %s -> %s (块: %s)", domain, ip, blockName)
|
||||||
|
}
|
180
cmd/cron.go
Normal file
180
cmd/cron.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cronCmd 定时任务命令
|
||||||
|
var cronCmd = &cobra.Command{
|
||||||
|
Use: "cron [blockName] [cronExpression]",
|
||||||
|
Short: "管理定时任务", Long: `管理块的定时任务配置。使用6字段cron表达式格式,支持秒级精度。
|
||||||
|
|
||||||
|
cron表达式格式: 秒 分 时 日 月 星期
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync cron # 列出所有定时任务(默认)
|
||||||
|
hostsync cron list # 列出所有定时任务
|
||||||
|
hostsync cron github "0 0 0 * * *" # 设置每日午夜更新
|
||||||
|
hostsync cron github "*/30 * * * * *" # 每30秒更新一次
|
||||||
|
hostsync cron github # 清除定时任务
|
||||||
|
|
||||||
|
常用cron表达式:
|
||||||
|
0 0 0 * * * # 每天午夜
|
||||||
|
0 0 */4 * * * # 每4小时
|
||||||
|
0 0 9 * * 1 # 每周一上午9点
|
||||||
|
0 */10 * * * * # 每10分钟
|
||||||
|
*/30 * * * * * # 每30秒
|
||||||
|
0 */5 * * * * # 每5分钟
|
||||||
|
0 0 12 * * * # 每天中午12点`,
|
||||||
|
Args: cobra.RangeArgs(0, 2), // 修改为允许0-2个参数
|
||||||
|
Run: runCron,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCron(cmd *cobra.Command, args []string) {
|
||||||
|
// 如果没有参数或第一个参数是"list",显示定时任务列表
|
||||||
|
if len(args) == 0 || (len(args) == 1 && args[0] == "list") {
|
||||||
|
listCronJobs()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
blockName := args[0]
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogError("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有两个参数,先验证cron表达式(不需要管理员权限)
|
||||||
|
if len(args) == 2 {
|
||||||
|
cronExpr := args[1]
|
||||||
|
cronManager := core.NewCronManager()
|
||||||
|
if err := cronManager.ValidateCronExpression(cronExpr); err != nil {
|
||||||
|
utils.LogError("无效的cron表达式: %v", err)
|
||||||
|
utils.LogInfo("cron表达式格式: 秒 分 时 日 月 星期")
|
||||||
|
utils.LogInfo("常用示例:")
|
||||||
|
utils.LogInfo(" \"0 0 0 * * *\" - 每日午夜")
|
||||||
|
utils.LogInfo(" \"0 */30 * * * *\" - 每30分钟")
|
||||||
|
utils.LogInfo(" \"*/30 * * * * *\" - 每30秒")
|
||||||
|
utils.LogInfo("更多信息: https://pkg.go.dev/github.com/robfig/cron")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过后,检查管理员权限
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
utils.LogWarning("块不存在: %s", blockName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 1 {
|
||||||
|
// 清除定时任务
|
||||||
|
clearCronJob(hm, blockName)
|
||||||
|
} else {
|
||||||
|
// 设置定时任务
|
||||||
|
cronExpr := args[1]
|
||||||
|
setCronJob(hm, blockName, cronExpr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setCronJob(hm *core.HostsManager, blockName, cronExpr string) {
|
||||||
|
// cron表达式已经在上层验证过了,这里直接设置
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
block.CronJob = cronExpr
|
||||||
|
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
utils.LogError("保存配置失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已设置定时任务: %s (%s)", blockName, cronExpr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearCronJob(hm *core.HostsManager, blockName string) {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block.CronJob == "" {
|
||||||
|
utils.LogInfo("块 %s 没有设置定时任务", blockName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
block.CronJob = ""
|
||||||
|
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
utils.LogError("保存配置失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已清除定时任务: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listCronJobs() {
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集有定时任务的块名称
|
||||||
|
var cronBlockNames []string
|
||||||
|
for name, block := range hm.Blocks {
|
||||||
|
if block.CronJob != "" {
|
||||||
|
cronBlockNames = append(cronBlockNames, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cronBlockNames) == 0 {
|
||||||
|
utils.LogInfo("没有找到任何定时任务")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对块名称进行排序,保持与 list 命令一致
|
||||||
|
sort.Strings(cronBlockNames)
|
||||||
|
|
||||||
|
utils.LogResult("定时任务列表 (%d 个)\n", len(cronBlockNames))
|
||||||
|
|
||||||
|
headers := []string{"块名称", "Cron表达式", "最后更新", "下次执行"}
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
|
for _, name := range cronBlockNames {
|
||||||
|
block := hm.Blocks[name]
|
||||||
|
lastUpdate := "从未更新"
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
lastUpdate = block.UpdateAt.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下次执行时间(简化版本)
|
||||||
|
nextRun := calculateNextRun(block.CronJob)
|
||||||
|
rows = append(rows, []string{block.Name, block.CronJob, lastUpdate, nextRun})
|
||||||
|
}
|
||||||
|
// 设置合适的列宽
|
||||||
|
columnWidths := []int{12, 20, 17, 20}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateNextRun 计算下次执行时间
|
||||||
|
func calculateNextRun(cronExpr string) string {
|
||||||
|
// 使用robfig/cron解析器解析6字段cron表达式
|
||||||
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
schedule, err := parser.Parse(cronExpr)
|
||||||
|
if err != nil {
|
||||||
|
return "解析错误"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算下次执行时间
|
||||||
|
nextTime := schedule.Next(time.Now())
|
||||||
|
return nextTime.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
45
cmd/disable.go
Normal file
45
cmd/disable.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// disableCmd 禁用命令
|
||||||
|
var disableCmd = &cobra.Command{
|
||||||
|
Use: "disable <blockName>",
|
||||||
|
Short: "禁用指定块",
|
||||||
|
Long: `禁用指定的配置块,其中的域名记录将被注释停用但不删除。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync disable github # 禁用 github 块`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runDisable,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runDisable(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
blockName := args[0]
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hm.DisableBlock(blockName); err != nil {
|
||||||
|
utils.LogError("禁用块失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已禁用块: %s", blockName)
|
||||||
|
}
|
45
cmd/enable.go
Normal file
45
cmd/enable.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// enableCmd 启用命令
|
||||||
|
var enableCmd = &cobra.Command{
|
||||||
|
Use: "enable <blockName>",
|
||||||
|
Short: "启用指定块",
|
||||||
|
Long: `启用指定的配置块,使其中的所有域名记录生效。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync enable github # 启用 github 块`,
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Run: runEnable,
|
||||||
|
}
|
||||||
|
|
||||||
|
func runEnable(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
blockName := args[0]
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := hm.EnableBlock(blockName); err != nil {
|
||||||
|
utils.LogError("启用块失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已启用块: %s", blockName)
|
||||||
|
}
|
100
cmd/format.go
Normal file
100
cmd/format.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// formatCmd 格式化命令
|
||||||
|
var formatCmd = &cobra.Command{
|
||||||
|
Use: "format [blockName]",
|
||||||
|
Short: "格式化配置文件",
|
||||||
|
Long: `格式化hosts配置文件,整理缩进和间距。
|
||||||
|
可以格式化整个文件或指定块。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync format # 格式化整个文件
|
||||||
|
hostsync format github # 格式化指定块
|
||||||
|
hostsync format --sort # 格式化并排序`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortEntries bool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
formatCmd.Flags().BoolVar(&sortEntries, "sort", false, "格式化并排序")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runFormat(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// 格式化整个文件
|
||||||
|
formatAllBlocks(hm)
|
||||||
|
} else {
|
||||||
|
// 格式化指定块
|
||||||
|
blockName := args[0]
|
||||||
|
formatBlock(hm, blockName)
|
||||||
|
}
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
utils.LogError("保存文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("格式化完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAllBlocks(hm *core.HostsManager) {
|
||||||
|
utils.LogInfo("格式化所有块 (%d 个)", len(hm.Blocks))
|
||||||
|
|
||||||
|
if sortEntries {
|
||||||
|
// 排序所有块中的条目
|
||||||
|
for _, block := range hm.Blocks {
|
||||||
|
sortBlockEntries(block)
|
||||||
|
}
|
||||||
|
utils.LogInfo("已按域名排序")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatBlock(hm *core.HostsManager, blockName string) {
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
utils.LogError("块不存在: %s", blockName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("格式化块: %s", blockName)
|
||||||
|
|
||||||
|
if sortEntries {
|
||||||
|
sortBlockEntries(block)
|
||||||
|
utils.LogInfo("已按域名排序")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortBlockEntries(block *core.HostBlock) {
|
||||||
|
// 按域名排序
|
||||||
|
for i := 0; i < len(block.Entries)-1; i++ {
|
||||||
|
for j := i + 1; j < len(block.Entries); j++ {
|
||||||
|
if strings.Compare(block.Entries[i].Domain, block.Entries[j].Domain) > 0 {
|
||||||
|
block.Entries[i], block.Entries[j] = block.Entries[j], block.Entries[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
532
cmd/init.go
Normal file
532
cmd/init.go
Normal file
@ -0,0 +1,532 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/service"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
initForce bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// initCmd 初始化命令
|
||||||
|
var initCmd = &cobra.Command{
|
||||||
|
Use: "init",
|
||||||
|
Short: "初始化系统配置",
|
||||||
|
Long: `初始化HostSync系统配置,包括:
|
||||||
|
|
||||||
|
- 在用户目录下创建 .hostsync 文件夹
|
||||||
|
- 安装程序到用户目录(如果不在用户目录运行则复制)
|
||||||
|
- 设置环境变量和别名(方便直接调用)
|
||||||
|
- 解析现有hosts文件(现有内容归入default块)
|
||||||
|
- 创建配置文件和服务器配置
|
||||||
|
- 注册系统服务(Windows服务/Linux systemd)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync init # 初始化系统
|
||||||
|
hostsync init --force # 强制重新初始化`,
|
||||||
|
Run: runInit,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initCmd.Flags().BoolVar(&initForce, "force", false, "强制重新初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInit(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("开始初始化HostSync (force=%t)", initForce)
|
||||||
|
|
||||||
|
// 1. 获取用户目录和程序安装路径
|
||||||
|
userDir, installDir, err := getInstallPaths()
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("获取安装路径失败: %v", err)
|
||||||
|
utils.LogError("获取安装路径失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
utils.LogInfo("用户配置目录: %s", userDir)
|
||||||
|
utils.LogInfo("程序安装目录: %s", installDir)
|
||||||
|
|
||||||
|
// 2. 创建用户配置目录
|
||||||
|
if err := createUserConfigDirs(userDir); err != nil {
|
||||||
|
utils.LogError("创建用户配置目录失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 安装程序文件到用户目录
|
||||||
|
if err := installProgram(installDir); err != nil {
|
||||||
|
utils.LogError("安装程序失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 设置环境变量和别名
|
||||||
|
if err := setupEnvironment(installDir); err != nil {
|
||||||
|
utils.LogError("设置环境失败: %v", err)
|
||||||
|
utils.LogInfo("💡 提示: 你可能需要手动将程序目录添加到 PATH 环境变量")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 创建配置文件
|
||||||
|
if err := createConfigFiles(userDir); err != nil {
|
||||||
|
utils.LogError("创建配置文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} // 6. 解析现有hosts文件
|
||||||
|
if err := parseExistingHosts(); err != nil {
|
||||||
|
utils.LogError("解析hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// 7. 注册系统服务
|
||||||
|
if err := registerSystemService(installDir); err != nil {
|
||||||
|
if initForce {
|
||||||
|
utils.LogError("注册系统服务失败: %v", err)
|
||||||
|
utils.LogInfo("💡 提示: 可以稍后手动使用 'hostsync service install' 安装服务")
|
||||||
|
} else {
|
||||||
|
utils.LogError("注册系统服务失败: %v", err)
|
||||||
|
utils.LogInfo("💡 提示: 使用 'hostsync init --force' 强制重新初始化服务")
|
||||||
|
utils.LogInfo("💡 或者稍后手动使用 'hostsync service install' 安装服务")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("系统服务注册成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("HostSync 初始化完成!")
|
||||||
|
utils.LogInfo("接下来你可以:")
|
||||||
|
utils.LogInfo(" - 使用 'hostsync list' 查看现有配置")
|
||||||
|
utils.LogInfo(" - 使用 'hostsync add <block> <domain>' 添加新域名")
|
||||||
|
utils.LogInfo(" - 使用 'hostsync service status' 查看服务状态")
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
utils.LogWarning("重启命令行窗口以生效环境变量设置")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createConfigFiles(userDir string) error {
|
||||||
|
utils.LogInfo("创建配置文件...")
|
||||||
|
|
||||||
|
configDir := filepath.Join(userDir, "config")
|
||||||
|
// 创建主配置文件 - 使用JSON编码避免转义问题
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"hostsPath": utils.GetHostsPath(),
|
||||||
|
"backupCount": 5,
|
||||||
|
"dnsTimeout": 5000,
|
||||||
|
"maxConcurrent": 10,
|
||||||
|
"logLevel": "info",
|
||||||
|
"LogPath": filepath.Join(userDir, "logs"),
|
||||||
|
}
|
||||||
|
configBytes, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("生成配置文件内容失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := filepath.Join(configDir, "config.json")
|
||||||
|
if !utils.FileExists(configPath) || initForce {
|
||||||
|
if err := utils.WriteFile(configPath, configBytes); err != nil {
|
||||||
|
return fmt.Errorf("创建配置文件失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 创建DNS服务器配置
|
||||||
|
serversPath := filepath.Join(configDir, "servers.json")
|
||||||
|
if !utils.FileExists(serversPath) || initForce {
|
||||||
|
// 创建默认DNS服务器配置
|
||||||
|
serversContent := `[
|
||||||
|
{
|
||||||
|
"Name": "Cloudflare",
|
||||||
|
"Dns": "1.1.1.1",
|
||||||
|
"Doh": "https://1.1.1.1/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Google",
|
||||||
|
"Dns": "8.8.8.8",
|
||||||
|
"Doh": "https://8.8.4.4/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OneDNS",
|
||||||
|
"Dns": "117.50.10.10",
|
||||||
|
"Doh": "https://doh.onedns.net/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AdGuard",
|
||||||
|
"Dns": "94.140.14.14",
|
||||||
|
"Doh": "https://94.140.14.14/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Yandex",
|
||||||
|
"Dns": "77.88.8.8",
|
||||||
|
"Doh": "https://77.88.8.8/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DNSPod",
|
||||||
|
"Dns": "119.29.29.29",
|
||||||
|
"Doh": "https://doh.pub/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dns.sb",
|
||||||
|
"Dns": "185.222.222.222",
|
||||||
|
"Doh": "https://185.222.222.222/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Quad101",
|
||||||
|
"Dns": "101.101.101.101",
|
||||||
|
"Doh": "https://101.101.101.101/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Quad9",
|
||||||
|
"Dns": "9.9.9.9",
|
||||||
|
"Doh": "https://9.9.9.9/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OpenDNS",
|
||||||
|
"Dns": "208.67.222.222",
|
||||||
|
"Doh": "https://208.67.222.222/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliDNS",
|
||||||
|
"Dns": "223.5.5.5",
|
||||||
|
"Doh": "https://223.5.5.5/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Applied",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://doh.applied-privacy.net/query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "cira.ca",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://private.canadianshield.cira.ca/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ControlD",
|
||||||
|
"Dns": "76.76.2.0",
|
||||||
|
"Doh": "https://dns.controld.com/p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "switch.ch",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://dns.switch.ch/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dnswarden",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://dns.dnswarden.com/uncensored"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OSZX",
|
||||||
|
"Dns": "217.160.156.119",
|
||||||
|
"Doh": "https://dns.oszx.co/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Tiarap",
|
||||||
|
"Dns": "174.138.21.128",
|
||||||
|
"Doh": "https://doh.tiar.app/dns-query"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
`
|
||||||
|
if err := utils.WriteFile(serversPath, []byte(serversContent)); err != nil {
|
||||||
|
return fmt.Errorf("创建DNS服务器配置失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("配置文件创建完成: %s", configDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseExistingHosts() error {
|
||||||
|
utils.LogInfo("解析现有hosts文件...")
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到任何块,说明这是一个全新的hosts文件
|
||||||
|
if len(hm.Blocks) == 0 {
|
||||||
|
utils.LogInfo("💡 检测到全新的hosts文件,已准备就绪")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("hosts文件解析完成,发现 %d 个配置块", len(hm.Blocks))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerSystemService(installDir string) error {
|
||||||
|
utils.LogInfo("注册系统服务...")
|
||||||
|
|
||||||
|
// 获取目标执行文件路径
|
||||||
|
var execPath string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
execPath = filepath.Join(installDir, "hostsync.exe")
|
||||||
|
} else {
|
||||||
|
execPath = filepath.Join(installDir, "hostsync")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保文件存在
|
||||||
|
if !utils.FileExists(execPath) {
|
||||||
|
return fmt.Errorf("执行文件不存在: %s", execPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
// 如果是强制初始化,确保完全停止和卸载现有服务
|
||||||
|
if initForce {
|
||||||
|
utils.LogInfo("强制模式:确保服务完全清理...")
|
||||||
|
// 先检查服务状态(可能在 installProgram 中已经卸载了)
|
||||||
|
status, err := serviceManager.Status()
|
||||||
|
if err == nil && status != service.StatusNotInstalled {
|
||||||
|
// 服务仍然存在,需要停止和卸载
|
||||||
|
utils.LogInfo("停止现有服务...")
|
||||||
|
if err := serviceManager.Stop(); err != nil {
|
||||||
|
utils.LogWarning("停止服务时遇到问题: %v", err) // 继续尝试卸载,可能服务已经停止
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("服务已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待服务完全停止
|
||||||
|
utils.LogInfo("等待服务完全停止...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 卸载服务
|
||||||
|
utils.LogInfo("卸载现有服务...")
|
||||||
|
if err := serviceManager.Uninstall(); err != nil {
|
||||||
|
return fmt.Errorf("卸载服务失败: %v", err)
|
||||||
|
}
|
||||||
|
utils.LogSuccess("服务已卸载")
|
||||||
|
|
||||||
|
// 再次等待,确保系统完全释放相关资源
|
||||||
|
utils.LogInfo("等待系统释放资源...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("服务已清理或未安装")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装服务
|
||||||
|
utils.LogInfo("安装系统服务...")
|
||||||
|
if err := serviceManager.Install(execPath); err != nil {
|
||||||
|
// 如果不是强制模式且服务已存在,这是正常情况
|
||||||
|
if !initForce && strings.Contains(err.Error(), "已存在") {
|
||||||
|
utils.LogInfo("💡 服务已存在,跳过安装")
|
||||||
|
// 即使服务已存在,也尝试启动它
|
||||||
|
if err := serviceManager.Start(); err != nil {
|
||||||
|
utils.LogWarning("启动服务失败: %v", err)
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("服务已启动")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("安装服务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安装成功后自动启动服务
|
||||||
|
utils.LogInfo("启动系统服务...")
|
||||||
|
if err := serviceManager.Start(); err != nil {
|
||||||
|
utils.LogWarning("启动服务失败: %v", err)
|
||||||
|
utils.LogInfo("💡 提示: 可以稍后手动使用 'hostsync service start' 启动服务")
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("系统服务已启动")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstallPaths 获取安装路径
|
||||||
|
func getInstallPaths() (userDir, installDir string, err error) {
|
||||||
|
// 获取当前用户目录
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("获取当前用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户配置目录
|
||||||
|
userDir = filepath.Join(currentUser.HomeDir, ".hostsync")
|
||||||
|
|
||||||
|
// 程序安装目录(在用户目录下)
|
||||||
|
installDir = filepath.Join(userDir, "bin")
|
||||||
|
|
||||||
|
return userDir, installDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createUserConfigDirs 创建用户配置目录
|
||||||
|
func createUserConfigDirs(userDir string) error {
|
||||||
|
utils.LogInfo("创建用户配置目录...")
|
||||||
|
dirs := []string{
|
||||||
|
userDir,
|
||||||
|
filepath.Join(userDir, "bin"),
|
||||||
|
filepath.Join(userDir, "config"),
|
||||||
|
filepath.Join(userDir, "logs"),
|
||||||
|
filepath.Join(userDir, "backup"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建目录 %s 失败: %v", dir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("用户配置目录创建完成: %s", userDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installProgram 安装程序到用户目录
|
||||||
|
func installProgram(installDir string) error {
|
||||||
|
utils.LogInfo("安装程序文件...")
|
||||||
|
|
||||||
|
// 获取当前执行文件路径
|
||||||
|
currentExecPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取当前执行文件路径失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentExecPath, err = filepath.Abs(currentExecPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取绝对路径失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 目标执行文件路径
|
||||||
|
var targetExecPath string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
targetExecPath = filepath.Join(installDir, "hostsync.exe")
|
||||||
|
} else {
|
||||||
|
targetExecPath = filepath.Join(installDir, "hostsync")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经在目标位置运行
|
||||||
|
if currentExecPath == targetExecPath {
|
||||||
|
utils.LogSuccess("程序已在目标位置运行")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
|
||||||
|
// 在强制模式下,确保服务完全停止和卸载后再进行文件操作
|
||||||
|
if initForce {
|
||||||
|
utils.LogInfo("强制模式:确保服务完全停止...")
|
||||||
|
// 检查服务状态
|
||||||
|
status, err := serviceManager.Status()
|
||||||
|
if err == nil && status != service.StatusNotInstalled {
|
||||||
|
// 停止服务
|
||||||
|
utils.LogInfo("停止现有服务...")
|
||||||
|
if err := serviceManager.Stop(); err != nil {
|
||||||
|
utils.LogWarning("停止服务时遇到问题: %v", err)
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("服务已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待服务完全停止
|
||||||
|
utils.LogInfo("等待服务完全停止...")
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// 卸载服务以释放文件句柄
|
||||||
|
utils.LogInfo("临时卸载服务以释放文件...")
|
||||||
|
if err := serviceManager.Uninstall(); err != nil {
|
||||||
|
utils.LogWarning("卸载服务时遇到问题: %v", err)
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("服务已临时卸载")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再次等待,确保系统完全释放文件句柄
|
||||||
|
utils.LogInfo("等待系统释放文件句柄...")
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 非强制模式下,只是尝试停止服务
|
||||||
|
utils.LogInfo("检查并停止现有服务...")
|
||||||
|
if err := serviceManager.Stop(); err == nil {
|
||||||
|
utils.LogSuccess("已停止现有服务")
|
||||||
|
// 给一点时间让服务完全停止
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 复制程序文件到目标位置
|
||||||
|
if err := copyFile(currentExecPath, targetExecPath); err != nil {
|
||||||
|
return fmt.Errorf("复制程序文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置执行权限
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := os.Chmod(targetExecPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("设置执行权限失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("程序已安装到: %s", targetExecPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupEnvironment 设置环境变量
|
||||||
|
func setupEnvironment(installDir string) error {
|
||||||
|
utils.LogInfo("检查并设置环境变量...")
|
||||||
|
|
||||||
|
// 首先检查当前环境是否已包含安装路径
|
||||||
|
if isPathInEnvironment(installDir) {
|
||||||
|
utils.LogSuccess("PATH环境变量已包含程序目录: %s", installDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("💡 需要将程序目录添加到 PATH 环境变量: %s", installDir)
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return setupWindowsEnvironment(installDir)
|
||||||
|
} else {
|
||||||
|
return setupUnixEnvironment(installDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPathInEnvironment 检查指定路径是否已在PATH环境变量中
|
||||||
|
func isPathInEnvironment(checkPath string) bool {
|
||||||
|
pathEnv := os.Getenv("PATH")
|
||||||
|
if pathEnv == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var separator string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
separator = ";"
|
||||||
|
} else {
|
||||||
|
separator = ":"
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := strings.Split(pathEnv, separator)
|
||||||
|
for _, path := range paths {
|
||||||
|
// 规范化路径进行比较
|
||||||
|
if normalizePath(path) == normalizePath(checkPath) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizePath 规范化路径用于比较
|
||||||
|
func normalizePath(path string) string {
|
||||||
|
absPath, err := filepath.Abs(strings.TrimSpace(path))
|
||||||
|
if err != nil {
|
||||||
|
return strings.TrimSpace(path)
|
||||||
|
}
|
||||||
|
return absPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile 复制文件
|
||||||
|
func copyFile(src, dst string) error {
|
||||||
|
srcFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer srcFile.Close()
|
||||||
|
|
||||||
|
dstFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstFile.Close()
|
||||||
|
|
||||||
|
_, err = dstFile.ReadFrom(srcFile)
|
||||||
|
return err
|
||||||
|
}
|
84
cmd/init_unix.go
Normal file
84
cmd/init_unix.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupWindowsEnvironment 在非 Windows 平台上不执行任何操作
|
||||||
|
func setupWindowsEnvironment(installDir string) error {
|
||||||
|
return fmt.Errorf("Windows 特定功能不支持当前平台 (%s)", runtime.GOOS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupUnixEnvironment 设置Unix/Linux环境
|
||||||
|
func setupUnixEnvironment(installDir string) error {
|
||||||
|
utils.LogInfo("配置Unix/Linux环境变量...")
|
||||||
|
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取当前用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查常见的shell配置文件
|
||||||
|
shellRcFiles := []string{
|
||||||
|
filepath.Join(currentUser.HomeDir, ".bashrc"),
|
||||||
|
filepath.Join(currentUser.HomeDir, ".zshrc"),
|
||||||
|
filepath.Join(currentUser.HomeDir, ".profile"),
|
||||||
|
}
|
||||||
|
|
||||||
|
exportLine := fmt.Sprintf("export PATH=\"%s:$PATH\"", installDir)
|
||||||
|
updated := false
|
||||||
|
|
||||||
|
for _, rcFile := range shellRcFiles {
|
||||||
|
if !utils.FileExists(rcFile) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 读取文件内容
|
||||||
|
content, err := os.ReadFile(rcFile)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogWarning("读取 %s 失败: %v", rcFile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已经添加过
|
||||||
|
if strings.Contains(string(content), installDir) {
|
||||||
|
utils.LogSuccess("%s 已包含程序路径", rcFile)
|
||||||
|
updated = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加PATH导出
|
||||||
|
f, err := os.OpenFile(rcFile, os.O_APPEND|os.O_WRONLY, 0644)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogWarning("打开 %s 失败: %v", rcFile, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
f.WriteString("\n# HostSync PATH\n")
|
||||||
|
f.WriteString(exportLine + "\n")
|
||||||
|
f.Close()
|
||||||
|
|
||||||
|
utils.LogSuccess("已添加PATH到 %s", rcFile)
|
||||||
|
updated = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !updated {
|
||||||
|
utils.LogWarning("未找到shell配置文件,请手动将以下路径添加到PATH:")
|
||||||
|
utils.LogInfo(" %s", installDir)
|
||||||
|
utils.LogInfo("💡 可以在 ~/.bashrc 或 ~/.zshrc 中添加:")
|
||||||
|
utils.LogInfo(" %s", exportLine)
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("💡 请重新加载shell配置或重启终端以使环境变量生效")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
131
cmd/init_windows.go
Normal file
131
cmd/init_windows.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setWindowsUserPathEnvironment 使用Windows Registry API设置用户PATH环境变量
|
||||||
|
func setWindowsUserPathEnvironment(installDir string) error {
|
||||||
|
// 打开用户环境变量注册表键
|
||||||
|
key, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE|registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开用户环境变量注册表失败: %v", err)
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
|
||||||
|
// 读取当前PATH值
|
||||||
|
currentPath, _, err := key.GetStringValue("PATH")
|
||||||
|
if err != nil && err != registry.ErrNotExist {
|
||||||
|
return fmt.Errorf("读取当前PATH环境变量失败: %v", err)
|
||||||
|
}
|
||||||
|
// 检查是否已经包含目标路径
|
||||||
|
if strings.Contains(strings.ToLower(currentPath), strings.ToLower(installDir)) {
|
||||||
|
utils.LogSuccess("PATH环境变量已包含目标路径: %s", installDir)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建新的PATH值
|
||||||
|
var newPath string
|
||||||
|
if currentPath == "" {
|
||||||
|
newPath = installDir
|
||||||
|
} else {
|
||||||
|
newPath = currentPath + ";" + installDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置新的PATH值
|
||||||
|
err = key.SetStringValue("PATH", newPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("设置PATH环境变量失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 广播环境变量变更消息
|
||||||
|
return broadcastEnvironmentChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcastEnvironmentChange 广播环境变量变更消息
|
||||||
|
func broadcastEnvironmentChange() error {
|
||||||
|
// 使用Windows API广播WM_SETTINGCHANGE消息
|
||||||
|
const (
|
||||||
|
HWND_BROADCAST = 0xFFFF
|
||||||
|
WM_SETTINGCHANGE = 0x001A
|
||||||
|
SMTO_NORMAL = 0x0000
|
||||||
|
)
|
||||||
|
|
||||||
|
envPtr, err := windows.UTF16PtrFromString("Environment")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("转换环境变量字符串失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user32 := windows.NewLazyDLL("user32.dll")
|
||||||
|
sendMessageTimeout := user32.NewProc("SendMessageTimeoutW")
|
||||||
|
|
||||||
|
ret, _, err := sendMessageTimeout.Call(
|
||||||
|
uintptr(HWND_BROADCAST),
|
||||||
|
uintptr(WM_SETTINGCHANGE),
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(envPtr)),
|
||||||
|
uintptr(SMTO_NORMAL),
|
||||||
|
uintptr(5000), // 5秒超时
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ret == 0 {
|
||||||
|
return fmt.Errorf("广播环境变量变更消息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupWindowsEnvironmentFallback PowerShell回退方法
|
||||||
|
func setupWindowsEnvironmentFallback(installDir string) error {
|
||||||
|
// 尝试使用PowerShell自动设置用户PATH环境变量
|
||||||
|
psCmd := fmt.Sprintf(`$userPath = [Environment]::GetEnvironmentVariable('PATH', 'User'); if ($userPath -eq $null) { $userPath = '' }; if ($userPath -notlike '*%s*') { if ($userPath -ne '') { $newPath = $userPath + ';%s' } else { $newPath = '%s' }; [Environment]::SetEnvironmentVariable('PATH', $newPath, 'User'); Write-Host '✅ PATH环境变量已更新' } else { Write-Host '✅ PATH环境变量已包含目标路径' }`, installDir, installDir, installDir)
|
||||||
|
|
||||||
|
// 尝试执行PowerShell命令
|
||||||
|
cmd := exec.Command("powershell.exe", "-Command", psCmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
utils.LogWarning("自动设置环境变量失败: %v", err)
|
||||||
|
utils.LogInfo("\n💡 请手动将以下路径添加到用户 PATH 环境变量:")
|
||||||
|
utils.LogInfo(" %s", installDir)
|
||||||
|
utils.LogInfo("💡 设置方法: 系统设置 → 高级系统设置 → 环境变量 → 用户变量中的PATH")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo(strings.TrimSpace(string(output)))
|
||||||
|
utils.LogWarning("💡 请重启命令行窗口以使环境变量生效")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupWindowsEnvironment 设置Windows环境
|
||||||
|
func setupWindowsEnvironment(installDir string) error {
|
||||||
|
utils.LogInfo("配置Windows用户环境变量...")
|
||||||
|
|
||||||
|
// 使用 Windows API 直接设置用户环境变量
|
||||||
|
err := setWindowsUserPathEnvironment(installDir)
|
||||||
|
if err != nil {
|
||||||
|
// 如果系统API调用失败,回退到PowerShell方法
|
||||||
|
utils.LogWarning("使用系统API设置环境变量失败: %v", err)
|
||||||
|
utils.LogInfo("尝试使用PowerShell方法...")
|
||||||
|
return setupWindowsEnvironmentFallback(installDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("PATH环境变量已成功更新")
|
||||||
|
utils.LogWarning("💡 请重启命令行窗口以使环境变量生效")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupUnixEnvironment 在 Windows 平台上不执行任何操作
|
||||||
|
func setupUnixEnvironment(installDir string) error {
|
||||||
|
return fmt.Errorf("Unix/Linux 特定功能不支持当前平台 (windows)")
|
||||||
|
}
|
269
cmd/list.go
Normal file
269
cmd/list.go
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
rawOutput bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// listCmd 列表命令
|
||||||
|
var listCmd = &cobra.Command{
|
||||||
|
Use: "list [blockName]",
|
||||||
|
Short: "查看 Hosts 配置",
|
||||||
|
Long: `查看 Hosts 配置,可以列出所有块或指定块的配置。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync list # 列出所有块配置
|
||||||
|
hostsync list github # 列出指定块配置
|
||||||
|
hostsync list --raw # 原始格式输出,便于导出`,
|
||||||
|
Run: runList,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
listCmd.Flags().BoolVar(&rawOutput, "raw", false, "原始格式输出")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runList(cmd *cobra.Command, args []string) {
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// 列出所有块
|
||||||
|
listAllBlocks(hm)
|
||||||
|
} else {
|
||||||
|
// 列出指定块
|
||||||
|
blockName := args[0]
|
||||||
|
listBlock(hm, blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func listAllBlocks(hm *core.HostsManager) {
|
||||||
|
if len(hm.Blocks) == 0 {
|
||||||
|
utils.LogInfo("没有找到任何配置块")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawOutput {
|
||||||
|
// 获取排序后的块名称列表用于原始输出
|
||||||
|
var blockNames []string
|
||||||
|
for name := range hm.Blocks {
|
||||||
|
blockNames = append(blockNames, name)
|
||||||
|
}
|
||||||
|
sort.Strings(blockNames)
|
||||||
|
|
||||||
|
// 原始格式输出
|
||||||
|
for _, name := range blockNames {
|
||||||
|
block := hm.Blocks[name]
|
||||||
|
utils.LogResult("# %s:\n", name)
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
prefix := ""
|
||||||
|
if !entry.Enabled {
|
||||||
|
prefix = "# "
|
||||||
|
}
|
||||||
|
if entry.Comment != "" {
|
||||||
|
utils.LogResult("%s%-16s %s # %s\n", prefix, entry.IP, entry.Domain, entry.Comment)
|
||||||
|
} else {
|
||||||
|
utils.LogResult("%s%-16s %s\n", prefix, entry.IP, entry.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if block.DNS != "" {
|
||||||
|
utils.LogResult("# useDns: %s\n", block.DNS)
|
||||||
|
}
|
||||||
|
if block.DoH != "" {
|
||||||
|
utils.LogResult("# useDoh: %s\n", block.DoH)
|
||||||
|
}
|
||||||
|
if block.Server != "" {
|
||||||
|
utils.LogResult("# useSrv: %s\n", block.Server)
|
||||||
|
}
|
||||||
|
if block.CronJob != "" {
|
||||||
|
utils.LogResult("# cronJob: %s\n", block.CronJob)
|
||||||
|
}
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
utils.LogResult("# updateAt: %s\n", block.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
utils.LogResult("# %s;\n\n", name)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格格式输出
|
||||||
|
headers := []string{"块名称", "状态", "条目数", "DNS配置", "最后更新"}
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
|
// 获取排序后的块名称列表
|
||||||
|
var blockNames []string
|
||||||
|
for name := range hm.Blocks {
|
||||||
|
blockNames = append(blockNames, name)
|
||||||
|
}
|
||||||
|
sort.Strings(blockNames)
|
||||||
|
|
||||||
|
for _, name := range blockNames {
|
||||||
|
block := hm.Blocks[name]
|
||||||
|
|
||||||
|
// 计算启用的条目数
|
||||||
|
enabledCount := 0
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
if entry.Enabled {
|
||||||
|
enabledCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据块状态和条目状态确定显示状态
|
||||||
|
var status string
|
||||||
|
if len(block.Entries) == 0 {
|
||||||
|
status = "空块"
|
||||||
|
} else if enabledCount == 0 {
|
||||||
|
status = "全部禁用"
|
||||||
|
} else if enabledCount == len(block.Entries) {
|
||||||
|
status = "全部启用"
|
||||||
|
} else {
|
||||||
|
status = "部分启用"
|
||||||
|
}
|
||||||
|
|
||||||
|
entryInfo := fmt.Sprintf("%d/%d", enabledCount, len(block.Entries))
|
||||||
|
|
||||||
|
dnsConfig := ""
|
||||||
|
if block.DNS != "" {
|
||||||
|
dnsConfig = fmt.Sprintf("DNS: %s", block.DNS)
|
||||||
|
} else if block.DoH != "" {
|
||||||
|
dnsConfig = fmt.Sprintf("DoH: %s", utils.TruncateWithWidth(block.DoH, 30))
|
||||||
|
} else if block.Server != "" {
|
||||||
|
dnsConfig = fmt.Sprintf("服务器: %s", block.Server)
|
||||||
|
} else {
|
||||||
|
dnsConfig = "系统默认"
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTime := "从未更新"
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
updateTime = block.UpdateAt.Format("2006-01-02 15:04")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{name, status, entryInfo, dnsConfig, updateTime})
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("Hosts 配置块列表 (%d 个块)\n", len(hm.Blocks))
|
||||||
|
|
||||||
|
// 设置合适的列宽
|
||||||
|
columnWidths := []int{12, 12, 8, 35, 17}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listBlock(hm *core.HostsManager, blockName string) {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
utils.LogError("块不存在: %s", blockName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rawOutput {
|
||||||
|
// 原始格式输出
|
||||||
|
fmt.Printf("# %s:\n", blockName)
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
prefix := ""
|
||||||
|
if !entry.Enabled {
|
||||||
|
prefix = "# "
|
||||||
|
}
|
||||||
|
if entry.Comment != "" {
|
||||||
|
fmt.Printf("%s%-16s %s # %s\n", prefix, entry.IP, entry.Domain, entry.Comment)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s%-16s %s\n", prefix, entry.IP, entry.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if block.DNS != "" {
|
||||||
|
fmt.Printf("# useDns: %s\n", block.DNS)
|
||||||
|
}
|
||||||
|
if block.DoH != "" {
|
||||||
|
fmt.Printf("# useDoh: %s\n", block.DoH)
|
||||||
|
}
|
||||||
|
if block.Server != "" {
|
||||||
|
fmt.Printf("# useSrv: %s\n", block.Server)
|
||||||
|
}
|
||||||
|
if block.CronJob != "" {
|
||||||
|
fmt.Printf("# cronJob: %s\n", block.CronJob)
|
||||||
|
}
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
fmt.Printf("# updateAt: %s\n", block.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
fmt.Printf("# %s;\n", blockName)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算启用的条目数
|
||||||
|
enabledCount := 0
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
if entry.Enabled {
|
||||||
|
enabledCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 详细信息显示
|
||||||
|
fmt.Printf("名称: %s\n", blockName)
|
||||||
|
|
||||||
|
// 根据块状态和条目状态确定显示状态
|
||||||
|
if !block.Enabled {
|
||||||
|
fmt.Printf("状态: 禁用\n")
|
||||||
|
} else if len(block.Entries) == 0 {
|
||||||
|
fmt.Printf("状态: 空块\n")
|
||||||
|
} else if enabledCount == 0 {
|
||||||
|
fmt.Printf("状态: 全部禁用 (0/%d)\n", len(block.Entries))
|
||||||
|
} else if enabledCount == len(block.Entries) {
|
||||||
|
fmt.Printf("状态: 全部启用 (%d/%d)\n", enabledCount, len(block.Entries))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("状态: 部分启用 (%d/%d)\n", enabledCount, len(block.Entries))
|
||||||
|
}
|
||||||
|
|
||||||
|
if block.DNS != "" {
|
||||||
|
fmt.Printf("DNS服务器: %s\n", block.DNS)
|
||||||
|
}
|
||||||
|
if block.DoH != "" {
|
||||||
|
fmt.Printf("DoH服务器: %s\n", block.DoH)
|
||||||
|
}
|
||||||
|
if block.Server != "" {
|
||||||
|
fmt.Printf("预设服务器: %s\n", block.Server)
|
||||||
|
}
|
||||||
|
if block.CronJob != "" {
|
||||||
|
fmt.Printf("定时任务: %s\n", block.CronJob)
|
||||||
|
}
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
fmt.Printf("最后更新: %s\n", block.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(block.Entries) == 0 {
|
||||||
|
utils.LogInfo("\n没有域名记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.LogInfo("\n域名记录 (%d 个条目):\n", len(block.Entries))
|
||||||
|
|
||||||
|
// 域名记录表格
|
||||||
|
headers := []string{"状态", "IP地址", "域名", "备注"}
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
status := "启用"
|
||||||
|
if !entry.Enabled {
|
||||||
|
status = "禁用"
|
||||||
|
}
|
||||||
|
|
||||||
|
comment := entry.Comment
|
||||||
|
if comment == "" {
|
||||||
|
comment = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{status, entry.IP, entry.Domain, comment})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置合适的列宽
|
||||||
|
columnWidths := []int{8, 16, 30, 20}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
346
cmd/log.go
Normal file
346
cmd/log.go
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logLines int
|
||||||
|
logFollow bool
|
||||||
|
logLevel string
|
||||||
|
)
|
||||||
|
|
||||||
|
// logCmd 日志命令
|
||||||
|
var logCmd = &cobra.Command{
|
||||||
|
Use: "log",
|
||||||
|
Short: "查看和管理日志",
|
||||||
|
Long: `查看和管理HostSync的日志文件。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync log # 查看最近50行日志
|
||||||
|
hostsync log -n 100 # 查看最近100行日志
|
||||||
|
hostsync log -f # 实时跟踪日志
|
||||||
|
hostsync log --level error # 只显示错误级别的日志
|
||||||
|
hostsync log list # 列出所有日志文件`,
|
||||||
|
Run: runLogShow,
|
||||||
|
}
|
||||||
|
|
||||||
|
// logListCmd 列出日志文件命令
|
||||||
|
var logListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "列出所有日志文件",
|
||||||
|
Long: `列出日志目录中的所有日志文件及其大小和修改时间。`,
|
||||||
|
Run: runLogList,
|
||||||
|
}
|
||||||
|
|
||||||
|
// logClearCmd 清理日志命令
|
||||||
|
var logClearCmd = &cobra.Command{
|
||||||
|
Use: "clear",
|
||||||
|
Short: "清理旧日志文件",
|
||||||
|
Long: `清理日志目录中的旧日志备份文件,保留当前日志和最近的备份。`,
|
||||||
|
Run: runLogClear,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logCmd.Flags().IntVarP(&logLines, "lines", "n", 50, "显示的行数")
|
||||||
|
logCmd.Flags().BoolVarP(&logFollow, "follow", "f", false, "实时跟踪日志输出")
|
||||||
|
logCmd.Flags().StringVar(&logLevel, "level", "", "过滤日志级别 (debug,info,warning,error)")
|
||||||
|
|
||||||
|
logCmd.AddCommand(logListCmd)
|
||||||
|
logCmd.AddCommand(logClearCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogShow(cmd *cobra.Command, args []string) {
|
||||||
|
// 初始化配置以获取日志路径
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
|
||||||
|
utils.LogError("未找到日志配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logFile := filepath.Join(config.AppConfig.LogPath, "hostsync.log")
|
||||||
|
// 检查日志文件是否存在
|
||||||
|
if _, err := os.Stat(logFile); os.IsNotExist(err) {
|
||||||
|
utils.LogInfo("日志文件尚未创建")
|
||||||
|
utils.LogInfo("日志文件路径: %s", logFile)
|
||||||
|
utils.LogInfo("执行一些命令后,日志文件将自动创建")
|
||||||
|
utils.LogInfo("例如: hostsync list, hostsync add test example.com 等")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if logFollow {
|
||||||
|
// 实时跟踪模式
|
||||||
|
followLog(logFile)
|
||||||
|
} else {
|
||||||
|
// 显示最近的日志
|
||||||
|
showRecentLog(logFile, logLines)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogList(cmd *cobra.Command, args []string) {
|
||||||
|
// 初始化配置
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
|
||||||
|
utils.LogError("未找到日志配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir := config.AppConfig.LogPath
|
||||||
|
|
||||||
|
// 读取日志目录
|
||||||
|
files, err := os.ReadDir(logDir)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("读取日志目录失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤和排序日志文件
|
||||||
|
var logFiles []os.FileInfo
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), "hostsync.log") {
|
||||||
|
info, err := file.Info()
|
||||||
|
if err == nil {
|
||||||
|
logFiles = append(logFiles, info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(logFiles) == 0 {
|
||||||
|
utils.LogInfo("未找到任何日志文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按修改时间排序
|
||||||
|
sort.Slice(logFiles, func(i, j int) bool {
|
||||||
|
return logFiles[i].ModTime().After(logFiles[j].ModTime())
|
||||||
|
})
|
||||||
|
// 显示日志文件列表
|
||||||
|
utils.LogInfo("日志目录: %s", logDir)
|
||||||
|
utils.LogInfo("")
|
||||||
|
|
||||||
|
// 准备表格数据
|
||||||
|
headers := []string{"文件名", "大小", "修改时间", "状态"}
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
|
for _, file := range logFiles {
|
||||||
|
size := formatFileSize(file.Size())
|
||||||
|
modTime := file.ModTime().Format("2006-01-02 15:04:05")
|
||||||
|
|
||||||
|
// 确定文件状态
|
||||||
|
var status string
|
||||||
|
if file.Name() == "hostsync.log" {
|
||||||
|
status = "当前"
|
||||||
|
} else {
|
||||||
|
status = "备份"
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{file.Name(), size, modTime, status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置列宽
|
||||||
|
columnWidths := []int{35, 12, 20, 10}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLogClear(cmd *cobra.Command, args []string) {
|
||||||
|
// 初始化配置
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
if config.AppConfig == nil || config.AppConfig.LogPath == "" {
|
||||||
|
utils.LogError("未找到日志配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logDir := config.AppConfig.LogPath
|
||||||
|
|
||||||
|
// 读取日志目录
|
||||||
|
files, err := os.ReadDir(logDir)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("读取日志目录失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找备份日志文件(不删除主日志文件)
|
||||||
|
var backupFiles []string
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasPrefix(file.Name(), "hostsync.log.") {
|
||||||
|
backupFiles = append(backupFiles, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(backupFiles) == 0 {
|
||||||
|
utils.LogInfo("没有需要清理的备份日志文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除备份文件
|
||||||
|
deletedCount := 0
|
||||||
|
for _, fileName := range backupFiles {
|
||||||
|
filePath := filepath.Join(logDir, fileName)
|
||||||
|
if err := os.Remove(filePath); err != nil {
|
||||||
|
utils.LogWarning("删除文件失败 %s: %v", fileName, err)
|
||||||
|
} else {
|
||||||
|
deletedCount++
|
||||||
|
utils.LogInfo("删除备份日志文件: %s", fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已清理 %d 个备份日志文件", deletedCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showRecentLog(logFile string, lines int) {
|
||||||
|
file, err := os.Open(logFile)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("打开日志文件失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 读取文件的最后N行
|
||||||
|
logLines := readLastLines(file, lines)
|
||||||
|
|
||||||
|
// 过滤日志级别
|
||||||
|
if logLevel != "" {
|
||||||
|
logLines = filterLogLevel(logLines, logLevel)
|
||||||
|
}
|
||||||
|
if len(logLines) == 0 {
|
||||||
|
utils.LogInfo("没有找到匹配的日志记录")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("最近 %d 行日志 (%s):", len(logLines), logFile)
|
||||||
|
utils.LogInfo("")
|
||||||
|
|
||||||
|
for _, line := range logLines {
|
||||||
|
utils.LogResult("%s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func followLog(logFile string) {
|
||||||
|
utils.LogInfo("实时跟踪日志文件: %s", logFile)
|
||||||
|
utils.LogInfo("按 Ctrl+C 停止跟踪")
|
||||||
|
utils.LogInfo("")
|
||||||
|
|
||||||
|
file, err := os.Open(logFile)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("打开日志文件失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 移动到文件末尾
|
||||||
|
file.Seek(0, io.SeekEnd)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for {
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if logLevel == "" || strings.Contains(strings.ToUpper(line), strings.ToUpper("["+logLevel+"]")) {
|
||||||
|
utils.LogResult("%s\n", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待新内容
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLastLines(file *os.File, lines int) []string {
|
||||||
|
// 获取文件大小
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSize := stat.Size()
|
||||||
|
if fileSize == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从文件末尾开始读取
|
||||||
|
var result []string
|
||||||
|
var buffer []byte
|
||||||
|
bufferSize := int64(4096)
|
||||||
|
position := fileSize
|
||||||
|
|
||||||
|
for position > 0 && len(result) < lines {
|
||||||
|
// 计算读取位置
|
||||||
|
readSize := bufferSize
|
||||||
|
if position < bufferSize {
|
||||||
|
readSize = position
|
||||||
|
}
|
||||||
|
position -= readSize
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
buffer = make([]byte, readSize)
|
||||||
|
_, err := file.ReadAt(buffer, position)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割行
|
||||||
|
text := string(buffer)
|
||||||
|
if position > 0 {
|
||||||
|
// 如果不是文件开头,去掉第一行(可能不完整)
|
||||||
|
if idx := strings.Index(text, "\n"); idx >= 0 {
|
||||||
|
text = text[idx+1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textLines := strings.Split(text, "\n")
|
||||||
|
|
||||||
|
// 反向添加行
|
||||||
|
for i := len(textLines) - 1; i >= 0; i-- {
|
||||||
|
if len(textLines[i]) > 0 {
|
||||||
|
result = append([]string{textLines[i]}, result...)
|
||||||
|
if len(result) >= lines {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回指定数量的行
|
||||||
|
if len(result) > lines {
|
||||||
|
result = result[len(result)-lines:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterLogLevel(lines []string, level string) []string {
|
||||||
|
var filtered []string
|
||||||
|
levelUpper := strings.ToUpper("[" + level + "]")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(strings.ToUpper(line), levelUpper) {
|
||||||
|
filtered = append(filtered, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatFileSize(bytes int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if bytes < unit {
|
||||||
|
return fmt.Sprintf("%d B", bytes)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := bytes / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
134
cmd/remove.go
Normal file
134
cmd/remove.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
skipConfirmation bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// removeCmd 删除命令
|
||||||
|
var removeCmd = &cobra.Command{
|
||||||
|
Use: "remove <blockName> [domain]",
|
||||||
|
Short: "删除域名记录或整个块",
|
||||||
|
Long: `从指定块中删除域名记录,或删除整个块。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync remove github github.com # 删除指定域名记录
|
||||||
|
hostsync remove github # 删除整个块
|
||||||
|
hostsync remove github --yes # 删除整个块,跳过确认`,
|
||||||
|
Args: cobra.RangeArgs(1, 2),
|
||||||
|
Run: runRemove,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
removeCmd.Flags().BoolVarP(&skipConfirmation, "yes", "y", false, "跳过确认提示")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runRemove(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
blockName := args[0]
|
||||||
|
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查块是否存在
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
utils.LogError("块不存在: %s", blockName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(args) == 2 {
|
||||||
|
// 删除单个域名记录
|
||||||
|
domain := args[1]
|
||||||
|
removeDomainEntry(hm, blockName, domain)
|
||||||
|
} else {
|
||||||
|
// 删除整个块
|
||||||
|
removeEntireBlock(hm, blockName, block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDomainEntry(hm *core.HostsManager, blockName, domain string) {
|
||||||
|
if err := hm.RemoveEntry(blockName, domain); err != nil {
|
||||||
|
utils.LogError("删除记录失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已删除记录: %s (块: %s)", domain, blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeEntireBlock(hm *core.HostsManager, blockName string, block *core.HostBlock) {
|
||||||
|
entryCount := len(block.Entries)
|
||||||
|
|
||||||
|
// 显示要删除的块信息
|
||||||
|
utils.LogInfo("准备删除块: %s", blockName)
|
||||||
|
utils.LogInfo(" 包含域名记录: %d 个", entryCount)
|
||||||
|
|
||||||
|
if block.DNS != "" {
|
||||||
|
utils.LogInfo(" DNS配置: %s", block.DNS)
|
||||||
|
}
|
||||||
|
if block.DoH != "" {
|
||||||
|
utils.LogInfo(" DoH配置: %s", block.DoH)
|
||||||
|
}
|
||||||
|
if block.Server != "" {
|
||||||
|
utils.LogInfo(" 预设服务器: %s", block.Server)
|
||||||
|
}
|
||||||
|
if block.CronJob != "" {
|
||||||
|
utils.LogInfo(" 定时任务: %s", block.CronJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("")
|
||||||
|
|
||||||
|
// 如果没有跳过确认,则要求用户确认
|
||||||
|
if !skipConfirmation {
|
||||||
|
if !confirmDeletion(blockName, entryCount) {
|
||||||
|
utils.LogInfo("操作已取消")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行删除操作
|
||||||
|
if err := hm.DeleteBlock(blockName); err != nil {
|
||||||
|
utils.LogError("删除块失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存文件
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
utils.LogError("保存失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("已删除块: %s (包含 %d 个域名记录)", blockName, entryCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmDeletion(blockName string, entryCount int) bool {
|
||||||
|
utils.LogWarning("警告: 此操作将永久删除块 '%s' 及其所有 %d 个域名记录!", blockName, entryCount)
|
||||||
|
utils.LogResult("请输入 'yes' 确认删除,或按 Enter 取消: ")
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
if scanner.Scan() {
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
return strings.ToLower(input) == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
132
cmd/root.go
Normal file
132
cmd/root.go
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
debugMode bool
|
||||||
|
version = "1.0.2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// rootCmd 根命令
|
||||||
|
var rootCmd = &cobra.Command{
|
||||||
|
Use: "hostsync",
|
||||||
|
Short: "一个强大的命令行 Hosts 文件管理工具",
|
||||||
|
Long: `HostSync 是一个强大的命令行 Hosts 文件管理工具,支持分块管理、智能 DNS 解析和定时自动更新。
|
||||||
|
|
||||||
|
功能特点:
|
||||||
|
- 🎯 按名称分块管理 Hosts 配置
|
||||||
|
- 🌐 支持多种 DNS 解析方式(DNS、DoH、预设服务器)
|
||||||
|
- 🔄 一键自动更新记录
|
||||||
|
- ⏰ 按任务定时自动更新块
|
||||||
|
- 📝 智能格式化与清理
|
||||||
|
- 🔧 后台服务模式运行`,
|
||||||
|
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
// 设置调试模式
|
||||||
|
utils.SetDebugMode(debugMode)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute 执行根命令
|
||||||
|
func Execute() {
|
||||||
|
if err := rootCmd.Execute(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 全局标志
|
||||||
|
rootCmd.PersistentFlags().BoolVar(&debugMode, "debug", false, "启用调试模式")
|
||||||
|
// 添加子命令
|
||||||
|
rootCmd.AddCommand(versionCmd)
|
||||||
|
rootCmd.AddCommand(listCmd)
|
||||||
|
rootCmd.AddCommand(enableCmd)
|
||||||
|
rootCmd.AddCommand(disableCmd)
|
||||||
|
rootCmd.AddCommand(addCmd)
|
||||||
|
rootCmd.AddCommand(removeCmd)
|
||||||
|
rootCmd.AddCommand(updateCmd)
|
||||||
|
rootCmd.AddCommand(formatCmd)
|
||||||
|
rootCmd.AddCommand(serverCmd)
|
||||||
|
rootCmd.AddCommand(cronCmd)
|
||||||
|
rootCmd.AddCommand(initCmd)
|
||||||
|
rootCmd.AddCommand(serviceCmd)
|
||||||
|
rootCmd.AddCommand(logCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// versionCmd 版本命令
|
||||||
|
var versionCmd = &cobra.Command{
|
||||||
|
Use: "version",
|
||||||
|
Short: "显示版本信息",
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
showVersionInfo()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// showVersionInfo 显示详细的版本和系统信息
|
||||||
|
func showVersionInfo() {
|
||||||
|
utils.LogResult("程序版本: v%s\n", version)
|
||||||
|
utils.LogResult("Go 版本: %s\n", runtime.Version())
|
||||||
|
utils.LogResult("系统架构: %s/%s\n", runtime.GOOS, runtime.GOARCH)
|
||||||
|
utils.LogResult("编译器: %s\n", runtime.Compiler)
|
||||||
|
|
||||||
|
// 显示配置信息
|
||||||
|
if config.AppConfig != nil {
|
||||||
|
utils.LogResult("\n配置信息:\n")
|
||||||
|
utils.LogResult(" Hosts 路径: %s\n", config.AppConfig.HostsPath)
|
||||||
|
utils.LogResult(" 日志级别: %s\n", config.AppConfig.LogLevel)
|
||||||
|
utils.LogResult(" 日志路径: %s\n", config.AppConfig.LogPath)
|
||||||
|
utils.LogResult(" 备份数量: %d 个\n", config.AppConfig.BackupCount)
|
||||||
|
utils.LogResult(" DNS 超时: %d ms\n", config.AppConfig.DNSTimeout)
|
||||||
|
utils.LogResult(" 最大并发: %d\n", config.AppConfig.MaxConcurrent)
|
||||||
|
// 显示DNS服务器数量
|
||||||
|
if len(config.DNSServers) > 0 {
|
||||||
|
utils.LogResult(" 可用DNS服务器: %d 个\n", len(config.DNSServers))
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogResult("\n文件路径:\n")
|
||||||
|
if config.ConfigPath != "" {
|
||||||
|
utils.LogResult(" 配置文件: %s\n", config.ConfigPath)
|
||||||
|
}
|
||||||
|
if config.ServersPath != "" {
|
||||||
|
utils.LogResult(" 服务器配置: %s\n", config.ServersPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.LogResult("\n配置信息: 未初始化\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示运行时信息
|
||||||
|
utils.LogResult("\n运行时信息:\n")
|
||||||
|
utils.LogResult(" CPU 核心数: %d\n", runtime.NumCPU())
|
||||||
|
utils.LogResult(" Goroutines: %d\n", runtime.NumGoroutine())
|
||||||
|
// 显示可执行文件信息
|
||||||
|
utils.LogResult("\n程序信息:\n")
|
||||||
|
if execPath, err := os.Executable(); err == nil {
|
||||||
|
if stat, err := os.Stat(execPath); err == nil {
|
||||||
|
utils.LogResult(" 可执行文件: %s\n", execPath)
|
||||||
|
utils.LogResult(" 文件大小: %s\n", formatFileSizeSimple(stat.Size()))
|
||||||
|
utils.LogResult(" 修改时间: %s\n", stat.ModTime().Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// formatFileSizeSimple 简单的文件大小格式化函数
|
||||||
|
func formatFileSizeSimple(size int64) string {
|
||||||
|
const unit = 1024
|
||||||
|
if size < unit {
|
||||||
|
return fmt.Sprintf("%d B", size)
|
||||||
|
}
|
||||||
|
div, exp := int64(unit), 0
|
||||||
|
for n := size / unit; n >= unit; n /= unit {
|
||||||
|
div *= unit
|
||||||
|
exp++
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.1f %cB", float64(size)/float64(div), "KMGTPE"[exp])
|
||||||
|
}
|
213
cmd/server.go
Normal file
213
cmd/server.go
Normal file
@ -0,0 +1,213 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serverCmd 服务器管理命令
|
||||||
|
var serverCmd = &cobra.Command{
|
||||||
|
Use: "server [command]",
|
||||||
|
Short: "管理预设 DNS 服务器",
|
||||||
|
Long: `管理预设DNS服务器配置,支持列出、测试服务器。
|
||||||
|
|
||||||
|
子命令:
|
||||||
|
list 列出所有预设服务器
|
||||||
|
test 测试所有服务器响应
|
||||||
|
test <name> 测试指定服务器
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync server # 列出所有预设服务器(默认)
|
||||||
|
hostsync server list # 列出所有预设服务器
|
||||||
|
hostsync server test # 测试所有服务器响应
|
||||||
|
hostsync server test Cloudflare # 测试指定服务器`,
|
||||||
|
Run: runServerDefault, // 添加默认运行函数
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverListCmd = &cobra.Command{
|
||||||
|
Use: "list",
|
||||||
|
Short: "列出所有预设服务器",
|
||||||
|
Run: runServerList,
|
||||||
|
}
|
||||||
|
|
||||||
|
var serverTestCmd = &cobra.Command{
|
||||||
|
Use: "test [serverName]",
|
||||||
|
Short: "测试服务器响应时间",
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runServerTest,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
serverCmd.AddCommand(serverListCmd)
|
||||||
|
serverCmd.AddCommand(serverTestCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
// runServerDefault 默认运行server命令时列出所有服务器
|
||||||
|
func runServerDefault(cmd *cobra.Command, args []string) {
|
||||||
|
runServerList(cmd, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServerList(cmd *cobra.Command, args []string) {
|
||||||
|
if len(config.DNSServers) == 0 {
|
||||||
|
utils.LogInfo("没有找到预设DNS服务器配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.LogResult("🌐 预设DNS服务器列表 (%d 个)\n", len(config.DNSServers))
|
||||||
|
|
||||||
|
headers := []string{"名称", "DNS地址", "DoH地址"}
|
||||||
|
var rows [][]string
|
||||||
|
|
||||||
|
for _, server := range config.DNSServers {
|
||||||
|
dnsAddr := server.DNS
|
||||||
|
if dnsAddr == "" {
|
||||||
|
dnsAddr = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
dohAddr := server.DoH
|
||||||
|
if dohAddr == "" {
|
||||||
|
dohAddr = "-"
|
||||||
|
} else {
|
||||||
|
dohAddr = utils.TruncateWithWidth(dohAddr, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = append(rows, []string{server.Name, dnsAddr, dohAddr})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置合适的列宽
|
||||||
|
columnWidths := []int{15, 20, 50}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServerTest(cmd *cobra.Command, args []string) {
|
||||||
|
resolver := core.NewDNSResolver()
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// 测试所有服务器
|
||||||
|
testAllServers(resolver)
|
||||||
|
} else {
|
||||||
|
// 测试指定服务器
|
||||||
|
serverName := args[0]
|
||||||
|
testServer(resolver, serverName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAllServers(resolver *core.DNSResolver) {
|
||||||
|
if len(config.DNSServers) == 0 {
|
||||||
|
utils.LogInfo("没有找到预设DNS服务器配置")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.LogInfo("测试所有DNS服务器 (%d 个)", len(config.DNSServers))
|
||||||
|
|
||||||
|
// 并发测试结果
|
||||||
|
type testResult struct {
|
||||||
|
index int
|
||||||
|
name string
|
||||||
|
dnsTime string
|
||||||
|
dohTime string
|
||||||
|
status string
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(chan testResult, len(config.DNSServers))
|
||||||
|
|
||||||
|
// 并发测试所有服务器
|
||||||
|
for i, server := range config.DNSServers {
|
||||||
|
go func(idx int, srv config.DNSServer) {
|
||||||
|
dnsTime := "-"
|
||||||
|
dohTime := "-"
|
||||||
|
status := "❌ 不可用"
|
||||||
|
|
||||||
|
// 测试DNS
|
||||||
|
if srv.DNS != "" {
|
||||||
|
if duration, err := resolver.TestDNSServer(srv.DNS); err == nil {
|
||||||
|
dnsTime = duration.String()
|
||||||
|
status = "✅ 可用"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试DoH
|
||||||
|
if srv.DoH != "" {
|
||||||
|
if duration, err := resolver.TestDoHServer(srv.DoH); err == nil {
|
||||||
|
dohTime = duration.String()
|
||||||
|
if status != "✅ 可用" {
|
||||||
|
status = "✅ 可用"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results <- testResult{
|
||||||
|
index: idx,
|
||||||
|
name: srv.Name,
|
||||||
|
dnsTime: dnsTime,
|
||||||
|
dohTime: dohTime,
|
||||||
|
status: status,
|
||||||
|
}
|
||||||
|
}(i, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集结果并显示进度
|
||||||
|
headers := []string{"服务器", "DNS响应时间", "DoH响应时间", "状态"}
|
||||||
|
resultMap := make(map[int]testResult)
|
||||||
|
completed := 0
|
||||||
|
|
||||||
|
for completed < len(config.DNSServers) {
|
||||||
|
result := <-results
|
||||||
|
resultMap[result.index] = result
|
||||||
|
completed++
|
||||||
|
utils.LogInfo("已完成测试: %d/%d", completed, len(config.DNSServers))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按原始顺序排序结果
|
||||||
|
var rows [][]string
|
||||||
|
for i := 0; i < len(config.DNSServers); i++ {
|
||||||
|
result := resultMap[i]
|
||||||
|
rows = append(rows, []string{result.name, result.dnsTime, result.dohTime, result.status})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置合适的列宽
|
||||||
|
columnWidths := []int{15, 15, 15, 12}
|
||||||
|
utils.FormatTable(headers, rows, columnWidths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testServer(resolver *core.DNSResolver, serverName string) {
|
||||||
|
server := config.GetDNSServer(serverName)
|
||||||
|
if server == nil {
|
||||||
|
utils.LogError("找不到服务器: %s", serverName)
|
||||||
|
utils.LogInfo("使用 'hostsync server list' 查看可用服务器")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("测试服务器: %s", serverName)
|
||||||
|
|
||||||
|
// 测试DNS
|
||||||
|
if server.DNS != "" {
|
||||||
|
utils.LogInfo("测试DNS (%s)...", server.DNS)
|
||||||
|
start := time.Now()
|
||||||
|
if duration, err := resolver.TestDNSServer(server.DNS); err != nil {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
utils.LogError("失败: %v (耗时: %v)", err, elapsed)
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("成功 (%v)", duration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("DNS: 未配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试DoH
|
||||||
|
if server.DoH != "" {
|
||||||
|
utils.LogInfo("测试DoH (%s)...", server.DoH)
|
||||||
|
start := time.Now()
|
||||||
|
if duration, err := resolver.TestDoHServer(server.DoH); err != nil {
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
utils.LogError("失败: %v (耗时: %v)", err, elapsed)
|
||||||
|
} else {
|
||||||
|
utils.LogSuccess("成功 (%v)", duration)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("DoH: 未配置")
|
||||||
|
}
|
||||||
|
}
|
464
cmd/service.go
Normal file
464
cmd/service.go
Normal file
@ -0,0 +1,464 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/service"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// serviceCmd 系统服务命令
|
||||||
|
var serviceCmd = &cobra.Command{
|
||||||
|
Use: "service",
|
||||||
|
Short: "系统服务管理",
|
||||||
|
Long: `管理HostSync系统服务,支持安装、卸载、启动、停止等操作。
|
||||||
|
|
||||||
|
支持的操作系统:
|
||||||
|
- Windows (Windows Service)
|
||||||
|
- Linux (systemd)
|
||||||
|
- macOS (LaunchD)
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync service install # 安装系统服务
|
||||||
|
hostsync service start # 启动服务
|
||||||
|
hostsync service stop # 停止服务
|
||||||
|
hostsync service status # 查看服务状态
|
||||||
|
hostsync service run # 以服务模式运行(通常由系统调用)`,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceInstallCmd 安装服务命令
|
||||||
|
var serviceInstallCmd = &cobra.Command{
|
||||||
|
Use: "install",
|
||||||
|
Short: "安装系统服务",
|
||||||
|
Long: `将HostSync安装为系统服务,支持开机自启动。`,
|
||||||
|
Run: runServiceInstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceUninstallCmd 卸载服务命令
|
||||||
|
var serviceUninstallCmd = &cobra.Command{
|
||||||
|
Use: "uninstall",
|
||||||
|
Short: "卸载系统服务",
|
||||||
|
Long: `从系统中卸载HostSync服务。`,
|
||||||
|
Run: runServiceUninstall,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceStartCmd 启动服务命令
|
||||||
|
var serviceStartCmd = &cobra.Command{
|
||||||
|
Use: "start",
|
||||||
|
Short: "启动系统服务",
|
||||||
|
Long: `启动HostSync系统服务。`,
|
||||||
|
Run: runServiceStart,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceStopCmd 停止服务命令
|
||||||
|
var serviceStopCmd = &cobra.Command{
|
||||||
|
Use: "stop",
|
||||||
|
Short: "停止系统服务",
|
||||||
|
Long: `停止HostSync系统服务。`,
|
||||||
|
Run: runServiceStop,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceRestartCmd 重启服务命令
|
||||||
|
var serviceRestartCmd = &cobra.Command{
|
||||||
|
Use: "restart",
|
||||||
|
Short: "重启系统服务",
|
||||||
|
Long: `重启HostSync系统服务。`,
|
||||||
|
Run: runServiceRestart,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceStatusCmd 查看服务状态命令
|
||||||
|
var serviceStatusCmd = &cobra.Command{
|
||||||
|
Use: "status",
|
||||||
|
Short: "查看服务状态",
|
||||||
|
Long: `查看HostSync系统服务当前状态。`,
|
||||||
|
Run: runServiceStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceRunCmd 运行服务命令
|
||||||
|
var serviceRunCmd = &cobra.Command{
|
||||||
|
Use: "run",
|
||||||
|
Short: "以服务模式运行",
|
||||||
|
Long: `以服务模式运行HostSync,通常由系统服务管理器调用。`,
|
||||||
|
Run: runServiceRun,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
serviceConfigDir string
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
serviceRunCmd.Flags().StringVarP(&serviceConfigDir, "config", "c", "", "指定配置文件目录路径")
|
||||||
|
serviceCmd.AddCommand(serviceInstallCmd)
|
||||||
|
serviceCmd.AddCommand(serviceUninstallCmd)
|
||||||
|
serviceCmd.AddCommand(serviceStartCmd)
|
||||||
|
serviceCmd.AddCommand(serviceStopCmd)
|
||||||
|
serviceCmd.AddCommand(serviceRestartCmd)
|
||||||
|
serviceCmd.AddCommand(serviceStatusCmd)
|
||||||
|
serviceCmd.AddCommand(serviceRunCmd)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceInstall(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("开始安装HostSync系统服务")
|
||||||
|
|
||||||
|
// 获取当前执行文件路径
|
||||||
|
execPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("获取执行文件路径失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("执行文件路径: %s", execPath)
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
if err := serviceManager.Install(execPath); err != nil {
|
||||||
|
utils.LogError("安装服务失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("服务安装成功")
|
||||||
|
utils.LogInfo("💡 可以使用以下命令管理服务:")
|
||||||
|
utils.LogInfo(" hostsync service start # 启动服务")
|
||||||
|
utils.LogInfo(" hostsync service stop # 停止服务")
|
||||||
|
utils.LogInfo(" hostsync service status # 查看状态")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceUninstall(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("开始卸载HostSync系统服务")
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
if err := serviceManager.Uninstall(); err != nil {
|
||||||
|
utils.LogError("卸载服务失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("服务卸载成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceStart(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("启动HostSync系统服务")
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
if err := serviceManager.Start(); err != nil {
|
||||||
|
utils.LogError("启动服务失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("服务启动成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceStop(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("停止HostSync系统服务")
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
if err := serviceManager.Stop(); err != nil {
|
||||||
|
utils.LogError("停止服务失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("服务停止成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceRestart(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
utils.LogInfo("重启HostSync系统服务")
|
||||||
|
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
if err := serviceManager.Restart(); err != nil {
|
||||||
|
utils.LogError("重启服务失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("服务重启成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceStatus(cmd *cobra.Command, args []string) {
|
||||||
|
serviceManager := service.NewServiceManager()
|
||||||
|
status, err := serviceManager.Status()
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("查询服务状态失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态描述
|
||||||
|
statusIcon := ""
|
||||||
|
statusText := ""
|
||||||
|
switch status {
|
||||||
|
case service.StatusRunning:
|
||||||
|
statusIcon = "🟢"
|
||||||
|
statusText = "运行中"
|
||||||
|
case service.StatusStopped:
|
||||||
|
statusIcon = "🔴"
|
||||||
|
statusText = "已停止"
|
||||||
|
case service.StatusNotInstalled:
|
||||||
|
statusIcon = "⚪"
|
||||||
|
statusText = "未安装"
|
||||||
|
default:
|
||||||
|
statusIcon = "❓"
|
||||||
|
statusText = "未知"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示基本信息
|
||||||
|
utils.LogResult("服务状态: %s %s\n", statusIcon, statusText)
|
||||||
|
|
||||||
|
// 如果服务正在运行,显示详细信息
|
||||||
|
if status == service.StatusRunning {
|
||||||
|
// 显示运行时信息
|
||||||
|
if execPath, err := os.Executable(); err == nil {
|
||||||
|
utils.LogResult("可执行文件: %s\n", execPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示进程信息
|
||||||
|
if pid := getServicePID(); pid > 0 {
|
||||||
|
utils.LogResult("进程ID: %d\n", pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示配置信息
|
||||||
|
if config.AppConfig != nil {
|
||||||
|
utils.LogResult("日志级别: %s\n", config.AppConfig.LogLevel)
|
||||||
|
if config.AppConfig.LogPath != "" {
|
||||||
|
utils.LogResult("日志路径: %s\n", config.AppConfig.LogPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示定时任务信息
|
||||||
|
showCronJobsStatusSimple()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果服务未运行,提示如何启动
|
||||||
|
if status != service.StatusRunning {
|
||||||
|
utils.LogResult("\n提示:\n")
|
||||||
|
if status == service.StatusNotInstalled {
|
||||||
|
utils.LogResult(" 1. 运行 'hostsync service install' 安装服务\n")
|
||||||
|
utils.LogResult(" 2. 运行 'hostsync service start' 启动服务\n")
|
||||||
|
} else {
|
||||||
|
utils.LogResult(" 运行 'hostsync service start' 启动服务\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServiceRun(cmd *cobra.Command, args []string) {
|
||||||
|
// 添加全局 panic 恢复
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
utils.LogError("服务出现严重错误: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// 如果指定了配置目录,则使用指定的目录初始化配置
|
||||||
|
if serviceConfigDir != "" {
|
||||||
|
utils.LogInfo("使用指定配置目录: %s", serviceConfigDir)
|
||||||
|
config.InitWithConfigDir(serviceConfigDir)
|
||||||
|
} else {
|
||||||
|
// 使用默认配置初始化
|
||||||
|
config.Init()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测运行环境
|
||||||
|
utils.LogDebug("检测运行环境...")
|
||||||
|
if isSystemdService := os.Getenv("INVOCATION_ID") != ""; isSystemdService {
|
||||||
|
utils.LogInfo("检测到 systemd 服务环境")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试作为系统服务运行
|
||||||
|
if handled, err := tryRunAsSystemService(); err != nil {
|
||||||
|
utils.LogError("系统服务运行失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else if handled {
|
||||||
|
// 已作为系统服务运行,直接返回
|
||||||
|
utils.LogInfo("已由系统服务管理器处理")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 作为普通进程运行(开发测试模式)
|
||||||
|
utils.LogInfo("启动HostSync服务模式...")
|
||||||
|
|
||||||
|
// 启动定时任务管理器
|
||||||
|
if err := startCronService(); err != nil {
|
||||||
|
utils.LogError("定时任务服务启动失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("HostSync服务已启动")
|
||||||
|
utils.LogInfo("定时任务管理器正在运行")
|
||||||
|
// 等待中断信号
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// 定期输出运行状态 (30秒)
|
||||||
|
statusTicker := time.NewTicker(30 * time.Second)
|
||||||
|
defer statusTicker.Stop()
|
||||||
|
|
||||||
|
// 定期重新同步定时任务配置 (5分钟)
|
||||||
|
syncTicker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer syncTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c:
|
||||||
|
utils.LogInfo("\n收到停止信号,正在关闭服务...")
|
||||||
|
stopCronService()
|
||||||
|
utils.LogSuccess("服务已安全关闭")
|
||||||
|
return
|
||||||
|
case <-statusTicker.C:
|
||||||
|
utils.LogInfo("%s - HostSync服务运行中", time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
case <-syncTicker.C:
|
||||||
|
// 定期重新同步定时任务配置
|
||||||
|
if globalCronManager != nil {
|
||||||
|
utils.LogDebug("开始定期同步定时任务配置...")
|
||||||
|
if err := globalCronManager.ReloadFromHosts(); err != nil {
|
||||||
|
utils.LogError("定时任务配置同步失败: %v", err)
|
||||||
|
} else {
|
||||||
|
utils.LogDebug("定时任务配置同步完成")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局定时任务管理器
|
||||||
|
var globalCronManager *core.CronManager
|
||||||
|
|
||||||
|
// startCronService 启动定时任务服务
|
||||||
|
func startCronService() error {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
utils.LogError("启动定时任务服务时发生错误: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
// 创建定时任务管理器
|
||||||
|
globalCronManager = core.NewCronManager()
|
||||||
|
globalCronManager.Start()
|
||||||
|
|
||||||
|
// 加载hosts文件中的定时任务
|
||||||
|
hostsManager := core.NewHostsManager()
|
||||||
|
if err := hostsManager.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
// 不立即返回错误,允许服务在没有hosts文件的情况下运行
|
||||||
|
utils.LogInfo("hosts文件加载失败,服务将在空任务状态下运行")
|
||||||
|
utils.LogSuccess("定时任务管理器已启动,当前无任务")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := globalCronManager.LoadFromHosts(hostsManager); err != nil {
|
||||||
|
utils.LogError("加载定时任务失败: %v", err)
|
||||||
|
utils.LogInfo("定时任务加载失败,服务将在空任务状态下运行")
|
||||||
|
utils.LogSuccess("定时任务管理器已启动,当前无任务")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("定时任务管理器已启动,共加载 %d 个任务", len(globalCronManager.ListJobs()))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopCronService 停止定时任务服务
|
||||||
|
func stopCronService() {
|
||||||
|
if globalCronManager != nil {
|
||||||
|
globalCronManager.Stop()
|
||||||
|
utils.LogSuccess("定时任务管理器已停止")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:截断字符串到指定长度
|
||||||
|
func truncateString(s string, maxLen int) string {
|
||||||
|
if len(s) <= maxLen {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if maxLen <= 3 {
|
||||||
|
return s[:maxLen]
|
||||||
|
}
|
||||||
|
return s[:maxLen-3] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取服务进程ID
|
||||||
|
func getServicePID() int {
|
||||||
|
// 这里可以通过系统调用获取服务的PID
|
||||||
|
// 简化实现,返回0表示无法获取
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示定时任务状态
|
||||||
|
func showCronJobsStatus() {
|
||||||
|
// 尝试连接到运行中的服务获取定时任务信息
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogResult("│ 定时任务: 无法加载配置%-36s│\n", "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计定时任务
|
||||||
|
cronCount := 0
|
||||||
|
var cronBlocks []string
|
||||||
|
for name, block := range hm.Blocks {
|
||||||
|
if block.CronJob != "" {
|
||||||
|
cronCount++
|
||||||
|
cronBlocks = append(cronBlocks, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogResult("│ 定时任务: %d 个活跃任务%-36s│\n", cronCount, "")
|
||||||
|
|
||||||
|
// 显示前3个任务
|
||||||
|
for i, blockName := range cronBlocks {
|
||||||
|
if i >= 3 {
|
||||||
|
utils.LogResult("│ ... 还有 %d 个任务%-38s│\n", cronCount-3, "")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block := hm.Blocks[blockName]
|
||||||
|
lastUpdate := "从未"
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
lastUpdate = block.UpdateAt.Format("01-02 15:04")
|
||||||
|
}
|
||||||
|
utils.LogResult("│ %s: %s (最后: %s)%*s│\n",
|
||||||
|
blockName, block.CronJob, lastUpdate,
|
||||||
|
47-len(blockName)-len(block.CronJob)-len(lastUpdate), "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示定时任务状态(简化版本)
|
||||||
|
func showCronJobsStatusSimple() {
|
||||||
|
// 尝试连接到运行中的服务获取定时任务信息
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogResult("定时任务: 无法加载配置\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计定时任务
|
||||||
|
cronCount := 0
|
||||||
|
var cronBlocks []string
|
||||||
|
for name, block := range hm.Blocks {
|
||||||
|
if block.CronJob != "" {
|
||||||
|
cronCount++
|
||||||
|
cronBlocks = append(cronBlocks, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogResult("定时任务: %d 个活跃任务\n", cronCount)
|
||||||
|
|
||||||
|
// 显示前3个任务
|
||||||
|
for i, blockName := range cronBlocks {
|
||||||
|
if i >= 3 {
|
||||||
|
utils.LogResult(" ... 还有 %d 个任务\n", cronCount-3)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
block := hm.Blocks[blockName]
|
||||||
|
lastUpdate := "从未"
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
lastUpdate = block.UpdateAt.Format("01-02 15:04")
|
||||||
|
}
|
||||||
|
utils.LogResult(" %s: %s (最后: %s)\n", blockName, block.CronJob, lastUpdate)
|
||||||
|
}
|
||||||
|
}
|
47
cmd/service_unix.go
Normal file
47
cmd/service_unix.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tryRunAsSystemService 尝试作为系统服务运行 (非 Windows)
|
||||||
|
func tryRunAsSystemService() (bool, error) {
|
||||||
|
// 检查是否由系统服务管理器启动
|
||||||
|
// 如果是,返回 false 让主服务逻辑继续运行
|
||||||
|
// 这里只是用来检测运行环境,不影响服务的实际启动
|
||||||
|
|
||||||
|
// 在 Linux 下,无论是否由 systemd 启动,都让主服务逻辑运行
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isRunningAsSystemService 检查是否作为系统服务运行
|
||||||
|
func isRunningAsSystemService() bool {
|
||||||
|
// 检查常见的系统服务环境变量
|
||||||
|
serviceIndicators := []string{
|
||||||
|
"INVOCATION_ID", // systemd
|
||||||
|
"JOURNAL_STREAM", // systemd journal
|
||||||
|
"NOTIFY_SOCKET", // systemd notify
|
||||||
|
"MANAGERPID", // systemd
|
||||||
|
"LISTEN_PID", // systemd socket activation
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, indicator := range serviceIndicators {
|
||||||
|
if os.Getenv(indicator) != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前用户和进程特征
|
||||||
|
if currentUser, err := user.Current(); err == nil {
|
||||||
|
// 如果运行用户是 root 且没有 TTY,可能是系统服务
|
||||||
|
if currentUser.Uid == "0" && os.Getenv("SSH_TTY") == "" && os.Getenv("TERM") == "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
30
cmd/service_windows.go
Normal file
30
cmd/service_windows.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/service"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// tryRunAsSystemService 尝试作为系统服务运行 (Windows)
|
||||||
|
func tryRunAsSystemService() (bool, error) {
|
||||||
|
isWindowsService, err := svc.IsWindowsService()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("检查服务环境失败: %v", err)
|
||||||
|
}
|
||||||
|
if isWindowsService {
|
||||||
|
// 作为 Windows 服务运行
|
||||||
|
utils.LogInfo("作为 Windows 服务启动...")
|
||||||
|
if err := service.RunAsWindowsService(); err != nil {
|
||||||
|
return false, fmt.Errorf("windows 服务运行失败: %v", err)
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
145
cmd/update.go
Normal file
145
cmd/update.go
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
forceDNS string
|
||||||
|
forceDoH string
|
||||||
|
forceServer string
|
||||||
|
saveConfig bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// updateCmd 更新命令
|
||||||
|
var updateCmd = &cobra.Command{
|
||||||
|
Use: "update [blockName]",
|
||||||
|
Short: "更新域名记录",
|
||||||
|
Long: `更新域名记录的IP地址。可以更新指定块或所有块。
|
||||||
|
支持强制使用指定的DNS服务器。
|
||||||
|
|
||||||
|
示例:
|
||||||
|
hostsync update # 更新所有块
|
||||||
|
hostsync update github # 更新指定块
|
||||||
|
hostsync update --dns 1.1.1.1 # 强制使用指定DNS更新
|
||||||
|
hostsync update --doh https://... # 强制使用DoH更新
|
||||||
|
hostsync update --srv Cloudflare # 强制使用预设服务器更新
|
||||||
|
hostsync update --save # 更新后保存DNS配置到块
|
||||||
|
hostsync update github --save # 更新指定块并保存DNS配置`,
|
||||||
|
Args: cobra.MaximumNArgs(1),
|
||||||
|
Run: runUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
updateCmd.Flags().StringVar(&forceDNS, "dns", "", "强制使用指定DNS服务器")
|
||||||
|
updateCmd.Flags().StringVar(&forceDoH, "doh", "", "强制使用DoH服务器")
|
||||||
|
updateCmd.Flags().StringVar(&forceServer, "srv", "", "强制使用预设服务器")
|
||||||
|
updateCmd.Flags().BoolVar(&saveConfig, "save", false, "保存DNS/DoH设置到块配置")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runUpdate(cmd *cobra.Command, args []string) {
|
||||||
|
utils.CheckAdmin()
|
||||||
|
|
||||||
|
hm := core.NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
utils.LogError("加载hosts文件失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := core.NewDNSResolver(debugMode)
|
||||||
|
|
||||||
|
if len(args) == 0 {
|
||||||
|
// 更新所有块
|
||||||
|
updateAllBlocks(hm, resolver)
|
||||||
|
} else {
|
||||||
|
// 更新指定块
|
||||||
|
blockName := args[0]
|
||||||
|
updateBlock(hm, resolver, blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAllBlocks(hm *core.HostsManager, resolver *core.DNSResolver) {
|
||||||
|
if len(hm.Blocks) == 0 {
|
||||||
|
utils.LogInfo("没有找到任何配置块")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.LogInfo("开始更新所有配置块 (%d 个)", len(hm.Blocks))
|
||||||
|
|
||||||
|
// 显示保存配置信息
|
||||||
|
if saveConfig && (forceDNS != "" || forceDoH != "" || forceServer != "") {
|
||||||
|
utils.LogInfo("将保存DNS配置到所有块设置中")
|
||||||
|
}
|
||||||
|
|
||||||
|
totalProcessed := 0
|
||||||
|
for name := range hm.Blocks {
|
||||||
|
utils.LogInfo("更新块: %s", name)
|
||||||
|
if err := resolver.UpdateBlock(hm, name, forceDNS, forceDoH, forceServer, saveConfig); err != nil {
|
||||||
|
utils.LogError("更新失败: %v", err)
|
||||||
|
} else {
|
||||||
|
totalProcessed++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if totalProcessed > 0 {
|
||||||
|
utils.LogSuccess("已处理 %d 个配置块", totalProcessed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示保存结果
|
||||||
|
if saveConfig && (forceDNS != "" || forceDoH != "" || forceServer != "") {
|
||||||
|
utils.LogSuccess("DNS配置已保存到所有块设置")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateBlock(hm *core.HostsManager, resolver *core.DNSResolver, blockName string) {
|
||||||
|
if !utils.ValidateBlockName(blockName) {
|
||||||
|
utils.LogError("无效的块名称: %s", blockName)
|
||||||
|
utils.LogInfo("块名称只能包含字母、数字、下划线和连字符")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
utils.LogError("块不存在: %s", blockName)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogInfo("开始更新块: %s", blockName)
|
||||||
|
|
||||||
|
// 显示DNS配置信息
|
||||||
|
if forceDNS != "" {
|
||||||
|
utils.LogInfo("强制使用DNS: %s", forceDNS)
|
||||||
|
} else if forceDoH != "" {
|
||||||
|
utils.LogInfo("强制使用DoH: %s", forceDoH)
|
||||||
|
} else if forceServer != "" {
|
||||||
|
utils.LogInfo("强制使用预设服务器: %s", forceServer)
|
||||||
|
} else {
|
||||||
|
// 显示块配置的DNS
|
||||||
|
if block.DNS != "" {
|
||||||
|
utils.LogInfo("使用DNS: %s", block.DNS)
|
||||||
|
} else if block.DoH != "" {
|
||||||
|
utils.LogInfo("使用DoH: %s", block.DoH)
|
||||||
|
} else if block.Server != "" {
|
||||||
|
utils.LogInfo("使用预设服务器: %s", block.Server)
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("使用系统默认DNS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示保存配置信息
|
||||||
|
if saveConfig && (forceDNS != "" || forceDoH != "" || forceServer != "") {
|
||||||
|
utils.LogInfo("将保存DNS配置到块设置中")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := resolver.UpdateBlock(hm, blockName, forceDNS, forceDoH, forceServer, saveConfig); err != nil {
|
||||||
|
utils.LogError("更新失败: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示保存结果
|
||||||
|
if saveConfig && (forceDNS != "" || forceDoH != "" || forceServer != "") {
|
||||||
|
utils.LogSuccess("DNS配置已保存到块设置")
|
||||||
|
}
|
||||||
|
}
|
7
config.json
Normal file
7
config.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"hostsPath": "C:\\Windows\\System32\\drivers\\etc\\hosts",
|
||||||
|
"backupCount": 5,
|
||||||
|
"dnsTimeout": 5000,
|
||||||
|
"maxConcurrent": 10,
|
||||||
|
"logLevel": "info"
|
||||||
|
}
|
215
config/config.go
Normal file
215
config/config.go
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config 程序配置结构
|
||||||
|
type Config struct {
|
||||||
|
HostsPath string `json:"hostsPath"`
|
||||||
|
BackupCount int `json:"backupCount"`
|
||||||
|
DNSTimeout int `json:"dnsTimeout"`
|
||||||
|
MaxConcurrent int `json:"maxConcurrent"`
|
||||||
|
LogLevel string `json:"logLevel"` // debug, info, warning, error, silent
|
||||||
|
LogPath string `json:"logPath"` // 空字符串表示只输出到控制台,有路径表示同时输出到文件和控制台
|
||||||
|
}
|
||||||
|
|
||||||
|
// DNSServer DNS服务器配置
|
||||||
|
type DNSServer struct {
|
||||||
|
Name string `json:"Name"`
|
||||||
|
DNS string `json:"Dns"`
|
||||||
|
DoH string `json:"Doh"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
AppConfig *Config
|
||||||
|
DNSServers []DNSServer
|
||||||
|
ConfigPath string
|
||||||
|
ServersPath string
|
||||||
|
)
|
||||||
|
|
||||||
|
// Init 初始化配置(使用默认用户目录)
|
||||||
|
func Init() {
|
||||||
|
userConfigDir, err := getUserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "获取用户配置目录失败: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
InitWithConfigDir(userConfigDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitWithConfigDir 使用指定配置目录初始化配置
|
||||||
|
func InitWithConfigDir(userConfigDir string) {
|
||||||
|
ConfigPath = filepath.Join(userConfigDir, "config", "config.json")
|
||||||
|
ServersPath = filepath.Join(userConfigDir, "config", "servers.json")
|
||||||
|
// 初始化默认配置
|
||||||
|
AppConfig = &Config{
|
||||||
|
HostsPath: getDefaultHostsPath(),
|
||||||
|
BackupCount: 5,
|
||||||
|
DNSTimeout: 5000,
|
||||||
|
MaxConcurrent: 10,
|
||||||
|
LogLevel: "info",
|
||||||
|
LogPath: filepath.Join(userConfigDir, "logs"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载配置文件
|
||||||
|
loadConfig()
|
||||||
|
loadServers()
|
||||||
|
// 创建日志目录
|
||||||
|
if err := os.MkdirAll(AppConfig.LogPath, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "创建日志目录失败: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserConfigDir 获取用户配置目录
|
||||||
|
func getUserConfigDir() (string, error) {
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取当前用户信息失败: %v", err)
|
||||||
|
}
|
||||||
|
return filepath.Join(currentUser.HomeDir, ".hostsync"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultHostsPath 获取默认hosts文件路径
|
||||||
|
func getDefaultHostsPath() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return `C:\Windows\System32\drivers\etc\hosts`
|
||||||
|
default:
|
||||||
|
return "/etc/hosts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfig 加载配置文件
|
||||||
|
func loadConfig() {
|
||||||
|
if _, err := os.Stat(ConfigPath); os.IsNotExist(err) {
|
||||||
|
// 配置文件不存在,创建默认配置
|
||||||
|
saveConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "读取配置文件失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时配置对象进行解析测试
|
||||||
|
tempConfig := &Config{}
|
||||||
|
if err := json.Unmarshal(data, tempConfig); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "解析配置文件失败: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "💡 将使用默认配置并重新创建配置文件")
|
||||||
|
saveConfig()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析成功,更新当前配置
|
||||||
|
AppConfig = tempConfig
|
||||||
|
|
||||||
|
// 检查并补充缺失的字段
|
||||||
|
needSave := false
|
||||||
|
if AppConfig.LogPath == "" {
|
||||||
|
userConfigDir, _ := getUserConfigDir()
|
||||||
|
AppConfig.LogPath = filepath.Join(userConfigDir, "logs")
|
||||||
|
needSave = true
|
||||||
|
}
|
||||||
|
if AppConfig.LogLevel == "" {
|
||||||
|
AppConfig.LogLevel = "info"
|
||||||
|
needSave = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有缺失字段,保存更新后的配置
|
||||||
|
if needSave {
|
||||||
|
saveConfig()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveConfig 保存配置文件
|
||||||
|
func saveConfig() {
|
||||||
|
data, err := json.MarshalIndent(AppConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "序列化配置失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保配置文件目录存在
|
||||||
|
configDir := filepath.Dir(ConfigPath)
|
||||||
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "创建配置目录失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(ConfigPath, data, 0644); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "保存配置文件失败: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadServers 加载DNS服务器配置
|
||||||
|
func loadServers() {
|
||||||
|
if _, err := os.Stat(ServersPath); os.IsNotExist(err) {
|
||||||
|
// DNS服务器配置文件不存在时,不输出错误信息
|
||||||
|
// init 命令会创建这个文件
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(ServersPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "读取DNS服务器配置失败: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建临时变量进行解析测试
|
||||||
|
var tempServers []DNSServer
|
||||||
|
if err := json.Unmarshal(data, &tempServers); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "解析DNS服务器配置失败: %v\n", err)
|
||||||
|
fmt.Fprintln(os.Stderr, "💡 请运行 'hostsync init --force' 重新创建配置文件")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析成功,更新服务器列表
|
||||||
|
DNSServers = tempServers
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDNSServer 根据名称获取DNS服务器
|
||||||
|
func GetDNSServer(name string) *DNSServer {
|
||||||
|
for _, server := range DNSServers {
|
||||||
|
if server.Name == name {
|
||||||
|
return &server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupViper 设置viper配置
|
||||||
|
func SetupViper() {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
viper.SetConfigType("json")
|
||||||
|
|
||||||
|
// 添加用户配置目录到搜索路径
|
||||||
|
if userConfigDir, err := getUserConfigDir(); err == nil {
|
||||||
|
viper.AddConfigPath(filepath.Join(userConfigDir, "config"))
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
// 设置默认值
|
||||||
|
viper.SetDefault("hostsPath", getDefaultHostsPath())
|
||||||
|
viper.SetDefault("backupCount", 5)
|
||||||
|
viper.SetDefault("dnsTimeout", 5000)
|
||||||
|
viper.SetDefault("maxConcurrent", 10)
|
||||||
|
viper.SetDefault("logLevel", "info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBackupDir 获取备份目录
|
||||||
|
func GetBackupDir() string {
|
||||||
|
if userConfigDir, err := getUserConfigDir(); err == nil {
|
||||||
|
return filepath.Join(userConfigDir, "backup")
|
||||||
|
}
|
||||||
|
return "backup" // 回退到相对路径
|
||||||
|
}
|
8
config/config.json
Normal file
8
config/config.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"hostsPath": "C:\\Windows\\System32\\drivers\\etc\\hosts",
|
||||||
|
"backupCount": 5,
|
||||||
|
"dnsTimeout": 5000,
|
||||||
|
"maxConcurrent": 10,
|
||||||
|
"logLevel": "info",
|
||||||
|
"logPath": "logs"
|
||||||
|
}
|
306
core/cron.go
Normal file
306
core/cron.go
Normal file
@ -0,0 +1,306 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全局文件保存互斥锁,防止多个定时任务同时保存文件
|
||||||
|
var hostsSaveMutex sync.Mutex
|
||||||
|
|
||||||
|
// CronManager 定时任务管理器
|
||||||
|
type CronManager struct {
|
||||||
|
cron *cron.Cron
|
||||||
|
jobs map[string]cron.EntryID
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCronManager 创建定时任务管理器
|
||||||
|
func NewCronManager() *CronManager {
|
||||||
|
// 创建支持秒级的cron实例,支持6位表达式
|
||||||
|
c := cron.New(cron.WithSeconds(), cron.WithLocation(time.Local))
|
||||||
|
|
||||||
|
return &CronManager{
|
||||||
|
cron: c,
|
||||||
|
jobs: make(map[string]cron.EntryID),
|
||||||
|
mutex: sync.RWMutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动调度器
|
||||||
|
func (cm *CronManager) Start() {
|
||||||
|
cm.cron.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止调度器
|
||||||
|
func (cm *CronManager) Stop() {
|
||||||
|
cm.cron.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddJob 添加定时任务
|
||||||
|
func (cm *CronManager) AddJob(blockName, cronExpr string, task func()) error {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
// 如果已存在,先删除
|
||||||
|
if existingID, exists := cm.jobs[blockName]; exists {
|
||||||
|
cm.cron.Remove(existingID)
|
||||||
|
delete(cm.jobs, blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新任务
|
||||||
|
entryID, err := cm.cron.AddFunc(cronExpr, func() {
|
||||||
|
utils.LogDebug("执行定时任务: %s", blockName)
|
||||||
|
task()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("添加定时任务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.jobs[blockName] = entryID
|
||||||
|
utils.LogDebug("已添加定时任务: %s (%s)", blockName, cronExpr)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveJob 删除定时任务
|
||||||
|
func (cm *CronManager) RemoveJob(blockName string) error {
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
entryID, exists := cm.jobs[blockName]
|
||||||
|
if !exists {
|
||||||
|
return fmt.Errorf("定时任务不存在: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.cron.Remove(entryID)
|
||||||
|
delete(cm.jobs, blockName)
|
||||||
|
utils.LogDebug("已删除定时任务: %s", blockName)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListJobs 列出所有定时任务
|
||||||
|
func (cm *CronManager) ListJobs() map[string]cron.EntryID {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
|
||||||
|
result := make(map[string]cron.EntryID)
|
||||||
|
for name, entryID := range cm.jobs {
|
||||||
|
result[name] = entryID
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetJob 获取指定的定时任务
|
||||||
|
func (cm *CronManager) GetJob(blockName string) (cron.EntryID, bool) {
|
||||||
|
cm.mutex.RLock()
|
||||||
|
defer cm.mutex.RUnlock()
|
||||||
|
|
||||||
|
entryID, exists := cm.jobs[blockName]
|
||||||
|
return entryID, exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromHosts 从hosts文件加载定时任务
|
||||||
|
func (cm *CronManager) LoadFromHosts(hm *HostsManager) error {
|
||||||
|
resolver := NewDNSResolver()
|
||||||
|
|
||||||
|
for blockName, block := range hm.Blocks {
|
||||||
|
if block.CronJob != "" {
|
||||||
|
task := func(bn string) func() {
|
||||||
|
return func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
utils.LogError("定时任务 %s 发生错误: %v", bn, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
utils.LogInfo("开始执行定时更新: %s", bn)
|
||||||
|
|
||||||
|
// 使用全局锁确保hosts文件保存的原子性
|
||||||
|
hostsSaveMutex.Lock()
|
||||||
|
defer hostsSaveMutex.Unlock()
|
||||||
|
|
||||||
|
// 重新加载hosts文件以获取最新配置
|
||||||
|
freshHM := NewHostsManager()
|
||||||
|
if err := freshHM.Load(); err != nil {
|
||||||
|
utils.LogError("定时任务 %s 重新加载hosts文件失败: %v", bn, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查块是否仍然存在
|
||||||
|
freshBlock := freshHM.GetBlock(bn)
|
||||||
|
if freshBlock == nil {
|
||||||
|
utils.LogError("定时任务 %s 对应的块已不存在,跳过执行", bn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查定时任务是否仍然启用
|
||||||
|
if freshBlock.CronJob == "" {
|
||||||
|
utils.LogInfo("定时任务 %s 已被禁用,跳过执行", bn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用最新的hosts数据执行更新
|
||||||
|
utils.LogInfo("开始DNS解析更新: %s", bn)
|
||||||
|
|
||||||
|
err := resolver.UpdateBlock(freshHM, bn, "", "", "", false)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("定时更新失败 %s: %v", bn, err)
|
||||||
|
} else {
|
||||||
|
// 获取更新后的块信息,记录更新时间
|
||||||
|
updatedBlock := freshHM.GetBlock(bn)
|
||||||
|
if updatedBlock != nil && !updatedBlock.UpdateAt.IsZero() {
|
||||||
|
utils.LogInfo("定时更新完成: %s,更新时间: %s", bn, updatedBlock.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("定时更新完成: %s", bn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(blockName)
|
||||||
|
|
||||||
|
if err := cm.AddJob(blockName, block.CronJob, task); err != nil {
|
||||||
|
utils.LogError("加载定时任务失败 %s: %v", blockName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateCronExpression 验证cron表达式
|
||||||
|
func (cm *CronManager) ValidateCronExpression(expr string) error {
|
||||||
|
// 使用robfig/cron/v3的解析器验证表达式
|
||||||
|
parser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
|
||||||
|
_, err := parser.Parse(expr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReloadFromHosts 重新从hosts文件同步定时任务配置
|
||||||
|
func (cm *CronManager) ReloadFromHosts() error {
|
||||||
|
utils.LogDebug("正在重新同步定时任务配置...")
|
||||||
|
|
||||||
|
// 加载最新的hosts文件
|
||||||
|
hm := NewHostsManager()
|
||||||
|
if err := hm.Load(); err != nil {
|
||||||
|
return fmt.Errorf("重新加载hosts文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cm.mutex.Lock()
|
||||||
|
defer cm.mutex.Unlock()
|
||||||
|
|
||||||
|
// 收集当前活跃的任务
|
||||||
|
currentJobs := make(map[string]string) // blockName -> cronExpr
|
||||||
|
for blockName := range cm.jobs {
|
||||||
|
currentJobs[blockName] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集hosts文件中的任务配置
|
||||||
|
hostsJobs := make(map[string]string)
|
||||||
|
for blockName, block := range hm.Blocks {
|
||||||
|
if block.CronJob != "" {
|
||||||
|
hostsJobs[blockName] = block.CronJob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除不再存在的任务
|
||||||
|
for blockName := range currentJobs {
|
||||||
|
if _, exists := hostsJobs[blockName]; !exists {
|
||||||
|
if entryID, exists := cm.jobs[blockName]; exists {
|
||||||
|
cm.cron.Remove(entryID)
|
||||||
|
delete(cm.jobs, blockName)
|
||||||
|
utils.LogInfo("已删除不存在的定时任务: %s", blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加或更新任务
|
||||||
|
resolver := NewDNSResolver()
|
||||||
|
for blockName, cronExpr := range hostsJobs {
|
||||||
|
// 检查是否需要更新任务
|
||||||
|
needsUpdate := false
|
||||||
|
if _, exists := cm.jobs[blockName]; !exists {
|
||||||
|
needsUpdate = true
|
||||||
|
} else {
|
||||||
|
// 这里可以进一步检查cron表达式是否有变化
|
||||||
|
// 目前简化处理,每次都重新创建以确保同步
|
||||||
|
if existingEntryID, exists := cm.jobs[blockName]; exists {
|
||||||
|
cm.cron.Remove(existingEntryID)
|
||||||
|
delete(cm.jobs, blockName)
|
||||||
|
}
|
||||||
|
needsUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsUpdate {
|
||||||
|
task := func(bn string) func() {
|
||||||
|
return func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
utils.LogError("定时任务 %s 发生错误: %v", bn, r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
utils.LogInfo("开始执行定时更新: %s", bn)
|
||||||
|
|
||||||
|
// 使用全局锁确保hosts文件保存的原子性
|
||||||
|
hostsSaveMutex.Lock()
|
||||||
|
defer hostsSaveMutex.Unlock()
|
||||||
|
|
||||||
|
// 重新加载hosts文件以获取最新配置
|
||||||
|
freshHM := NewHostsManager()
|
||||||
|
if err := freshHM.Load(); err != nil {
|
||||||
|
utils.LogError("定时任务 %s 重新加载hosts文件失败: %v", bn, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查块是否仍然存在
|
||||||
|
freshBlock := freshHM.GetBlock(bn)
|
||||||
|
if freshBlock == nil {
|
||||||
|
utils.LogError("定时任务 %s 对应的块已不存在,跳过执行", bn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查定时任务是否仍然启用
|
||||||
|
if freshBlock.CronJob == "" {
|
||||||
|
utils.LogInfo("定时任务 %s 已被禁用,跳过执行", bn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用最新的hosts数据执行更新
|
||||||
|
utils.LogInfo("开始DNS解析更新: %s", bn)
|
||||||
|
|
||||||
|
err := resolver.UpdateBlock(freshHM, bn, "", "", "", false)
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("定时更新失败 %s: %v", bn, err)
|
||||||
|
} else {
|
||||||
|
// 获取更新后的块信息,记录更新时间
|
||||||
|
updatedBlock := freshHM.GetBlock(bn)
|
||||||
|
if updatedBlock != nil && !updatedBlock.UpdateAt.IsZero() {
|
||||||
|
utils.LogInfo("定时更新完成: %s,更新时间: %s", bn, updatedBlock.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
} else {
|
||||||
|
utils.LogInfo("定时更新完成: %s", bn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(blockName)
|
||||||
|
|
||||||
|
entryID, err := cm.cron.AddFunc(cronExpr, func() {
|
||||||
|
utils.LogDebug("执行定时任务: %s", blockName)
|
||||||
|
task()
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.LogError("重新加载定时任务失败 %s: %v", blockName, err)
|
||||||
|
} else {
|
||||||
|
cm.jobs[blockName] = entryID
|
||||||
|
utils.LogDebug("已重新加载定时任务: %s (%s)", blockName, cronExpr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogDebug("定时任务配置重新同步完成,当前活跃任务: %d 个", len(cm.jobs))
|
||||||
|
return nil
|
||||||
|
}
|
350
core/dns.go
Normal file
350
core/dns.go
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNSResolver DNS解析器
|
||||||
|
type DNSResolver struct {
|
||||||
|
timeout time.Duration
|
||||||
|
debugMode bool
|
||||||
|
dohClient *DoHClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDNSResolver 创建DNS解析器
|
||||||
|
func NewDNSResolver(debugMode ...bool) *DNSResolver {
|
||||||
|
timeout := time.Duration(config.AppConfig.DNSTimeout) * time.Millisecond
|
||||||
|
|
||||||
|
debug := false
|
||||||
|
if len(debugMode) > 0 {
|
||||||
|
debug = debugMode[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DNSResolver{
|
||||||
|
timeout: timeout,
|
||||||
|
debugMode: debug,
|
||||||
|
dohClient: NewDoHClient(timeout, debug),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveDomain 解析域名
|
||||||
|
func (r *DNSResolver) ResolveDomain(domain string, dnsServer, dohServer string) (string, error) {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("开始解析域名: %s", domain)
|
||||||
|
if dohServer != "" {
|
||||||
|
utils.LogDebug("使用DoH服务器: %s", dohServer)
|
||||||
|
} else if dnsServer != "" {
|
||||||
|
utils.LogDebug("使用DNS服务器: %s", dnsServer)
|
||||||
|
} else {
|
||||||
|
utils.LogDebug("使用系统默认DNS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用DoH
|
||||||
|
if dohServer != "" {
|
||||||
|
if ip, err := r.dohClient.Resolve(domain, dohServer); err == nil {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("DoH解析成功: %s -> %s", domain, ip)
|
||||||
|
}
|
||||||
|
return ip, nil
|
||||||
|
} else if r.debugMode {
|
||||||
|
utils.LogDebug("DoH解析失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用传统DNS
|
||||||
|
if dnsServer != "" {
|
||||||
|
if ip, err := r.resolveWithDNS(domain, dnsServer); err == nil {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("DNS解析成功: %s -> %s\n", domain, ip)
|
||||||
|
}
|
||||||
|
return ip, nil
|
||||||
|
} else if r.debugMode {
|
||||||
|
utils.LogDebug("DNS解析失败: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用系统默认DNS
|
||||||
|
if ip, err := r.resolveWithSystem(domain); err == nil {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("系统DNS解析成功: %s -> %s", domain, ip)
|
||||||
|
}
|
||||||
|
return ip, nil
|
||||||
|
} else {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("系统DNS解析失败: %v", err)
|
||||||
|
}
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveWithDNS 使用DNS服务器解析
|
||||||
|
func (r *DNSResolver) resolveWithDNS(domain, server string) (string, error) {
|
||||||
|
// 确保服务器地址包含端口
|
||||||
|
if !strings.Contains(server, ":") {
|
||||||
|
server += ":53"
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(dns.Client)
|
||||||
|
c.Timeout = r.timeout
|
||||||
|
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(domain), dns.TypeA)
|
||||||
|
|
||||||
|
resp, _, err := c.Exchange(m, server)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("DNS查询失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(resp.Answer) == 0 {
|
||||||
|
return "", fmt.Errorf("没有找到A记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
if a, ok := ans.(*dns.A); ok {
|
||||||
|
return a.A.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("没有有效的A记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveWithSystem 使用系统默认DNS解析
|
||||||
|
func (r *DNSResolver) resolveWithSystem(domain string) (string, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), r.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ips, err := net.DefaultResolver.LookupIPAddr(ctx, domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("系统DNS解析失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ip := range ips {
|
||||||
|
if ip.IP.To4() != nil { // IPv4
|
||||||
|
return ip.IP.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("没有找到IPv4地址")
|
||||||
|
}
|
||||||
|
|
||||||
|
// BatchResolve 批量解析域名
|
||||||
|
func (r *DNSResolver) BatchResolve(domains []string, dnsServer, dohServer string) map[string]string {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("开始批量解析 %d 个域名", len(domains))
|
||||||
|
}
|
||||||
|
|
||||||
|
results := make(map[string]string)
|
||||||
|
var mu sync.Mutex
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
// 控制并发数
|
||||||
|
semaphore := make(chan struct{}, config.AppConfig.MaxConcurrent)
|
||||||
|
|
||||||
|
for _, domain := range domains {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(d string) {
|
||||||
|
defer wg.Done()
|
||||||
|
semaphore <- struct{}{} // 获取信号量
|
||||||
|
defer func() { <-semaphore }() // 释放信号量
|
||||||
|
|
||||||
|
if ip, err := r.ResolveDomain(d, dnsServer, dohServer); err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
results[d] = ip
|
||||||
|
mu.Unlock()
|
||||||
|
} else if r.debugMode {
|
||||||
|
utils.LogDebug("解析失败 %s: %v", d, err)
|
||||||
|
}
|
||||||
|
}(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("批量解析完成,成功解析 %d/%d 个域名", len(results), len(domains))
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDNSServer 测试DNS服务器
|
||||||
|
func (r *DNSResolver) TestDNSServer(server string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
|
_, err := r.resolveWithDNS("github.com", server)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDoHServer 测试DoH服务器
|
||||||
|
func (r *DNSResolver) TestDoHServer(server string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
|
_, err := r.dohClient.Resolve("github.com", server)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBlock 更新块中的域名解析
|
||||||
|
func (r *DNSResolver) UpdateBlock(hm *HostsManager, blockName string, forceDNS, forceDoH, forceServer string, saveConfig bool) error {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("块不存在: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("开始更新块: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确定使用的DNS服务器
|
||||||
|
dnsServer := forceDNS
|
||||||
|
dohServer := forceDoH
|
||||||
|
|
||||||
|
if dnsServer == "" && dohServer == "" && forceServer == "" {
|
||||||
|
// 使用块配置的DNS设置
|
||||||
|
if block.DNS != "" {
|
||||||
|
dnsServer = block.DNS
|
||||||
|
} else if block.DoH != "" {
|
||||||
|
dohServer = block.DoH
|
||||||
|
} else if block.Server != "" {
|
||||||
|
// 使用预设服务器
|
||||||
|
if srv := config.GetDNSServer(block.Server); srv != nil {
|
||||||
|
if srv.DNS != "" {
|
||||||
|
dnsServer = srv.DNS
|
||||||
|
}
|
||||||
|
if srv.DoH != "" {
|
||||||
|
dohServer = srv.DoH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if forceServer != "" {
|
||||||
|
// 强制使用预设服务器
|
||||||
|
if srv := config.GetDNSServer(forceServer); srv != nil {
|
||||||
|
if srv.DNS != "" {
|
||||||
|
dnsServer = srv.DNS
|
||||||
|
}
|
||||||
|
if srv.DoH != "" {
|
||||||
|
dohServer = srv.DoH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("DNS配置 - DNS: %s, DoH: %s", dnsServer, dohServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果开启saveConfig,保存DNS配置到块中
|
||||||
|
if saveConfig {
|
||||||
|
configUpdated := false
|
||||||
|
if forceDNS != "" && forceDNS != block.DNS {
|
||||||
|
block.DNS = forceDNS
|
||||||
|
block.DoH = "" // 清除DoH设置
|
||||||
|
block.Server = "" // 清除预设服务器
|
||||||
|
configUpdated = true
|
||||||
|
utils.LogInfo("已将DNS服务器 '%s' 保存到块 '%s'", forceDNS, blockName)
|
||||||
|
}
|
||||||
|
if forceDoH != "" && forceDoH != block.DoH {
|
||||||
|
block.DoH = forceDoH
|
||||||
|
block.DNS = "" // 清除DNS设置
|
||||||
|
block.Server = "" // 清除预设服务器
|
||||||
|
configUpdated = true
|
||||||
|
utils.LogInfo("已将DoH服务器 '%s' 保存到块 '%s'", forceDoH, blockName)
|
||||||
|
}
|
||||||
|
if forceServer != "" && forceServer != block.Server {
|
||||||
|
block.Server = forceServer
|
||||||
|
block.DNS = "" // 清除DNS设置
|
||||||
|
block.DoH = "" // 清除DoH设置
|
||||||
|
configUpdated = true
|
||||||
|
utils.LogInfo("已将预设服务器 '%s' 保存到块 '%s'", forceServer, blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if configUpdated {
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
return fmt.Errorf("保存配置失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 收集需要解析的域名
|
||||||
|
domains := make([]string, 0, len(block.Entries))
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
if entry.Enabled {
|
||||||
|
domains = append(domains, entry.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return fmt.Errorf("没有需要更新的域名")
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("需要解析 %d 个域名: %v", len(domains), domains)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量解析
|
||||||
|
results := r.BatchResolve(domains, dnsServer, dohServer)
|
||||||
|
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("解析结果: %d 个成功", len(results))
|
||||||
|
for domain, ip := range results {
|
||||||
|
utils.LogDebug(" %s -> %s", domain, ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新IP地址
|
||||||
|
updated := 0
|
||||||
|
for i, entry := range block.Entries {
|
||||||
|
if entry.Enabled {
|
||||||
|
if newIP, ok := results[entry.Domain]; ok {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("检查 %s: 当前IP=%s, 新IP=%s", entry.Domain, entry.IP, newIP)
|
||||||
|
}
|
||||||
|
if newIP != entry.IP {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("更新 %s: %s -> %s", entry.Domain, entry.IP, newIP)
|
||||||
|
}
|
||||||
|
block.Entries[i].IP = newIP
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if r.debugMode {
|
||||||
|
utils.LogDebug("解析失败: %s", entry.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if updated > 0 {
|
||||||
|
// 更新时间
|
||||||
|
block.UpdateAt = time.Now()
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
return fmt.Errorf("保存文件失败: %v", err)
|
||||||
|
}
|
||||||
|
utils.LogInfo("已更新 %d 个域名的IP地址", updated)
|
||||||
|
} else {
|
||||||
|
// 即使没有IP需要更新,也要更新时间戳,让用户知道定时任务确实执行了
|
||||||
|
block.UpdateAt = time.Now()
|
||||||
|
if err := hm.Save(); err != nil {
|
||||||
|
return fmt.Errorf("保存文件失败: %v", err)
|
||||||
|
}
|
||||||
|
utils.LogInfo("没有IP地址需要更新,已更新检查时间")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
271
core/doh.go
Normal file
271
core/doh.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DoHClient DoH客户端
|
||||||
|
type DoHClient struct {
|
||||||
|
client *http.Client
|
||||||
|
debugMode bool
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDoHClient 创建DoH客户端
|
||||||
|
func NewDoHClient(timeout time.Duration, debugMode bool) *DoHClient {
|
||||||
|
// 为DoH请求设置合理的超时时间
|
||||||
|
dohTimeout := timeout
|
||||||
|
if dohTimeout < 5*time.Second {
|
||||||
|
dohTimeout = 5 * time.Second // DoH请求至少5秒超时
|
||||||
|
}
|
||||||
|
if dohTimeout > 15*time.Second {
|
||||||
|
dohTimeout = 15 * time.Second // DoH请求最多15秒超时
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DoHClient{
|
||||||
|
debugMode: debugMode,
|
||||||
|
timeout: timeout,
|
||||||
|
client: &http.Client{
|
||||||
|
Timeout: dohTimeout,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
MaxIdleConns: 100,
|
||||||
|
MaxIdleConnsPerHost: 10,
|
||||||
|
IdleConnTimeout: 30 * time.Second,
|
||||||
|
TLSHandshakeTimeout: 5 * time.Second, // TLS握手超时
|
||||||
|
ExpectContinueTimeout: 1 * time.Second,
|
||||||
|
ResponseHeaderTimeout: 10 * time.Second, // 响应头超时
|
||||||
|
DisableKeepAlives: false, // 启用Keep-Alive以提高性能
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 使用DoH解析域名
|
||||||
|
func (c *DoHClient) Resolve(domain, dohURL string) (string, error) {
|
||||||
|
// 创建DNS查询消息
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(domain), dns.TypeA)
|
||||||
|
|
||||||
|
// 根据RFC 8484,为了HTTP缓存友好性,DNS ID应该设置为0
|
||||||
|
m.Id = 0
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH 创建DNS查询,域名: %s, ID: %d", domain, m.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将DNS消息打包
|
||||||
|
data, err := m.Pack()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("打包DNS消息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH DNS消息大小: %d 字节", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先尝试POST方法(推荐),fallback到GET方法
|
||||||
|
ip, err := c.doRequest(dohURL, data, "POST")
|
||||||
|
if err != nil {
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH POST请求失败,尝试GET: %v", err)
|
||||||
|
}
|
||||||
|
return c.doRequest(dohURL, data, "GET")
|
||||||
|
}
|
||||||
|
return ip, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// doRequest 执行DoH请求
|
||||||
|
func (c *DoHClient) doRequest(dohURL string, dnsData []byte, method string) (string, error) {
|
||||||
|
var req *http.Request
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if method == "POST" {
|
||||||
|
// POST方法:直接发送DNS消息作为请求体 (符合RFC 8484 Section 4.1)
|
||||||
|
// 确保URL不包含查询参数,因为POST使用原始路径
|
||||||
|
baseURL := strings.Split(dohURL, "?")[0]
|
||||||
|
|
||||||
|
req, err = http.NewRequest("POST", baseURL, bytes.NewReader(dnsData))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建DoH POST请求失败: %v", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/dns-message")
|
||||||
|
req.Header.Set("Content-Length", fmt.Sprintf("%d", len(dnsData)))
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH POST 请求URL: %s", baseURL)
|
||||||
|
utils.LogDebug("DoH POST 数据大小: %d 字节", len(dnsData))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// GET方法:Base64url编码并作为URL参数 (符合RFC 8484 Section 6)
|
||||||
|
// 使用Base64url编码(无填充),确保URL安全字符转换
|
||||||
|
b64data := base64.RawURLEncoding.EncodeToString(dnsData)
|
||||||
|
|
||||||
|
// 根据RFC 8484 Section 6,确保使用URL安全的Base64编码
|
||||||
|
// 虽然RawURLEncoding已经处理了大部分,但我们再次确认字符替换
|
||||||
|
b64data = strings.ReplaceAll(b64data, "+", "-")
|
||||||
|
b64data = strings.ReplaceAll(b64data, "/", "_")
|
||||||
|
// 移除任何可能的填充字符(RawURLEncoding应该已经不包含,但确保安全)
|
||||||
|
b64data = strings.TrimRight(b64data, "=")
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH GET Base64url编码后: %s (长度: %d)", b64data, len(b64data))
|
||||||
|
// 验证编码的正确性
|
||||||
|
if decoded, err := base64.RawURLEncoding.DecodeString(b64data); err == nil {
|
||||||
|
utils.LogDebug("DoH GET Base64url解码验证成功,原始数据长度: %d", len(decoded))
|
||||||
|
} else {
|
||||||
|
utils.LogDebug("DoH GET Base64url解码验证失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理URL模板:如果URL包含{?dns}模板,替换它;否则直接添加查询参数
|
||||||
|
var finalURL string
|
||||||
|
if strings.Contains(dohURL, "{?dns}") {
|
||||||
|
// URI模板格式,替换{?dns}
|
||||||
|
finalURL = strings.ReplaceAll(dohURL, "{?dns}", "?dns="+b64data)
|
||||||
|
} else if strings.Contains(dohURL, "?") {
|
||||||
|
// URL已包含查询参数,添加dns参数
|
||||||
|
finalURL = dohURL + "&dns=" + b64data
|
||||||
|
} else {
|
||||||
|
// URL不包含查询参数,添加dns参数
|
||||||
|
finalURL = dohURL + "?dns=" + b64data
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", finalURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("创建DoH GET请求失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH GET 请求URL: %s", finalURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置标准DoH头部(符合RFC 8484 Section 4.1)
|
||||||
|
req.Header.Set("Accept", "application/dns-message")
|
||||||
|
req.Header.Set("User-Agent", "HostSync/1.0")
|
||||||
|
|
||||||
|
// 根据RFC 8484建议,GET方法更缓存友好,但POST方法通常更小
|
||||||
|
if method == "GET" {
|
||||||
|
// GET请求更适合HTTP缓存
|
||||||
|
req.Header.Set("Cache-Control", "max-age=300")
|
||||||
|
} else {
|
||||||
|
// POST请求避免缓存问题
|
||||||
|
req.Header.Set("Cache-Control", "no-cache")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH %s请求 URL: %s", method, req.URL.String())
|
||||||
|
utils.LogDebug("DoH 请求头: %v", req.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置超时上下文
|
||||||
|
timeout := c.client.Timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
// 执行请求
|
||||||
|
resp, err := c.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
// 检查是否为超时错误
|
||||||
|
if ctx.Err() == context.DeadlineExceeded {
|
||||||
|
return "", fmt.Errorf("DoH请求超时 (超过%v): %v", timeout, err)
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("DoH请求失败: %v", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH 响应状态: %d %s", resp.StatusCode, resp.Status)
|
||||||
|
utils.LogDebug("DoH 响应头: %v", resp.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据RFC 8484 Section 4.2.1,成功的2xx状态码用于任何有效的DNS响应
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("DoH请求返回错误状态: %d %s, 响应体: %s",
|
||||||
|
resp.StatusCode, resp.Status, string(bodyBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证响应内容类型(符合RFC 8484 Section 6)
|
||||||
|
contentType := resp.Header.Get("Content-Type")
|
||||||
|
if contentType != "" && !strings.Contains(contentType, "application/dns-message") {
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH 意外的Content-Type: %s (期望: application/dns-message)", contentType)
|
||||||
|
}
|
||||||
|
// 继续处理,某些服务器可能不设置正确的Content-Type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取响应数据
|
||||||
|
respData, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("读取DoH响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH 响应数据大小: %d 字节", len(respData))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查响应数据是否为空
|
||||||
|
if len(respData) == 0 {
|
||||||
|
return "", fmt.Errorf("DoH响应数据为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析DNS响应
|
||||||
|
var respMsg dns.Msg
|
||||||
|
if err := respMsg.Unpack(respData); err != nil {
|
||||||
|
return "", fmt.Errorf("解析DNS响应失败 (数据可能损坏): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH DNS响应 - ID: %d, 答案数量: %d, 响应代码: %d (%s)",
|
||||||
|
respMsg.Id, len(respMsg.Answer), respMsg.Rcode, dns.RcodeToString[respMsg.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查DNS响应代码(符合RFC标准)
|
||||||
|
if respMsg.Rcode != dns.RcodeSuccess {
|
||||||
|
return "", fmt.Errorf("DNS响应错误,响应代码: %d (%s)",
|
||||||
|
respMsg.Rcode, dns.RcodeToString[respMsg.Rcode])
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(respMsg.Answer) == 0 {
|
||||||
|
return "", fmt.Errorf("DoH响应中没有找到答案记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找A记录
|
||||||
|
for _, ans := range respMsg.Answer {
|
||||||
|
if a, ok := ans.(*dns.A); ok {
|
||||||
|
if c.debugMode {
|
||||||
|
utils.LogDebug("DoH 找到A记录: %s (TTL: %d)", a.A.String(), a.Hdr.Ttl)
|
||||||
|
}
|
||||||
|
return a.A.String(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("DoH响应中没有找到A记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 测试DoH服务器
|
||||||
|
func (c *DoHClient) Test(server string) (time.Duration, error) {
|
||||||
|
start := time.Now()
|
||||||
|
_, err := c.Resolve("github.com", server)
|
||||||
|
duration := time.Since(start)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return duration, nil
|
||||||
|
}
|
396
core/hosts.go
Normal file
396
core/hosts.go
Normal file
@ -0,0 +1,396 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostEntry hosts条目
|
||||||
|
type HostEntry struct {
|
||||||
|
IP string
|
||||||
|
Domain string
|
||||||
|
Comment string
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostBlock hosts块
|
||||||
|
type HostBlock struct {
|
||||||
|
Name string
|
||||||
|
Entries []HostEntry
|
||||||
|
DNS string
|
||||||
|
DoH string
|
||||||
|
Server string
|
||||||
|
CronJob string
|
||||||
|
UpdateAt time.Time
|
||||||
|
Enabled bool
|
||||||
|
Comments []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostsManager hosts文件管理器
|
||||||
|
type HostsManager struct {
|
||||||
|
FilePath string
|
||||||
|
Blocks map[string]*HostBlock
|
||||||
|
Others []string // 非块内容
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
blockStartPattern = regexp.MustCompile(`^#\s*([a-zA-Z0-9_-]+):\s*$`)
|
||||||
|
blockEndPattern = regexp.MustCompile(`^#\s*([a-zA-Z0-9_-]+);\s*$`)
|
||||||
|
commentPattern = regexp.MustCompile(`^#\s*(\w+):\s*(.*)$`)
|
||||||
|
hostPattern = regexp.MustCompile(`^#?\s*([^\s#]+)\s+([^\s#]+)(?:\s*#(.*))?$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewHostsManager 创建hosts管理器
|
||||||
|
func NewHostsManager() *HostsManager {
|
||||||
|
return &HostsManager{
|
||||||
|
FilePath: config.AppConfig.HostsPath,
|
||||||
|
Blocks: make(map[string]*HostBlock),
|
||||||
|
Others: make([]string, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 加载hosts文件
|
||||||
|
func (hm *HostsManager) Load() error {
|
||||||
|
if !utils.FileExists(hm.FilePath) {
|
||||||
|
return fmt.Errorf("hosts文件不存在: %s", hm.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Open(hm.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开hosts文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentBlock *HostBlock
|
||||||
|
var inBlock bool
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// 先检查是否是配置注释(在块内)
|
||||||
|
if inBlock && currentBlock != nil {
|
||||||
|
if matches := commentPattern.FindStringSubmatch(line); matches != nil {
|
||||||
|
// 这是配置注释,处理块内容
|
||||||
|
hm.parseBlockLine(line, currentBlock)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查块开始
|
||||||
|
if matches := blockStartPattern.FindStringSubmatch(line); matches != nil {
|
||||||
|
blockName := matches[1]
|
||||||
|
currentBlock = &HostBlock{
|
||||||
|
Name: blockName,
|
||||||
|
Entries: make([]HostEntry, 0),
|
||||||
|
Comments: make([]string, 0),
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
hm.Blocks[blockName] = currentBlock
|
||||||
|
inBlock = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查块结束
|
||||||
|
if matches := blockEndPattern.FindStringSubmatch(line); matches != nil {
|
||||||
|
inBlock = false
|
||||||
|
currentBlock = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if inBlock && currentBlock != nil {
|
||||||
|
// 处理块内容
|
||||||
|
hm.parseBlockLine(line, currentBlock)
|
||||||
|
} else {
|
||||||
|
// 非块内容 - 只保存非空行
|
||||||
|
originalLine := scanner.Text()
|
||||||
|
if strings.TrimSpace(originalLine) != "" {
|
||||||
|
hm.Others = append(hm.Others, originalLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseBlockLine 解析块内的行
|
||||||
|
func (hm *HostsManager) parseBlockLine(line string, block *HostBlock) {
|
||||||
|
originalLine := line
|
||||||
|
trimmedLine := strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// 检查是否是配置注释
|
||||||
|
if matches := commentPattern.FindStringSubmatch(trimmedLine); matches != nil {
|
||||||
|
key, value := matches[1], matches[2]
|
||||||
|
switch key {
|
||||||
|
case "useDns":
|
||||||
|
block.DNS = value
|
||||||
|
case "useDoh":
|
||||||
|
block.DoH = value
|
||||||
|
case "useSrv":
|
||||||
|
block.Server = value
|
||||||
|
case "cronJob":
|
||||||
|
block.CronJob = value
|
||||||
|
case "updateAt":
|
||||||
|
if t, err := time.Parse("2006-01-02 15:04:05", value); err == nil {
|
||||||
|
block.UpdateAt = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是host条目(包括被注释的)
|
||||||
|
if matches := hostPattern.FindStringSubmatch(trimmedLine); matches != nil {
|
||||||
|
ip, domain := matches[1], matches[2]
|
||||||
|
comment := ""
|
||||||
|
enabled := true
|
||||||
|
|
||||||
|
if len(matches) > 3 && matches[3] != "" {
|
||||||
|
comment = strings.TrimSpace(matches[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否被注释掉(以 # 开头)
|
||||||
|
if strings.HasPrefix(trimmedLine, "#") {
|
||||||
|
enabled = false
|
||||||
|
// 需要从注释中提取实际的 IP 和域名
|
||||||
|
// 重新解析去掉开头 # 后的内容
|
||||||
|
lineWithoutComment := strings.TrimSpace(strings.TrimPrefix(trimmedLine, "#"))
|
||||||
|
if newMatches := regexp.MustCompile(`^([^\s#]+)\s+([^\s#]+)(?:\s*#(.*))?$`).FindStringSubmatch(lineWithoutComment); newMatches != nil {
|
||||||
|
ip, domain = newMatches[1], newMatches[2]
|
||||||
|
if len(newMatches) > 3 && newMatches[3] != "" {
|
||||||
|
comment = strings.TrimSpace(newMatches[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := HostEntry{
|
||||||
|
IP: ip,
|
||||||
|
Domain: domain,
|
||||||
|
Comment: comment,
|
||||||
|
Enabled: enabled,
|
||||||
|
}
|
||||||
|
block.Entries = append(block.Entries, entry)
|
||||||
|
return
|
||||||
|
} // 其他注释
|
||||||
|
if strings.HasPrefix(trimmedLine, "#") {
|
||||||
|
block.Comments = append(block.Comments, originalLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save 保存hosts文件
|
||||||
|
func (hm *HostsManager) Save() error {
|
||||||
|
// 备份原文件
|
||||||
|
if err := utils.BackupFile(hm.FilePath); err != nil {
|
||||||
|
return fmt.Errorf("备份文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := os.Create(hm.FilePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建hosts文件失败: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// 写入非块内容 (过滤空行并确保末尾只有一个空行)
|
||||||
|
hasNonBlockContent := false
|
||||||
|
for _, line := range hm.Others {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
if hasNonBlockContent {
|
||||||
|
fmt.Fprintln(file) // 只在有内容时添加空行分隔
|
||||||
|
}
|
||||||
|
fmt.Fprintln(file, line)
|
||||||
|
hasNonBlockContent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入块内容
|
||||||
|
blockNames := make([]string, 0, len(hm.Blocks))
|
||||||
|
for name := range hm.Blocks {
|
||||||
|
blockNames = append(blockNames, name)
|
||||||
|
}
|
||||||
|
sort.Strings(blockNames)
|
||||||
|
|
||||||
|
for i, name := range blockNames {
|
||||||
|
block := hm.Blocks[name]
|
||||||
|
// 在块之间添加空行分隔(除了第一个块且前面有非块内容)
|
||||||
|
if i > 0 || hasNonBlockContent {
|
||||||
|
fmt.Fprintln(file)
|
||||||
|
}
|
||||||
|
hm.writeBlock(file, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// writeBlock 写入块内容
|
||||||
|
func (hm *HostsManager) writeBlock(file *os.File, block *HostBlock) {
|
||||||
|
fmt.Fprintf(file, "# %s:\n", block.Name)
|
||||||
|
|
||||||
|
// 写入hosts条目
|
||||||
|
for _, entry := range block.Entries {
|
||||||
|
prefix := ""
|
||||||
|
if !entry.Enabled {
|
||||||
|
prefix = "# "
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry.Comment != "" {
|
||||||
|
fmt.Fprintf(file, "%s%-16s %s # %s\n", prefix, entry.IP, entry.Domain, entry.Comment)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(file, "%s%-16s %s\n", prefix, entry.IP, entry.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入配置注释
|
||||||
|
if block.DNS != "" {
|
||||||
|
fmt.Fprintf(file, "# useDns: %s\n", block.DNS)
|
||||||
|
}
|
||||||
|
if block.DoH != "" {
|
||||||
|
fmt.Fprintf(file, "# useDoh: %s\n", block.DoH)
|
||||||
|
}
|
||||||
|
if block.Server != "" {
|
||||||
|
fmt.Fprintf(file, "# useSrv: %s\n", block.Server)
|
||||||
|
}
|
||||||
|
if block.CronJob != "" {
|
||||||
|
fmt.Fprintf(file, "# cronJob: %s\n", block.CronJob)
|
||||||
|
}
|
||||||
|
if !block.UpdateAt.IsZero() {
|
||||||
|
fmt.Fprintf(file, "# updateAt: %s\n", block.UpdateAt.Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入其他注释
|
||||||
|
for _, comment := range block.Comments {
|
||||||
|
fmt.Fprintln(file, comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(file, "# %s;\n", block.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBlock 获取指定块
|
||||||
|
func (hm *HostsManager) GetBlock(name string) *HostBlock {
|
||||||
|
return hm.Blocks[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBlock 创建新块
|
||||||
|
func (hm *HostsManager) CreateBlock(name string) *HostBlock {
|
||||||
|
if !utils.ValidateBlockName(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := hm.Blocks[name]; exists {
|
||||||
|
return hm.Blocks[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
block := &HostBlock{
|
||||||
|
Name: name,
|
||||||
|
Entries: make([]HostEntry, 0),
|
||||||
|
Comments: make([]string, 0),
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
hm.Blocks[name] = block
|
||||||
|
return block
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteBlock 删除块
|
||||||
|
func (hm *HostsManager) DeleteBlock(name string) error {
|
||||||
|
if _, exists := hm.Blocks[name]; !exists {
|
||||||
|
return fmt.Errorf("块不存在: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(hm.Blocks, name)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableBlock 启用块
|
||||||
|
func (hm *HostsManager) EnableBlock(name string) error {
|
||||||
|
block := hm.GetBlock(name)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("块不存在: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range block.Entries {
|
||||||
|
block.Entries[i].Enabled = true
|
||||||
|
}
|
||||||
|
block.Enabled = true
|
||||||
|
|
||||||
|
return hm.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableBlock 禁用块
|
||||||
|
func (hm *HostsManager) DisableBlock(name string) error {
|
||||||
|
block := hm.GetBlock(name)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("块不存在: %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range block.Entries {
|
||||||
|
block.Entries[i].Enabled = false
|
||||||
|
}
|
||||||
|
block.Enabled = false
|
||||||
|
|
||||||
|
return hm.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntry 添加条目
|
||||||
|
func (hm *HostsManager) AddEntry(blockName, domain, ip string) error {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
block = hm.CreateBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("无法创建块: %s", blockName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在
|
||||||
|
for i, entry := range block.Entries {
|
||||||
|
if entry.Domain == domain {
|
||||||
|
// 更新IP
|
||||||
|
block.Entries[i].IP = ip
|
||||||
|
block.Entries[i].Enabled = true
|
||||||
|
return hm.Save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新条目
|
||||||
|
entry := HostEntry{
|
||||||
|
IP: ip,
|
||||||
|
Domain: domain,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
block.Entries = append(block.Entries, entry)
|
||||||
|
|
||||||
|
return hm.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEntry 删除条目
|
||||||
|
func (hm *HostsManager) RemoveEntry(blockName, domain string) error {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("块不存在: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, entry := range block.Entries {
|
||||||
|
if entry.Domain == domain {
|
||||||
|
// 删除条目
|
||||||
|
block.Entries = append(block.Entries[:i], block.Entries[i+1:]...)
|
||||||
|
return hm.Save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("域名不存在: %s", domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateBlockTime 更新块的更新时间
|
||||||
|
func (hm *HostsManager) UpdateBlockTime(blockName string) error {
|
||||||
|
block := hm.GetBlock(blockName)
|
||||||
|
if block == nil {
|
||||||
|
return fmt.Errorf("块不存在: %s", blockName)
|
||||||
|
}
|
||||||
|
|
||||||
|
block.UpdateAt = time.Now()
|
||||||
|
return hm.Save()
|
||||||
|
}
|
34
go.mod
Normal file
34
go.mod
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
module github.com/evil7/hostsync
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.4
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/miekg/dns v1.1.66
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/spf13/viper v1.20.1
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.14.0 // indirect
|
||||||
|
github.com/spf13/cast v1.9.2 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
golang.org/x/mod v0.25.0 // indirect
|
||||||
|
golang.org/x/net v0.41.0 // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/sys v0.33.0
|
||||||
|
golang.org/x/text v0.26.0 // indirect
|
||||||
|
golang.org/x/tools v0.34.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
69
go.sum
Normal file
69
go.sum
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
|
||||||
|
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
|
||||||
|
github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||||
|
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
|
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||||
|
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||||
|
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||||
|
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
|
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||||
|
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
|
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
||||||
|
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||||
|
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||||
|
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
|
||||||
|
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
330
install.ps1
Normal file
330
install.ps1
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
# 使用方法: irm "https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.ps1" | iex
|
||||||
|
|
||||||
|
# 设置严格模式和错误处理
|
||||||
|
Set-StrictMode -Version 3.0
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
# 默认配置
|
||||||
|
$Version = 'latest'
|
||||||
|
$InstallDir = "$env:USERPROFILE\.hostsync\bin"
|
||||||
|
$NoInit = $false
|
||||||
|
$Portable = $false
|
||||||
|
|
||||||
|
# 颜色输出函数
|
||||||
|
function Write-ColorOutput {
|
||||||
|
param([string]$Message, [string]$Color = 'White')
|
||||||
|
Write-Host $Message -ForegroundColor $Color
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Success { param([string]$Message) Write-ColorOutput $Message 'Green' }
|
||||||
|
function Write-Info { param([string]$Message) Write-ColorOutput $Message 'Cyan' }
|
||||||
|
function Write-Warning { param([string]$Message) Write-ColorOutput $Message 'Yellow' }
|
||||||
|
function Write-Error { param([string]$Message) Write-ColorOutput $Message 'Red' }
|
||||||
|
|
||||||
|
# 检查管理员权限
|
||||||
|
function Test-Administrator {
|
||||||
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||||
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取最新版本
|
||||||
|
function Get-LatestVersion {
|
||||||
|
try {
|
||||||
|
Write-Info '? 获取最新版本信息...'
|
||||||
|
$api = 'https://git.xykqyy.com/api/v1/repos/ljp/hostSync/releases/latest'
|
||||||
|
$response = Invoke-RestMethod -Uri $api -ErrorAction Stop
|
||||||
|
return $response.tag_name
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning '?? 无法获取最新版本,使用 v1.0.0'
|
||||||
|
return 'v1.0.0'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检测系统架构
|
||||||
|
function Get-Architecture {
|
||||||
|
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||||
|
switch ($arch) {
|
||||||
|
'AMD64' { return 'amd64' }
|
||||||
|
'x86' { return '386' }
|
||||||
|
'ARM64' { return 'arm64' }
|
||||||
|
default { return 'amd64' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
function Get-FileFromUrl {
|
||||||
|
param([string]$Url, [string]$Output)
|
||||||
|
|
||||||
|
Write-Info "? 下载: $(Split-Path $Output -Leaf)"
|
||||||
|
Write-Info "? 地址: $Url"
|
||||||
|
|
||||||
|
try {
|
||||||
|
# 使用 Invoke-WebRequest 下载
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
Invoke-WebRequest -Uri $Url -OutFile $Output -ErrorAction Stop
|
||||||
|
$ProgressPreference = 'Continue'
|
||||||
|
|
||||||
|
if (Test-Path $Output) {
|
||||||
|
$size = [math]::Round((Get-Item $Output).Length / 1MB, 2)
|
||||||
|
Write-Success "? 下载完成 ($size MB)"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "? IWR 下载失败: $($_.Exception.Message)"
|
||||||
|
# 尝试使用 curl 作为备用方案
|
||||||
|
Write-Info '? 尝试使用 curl...'
|
||||||
|
try {
|
||||||
|
$curlArgs = @('-L', $Url, '-o', $Output, '--silent', '--show-error')
|
||||||
|
$curlProcess = Start-Process -FilePath 'curl.exe' -ArgumentList $curlArgs -Wait -PassThru -WindowStyle Hidden
|
||||||
|
|
||||||
|
if ($curlProcess.ExitCode -eq 0 -and (Test-Path $Output)) {
|
||||||
|
Write-Success '? 使用 curl 下载完成'
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Error "? curl 退出码: $($curlProcess.ExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "? curl 执行失败: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查并卸载已有服务
|
||||||
|
function Remove-ExistingService {
|
||||||
|
Write-Info '? 检查现有安装...'
|
||||||
|
|
||||||
|
# 检查 PATH 中的 hostsync
|
||||||
|
$existingPath = $null
|
||||||
|
$pathDirs = $env:PATH -split ';'
|
||||||
|
foreach ($dir in $pathDirs) {
|
||||||
|
$testPath = Join-Path $dir 'hostsync.exe'
|
||||||
|
if (Test-Path $testPath) {
|
||||||
|
$existingPath = $testPath
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查默认安装位置
|
||||||
|
if (-not $existingPath) {
|
||||||
|
$defaultPath = "$env:USERPROFILE\.hostsync\bin\hostsync.exe"
|
||||||
|
if (Test-Path $defaultPath) {
|
||||||
|
$existingPath = $defaultPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($existingPath) {
|
||||||
|
Write-Info "? 发现现有安装: $existingPath"
|
||||||
|
|
||||||
|
try {
|
||||||
|
# 尝试停止并卸载服务
|
||||||
|
Write-Info '?? 停止现有服务...'
|
||||||
|
$stopArgs = @{
|
||||||
|
FilePath = $existingPath
|
||||||
|
ArgumentList = @('service', 'stop')
|
||||||
|
Wait = $true
|
||||||
|
PassThru = $true
|
||||||
|
RedirectStandardOutput = $true
|
||||||
|
RedirectStandardError = $true
|
||||||
|
WindowStyle = 'Hidden'
|
||||||
|
}
|
||||||
|
Start-Process @stopArgs | Out-Null
|
||||||
|
|
||||||
|
Write-Info '?? 卸载现有服务...'
|
||||||
|
$uninstallArgs = @{
|
||||||
|
FilePath = $existingPath
|
||||||
|
ArgumentList = @('service', 'uninstall')
|
||||||
|
Wait = $true
|
||||||
|
PassThru = $true
|
||||||
|
RedirectStandardOutput = $true
|
||||||
|
RedirectStandardError = $true
|
||||||
|
WindowStyle = 'Hidden'
|
||||||
|
}
|
||||||
|
Start-Process @uninstallArgs | Out-Null
|
||||||
|
|
||||||
|
Write-Success '? 已清理现有服务'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "?? 清理现有服务时出现错误: $($_.Exception.Message)"
|
||||||
|
Write-Info '? 继续安装过程...'
|
||||||
|
}
|
||||||
|
|
||||||
|
# 等待一下确保服务完全停止
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Info '? 未发现现有安装'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主安装函数
|
||||||
|
function Install-HostSync {
|
||||||
|
Write-ColorOutput @'
|
||||||
|
? HostSync 快速安装脚本
|
||||||
|
===============================
|
||||||
|
强大的 Hosts 文件管理工具
|
||||||
|
|
||||||
|
功能特点:
|
||||||
|
- ? 分块管理 Hosts 配置
|
||||||
|
- ? 智能 DNS 解析
|
||||||
|
- ? 定时自动更新
|
||||||
|
- ? 后台服务运行
|
||||||
|
|
||||||
|
'@ 'Magenta'
|
||||||
|
|
||||||
|
# 检查并卸载已有服务
|
||||||
|
Remove-ExistingService
|
||||||
|
|
||||||
|
# 检查管理员权限
|
||||||
|
if (-not (Test-Administrator)) {
|
||||||
|
Write-Warning '?? 未检测到管理员权限,将跳过服务安装'
|
||||||
|
Write-Info '? 如需安装服务,请稍后以管理员身份运行: hostsync service install'
|
||||||
|
$NoInit = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确定版本
|
||||||
|
if ($Version -eq 'latest') {
|
||||||
|
$Version = Get-LatestVersion
|
||||||
|
}
|
||||||
|
Write-Info "? 安装版本: $Version"# 处理版本号格式 - 确保有v前缀用于tag,去掉v前缀用于文件名
|
||||||
|
$TagVersion = $Version
|
||||||
|
$FileVersion = $Version
|
||||||
|
if ($Version -match '^v(\d+\.\d+\.\d+)$') {
|
||||||
|
$TagVersion = $Version
|
||||||
|
$FileVersion = $matches[1]
|
||||||
|
}
|
||||||
|
elseif ($Version -match '^(\d+\.\d+\.\d+)$') {
|
||||||
|
$TagVersion = "v$Version"
|
||||||
|
$FileVersion = $Version
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确定架构
|
||||||
|
$Arch = Get-Architecture
|
||||||
|
Write-Info "?? 系统架构: $Arch" # 确定安装目录 (固定为默认位置)
|
||||||
|
Write-Info "? 安装目录: $InstallDir"
|
||||||
|
|
||||||
|
# 创建安装目录
|
||||||
|
if (-not (Test-Path $InstallDir)) {
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||||
|
Write-Success '? 创建安装目录'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error "? 创建目录失败: $($_.Exception.Message)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
} # 构建下载 URL
|
||||||
|
$FileName = "hostsync-$FileVersion-windows-$Arch.exe"
|
||||||
|
$DownloadUrl = "https://git.xykqyy.com/ljp/hostSync/releases/download/$TagVersion/$FileName"
|
||||||
|
$OutputFile = Join-Path $InstallDir 'hostsync.exe' # 下载文件
|
||||||
|
if (-not (Get-FileFromUrl $DownloadUrl $OutputFile)) {
|
||||||
|
Write-Error '? 下载失败,请检查网络连接或手动下载'
|
||||||
|
Write-Info '? 手动下载地址: https://git.xykqyy.com/ljp/hostSync/releases'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 验证文件
|
||||||
|
if (-not (Test-Path $OutputFile)) {
|
||||||
|
Write-Error '? 安装文件不存在'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Success '? HostSync 下载完成!' # 添加到 PATH
|
||||||
|
Write-Info '? 配置环境变量...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
$currentPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||||
|
if ($currentPath -notlike "*$InstallDir*") {
|
||||||
|
$newPath = "$currentPath;$InstallDir"
|
||||||
|
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
|
||||||
|
Write-Success '? 已添加到 PATH 环境变量'
|
||||||
|
Write-Warning '?? 请重启终端以生效环境变量'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Info '? PATH 已包含安装目录'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "?? 设置环境变量失败: $($_.Exception.Message)"
|
||||||
|
Write-Info "? 你可以手动将 '$InstallDir' 添加到 PATH"
|
||||||
|
}# 运行初始化
|
||||||
|
if (-not $NoInit) {
|
||||||
|
Write-Info '? 开始初始化 HostSync...'
|
||||||
|
|
||||||
|
try {
|
||||||
|
# 使用 Start-Process 来更好地控制进程执行
|
||||||
|
Write-Info "? 执行命令: $OutputFile init"
|
||||||
|
|
||||||
|
$processArgs = @{
|
||||||
|
FilePath = $OutputFile
|
||||||
|
ArgumentList = @('init')
|
||||||
|
Wait = $true
|
||||||
|
PassThru = $true
|
||||||
|
RedirectStandardOutput = $true
|
||||||
|
RedirectStandardError = $true
|
||||||
|
WindowStyle = 'Hidden'
|
||||||
|
}
|
||||||
|
|
||||||
|
$process = Start-Process @processArgs
|
||||||
|
|
||||||
|
if ($process.ExitCode -eq 0) {
|
||||||
|
Write-Success '? 初始化完成!'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warning "?? 初始化退出码: $($process.ExitCode)"
|
||||||
|
Write-Info '? 可能需要管理员权限,你可以稍后手动运行:'
|
||||||
|
Write-Info ' 以管理员身份打开 PowerShell'
|
||||||
|
Write-Info ' 运行: hostsync init'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "?? 初始化过程出现异常: $($_.Exception.Message)"
|
||||||
|
Write-Info '? 请尝试手动初始化:'
|
||||||
|
Write-Info ' 1. 以管理员身份打开 PowerShell'
|
||||||
|
Write-Info " 2. 运行: `"$OutputFile`" init"
|
||||||
|
Write-Info ' 3. 或者: hostsync init (如果已添加到PATH)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 显示完成信息
|
||||||
|
Write-Success @"
|
||||||
|
|
||||||
|
? HostSync 安装完成!
|
||||||
|
=====================
|
||||||
|
|
||||||
|
? 安装位置: $OutputFile
|
||||||
|
? 配置目录: $env:USERPROFILE\.hostsync\
|
||||||
|
|
||||||
|
? 快速开始:
|
||||||
|
"@
|
||||||
|
|
||||||
|
Write-Info ' hostsync list # 查看配置'
|
||||||
|
Write-Info ' hostsync add test example.com # 添加域名'
|
||||||
|
Write-Info @'
|
||||||
|
hostsync update # 更新解析
|
||||||
|
hostsync service status # 查看服务状态
|
||||||
|
|
||||||
|
? 更多帮助:
|
||||||
|
hostsync help # 查看帮助
|
||||||
|
hostsync version # 查看版本
|
||||||
|
|
||||||
|
? 项目地址: https://git.xykqyy.com/ljp/hostSync
|
||||||
|
'@
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误处理
|
||||||
|
trap {
|
||||||
|
Write-Error "? 安装过程中发生错误: $($_.Exception.Message)"
|
||||||
|
Write-Info '? 请检查网络连接或手动下载安装'
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 执行安装
|
||||||
|
Remove-ExistingService
|
||||||
|
Install-HostSync
|
346
install.sh
Normal file
346
install.sh
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# 使用方法: curl -fsSL https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.sh | bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 默认配置
|
||||||
|
VERSION="latest"
|
||||||
|
INSTALL_DIR="$HOME/.hostsync/bin"
|
||||||
|
NO_INIT=false
|
||||||
|
PORTABLE=false
|
||||||
|
FORCE=false
|
||||||
|
|
||||||
|
# 颜色输出
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
CYAN='\033[0;36m'
|
||||||
|
MAGENTA='\033[0;35m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# 输出函数
|
||||||
|
info() { echo -e "${CYAN}$1${NC}"; }
|
||||||
|
success() { echo -e "${GREEN}$1${NC}"; }
|
||||||
|
warning() { echo -e "${YELLOW}$1${NC}"; }
|
||||||
|
error() { echo -e "${RED}$1${NC}"; }
|
||||||
|
|
||||||
|
# 检查并卸载已有服务
|
||||||
|
remove_existing_service() {
|
||||||
|
info "🔍 检查现有安装..."
|
||||||
|
|
||||||
|
# 检查 PATH 中的 hostsync
|
||||||
|
local existing_path=""
|
||||||
|
if command -v hostsync >/dev/null 2>&1; then
|
||||||
|
existing_path=$(which hostsync)
|
||||||
|
elif [[ -f "$HOME/.hostsync/bin/hostsync" ]]; then
|
||||||
|
existing_path="$HOME/.hostsync/bin/hostsync"
|
||||||
|
elif [[ -f "/usr/local/bin/hostsync" ]]; then
|
||||||
|
existing_path="/usr/local/bin/hostsync"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$existing_path" ]]; then
|
||||||
|
info "📦 发现现有安装: $existing_path"
|
||||||
|
|
||||||
|
# 尝试停止并卸载服务
|
||||||
|
info "⏹️ 停止现有服务..."
|
||||||
|
"$existing_path" service stop >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
info "🗑️ 卸载现有服务..."
|
||||||
|
"$existing_path" service uninstall >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
success "✅ 已清理现有服务"
|
||||||
|
|
||||||
|
# 等待一下确保服务完全停止
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
info "💡 未发现现有安装"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检查root权限
|
||||||
|
check_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
warning "⚠️ 未检测到 root 权限,将跳过服务安装"
|
||||||
|
info "💡 如需安装服务,请稍后以 root 身份运行: hostsync service install"
|
||||||
|
NO_INIT=true
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 检测操作系统和架构
|
||||||
|
detect_platform() {
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
case $OS in
|
||||||
|
linux*)
|
||||||
|
OS="linux"
|
||||||
|
;;
|
||||||
|
darwin*)
|
||||||
|
OS="darwin"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "❌ 不支持的操作系统: $OS"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case $ARCH in
|
||||||
|
x86_64 | amd64)
|
||||||
|
ARCH="amd64"
|
||||||
|
;;
|
||||||
|
i*86 | x86)
|
||||||
|
ARCH="386"
|
||||||
|
;;
|
||||||
|
aarch64 | arm64)
|
||||||
|
ARCH="arm64"
|
||||||
|
;;
|
||||||
|
armv7* | armv6*)
|
||||||
|
ARCH="armv7"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "❌ 不支持的架构: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
info "🏗️ 检测到平台: $OS-$ARCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取最新版本
|
||||||
|
get_latest_version() {
|
||||||
|
if [[ "$VERSION" == "latest" ]]; then
|
||||||
|
info "🔍 获取最新版本信息..."
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
VERSION=$(curl -fsSL "https://git.xykqyy.com/api/v1/repos/ljp/hostSync/releases/latest" | grep '"tag_name"' | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/')
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
VERSION=$(wget -qO- "https://git.xykqyy.com/api/v1/repos/ljp/hostSync/releases/latest" | grep '"tag_name"' | sed -E 's/.*"tag_name":\s*"([^"]+)".*/\1/')
|
||||||
|
else
|
||||||
|
warning "⚠️ 无法获取最新版本,使用 v1.0.0"
|
||||||
|
VERSION="v1.0.0"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
warning "⚠️ 无法获取版本信息,使用 v1.0.0"
|
||||||
|
VERSION="v1.0.0"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "📦 安装版本: $VERSION"
|
||||||
|
|
||||||
|
# 处理版本号格式 - 分离tag版本和文件版本
|
||||||
|
TAG_VERSION="$VERSION"
|
||||||
|
FILE_VERSION="$VERSION"
|
||||||
|
|
||||||
|
# 确保tag版本有v前缀
|
||||||
|
if [[ ! "$TAG_VERSION" =~ ^v ]]; then
|
||||||
|
TAG_VERSION="v$TAG_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确保文件版本没有v前缀
|
||||||
|
if [[ "$FILE_VERSION" =~ ^v ]]; then
|
||||||
|
FILE_VERSION="${FILE_VERSION:1}" # 去掉 'v' 前缀
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! "$FILE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
error "❌ 无效的版本号格式: $FILE_VERSION"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 确定安装目录
|
||||||
|
setup_install_dir() {
|
||||||
|
if [[ -z "$INSTALL_DIR" ]]; then
|
||||||
|
if [[ "$PORTABLE" == true ]]; then
|
||||||
|
INSTALL_DIR="./hostsync"
|
||||||
|
else
|
||||||
|
INSTALL_DIR="$HOME/.hostsync/bin"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "📂 安装目录: $INSTALL_DIR"
|
||||||
|
|
||||||
|
# 创建安装目录
|
||||||
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
||||||
|
mkdir -p "$INSTALL_DIR" || {
|
||||||
|
error "❌ 创建目录失败: $INSTALL_DIR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
success "✅ 创建安装目录"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
download_file() {
|
||||||
|
local url="$1"
|
||||||
|
local output="$2"
|
||||||
|
|
||||||
|
info "📥 下载: $(basename "$output")"
|
||||||
|
info "🌐 地址: $url"
|
||||||
|
|
||||||
|
# 尝试使用 curl
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
if curl -fsSL "$url" -o "$output"; then
|
||||||
|
success "✅ 下载完成"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 尝试使用 wget
|
||||||
|
if command -v wget >/dev/null 2>&1; then
|
||||||
|
if wget -q "$url" -O "$output"; then
|
||||||
|
success "✅ 下载完成"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
error "❌ 下载失败,请检查网络连接"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 设置环境变量
|
||||||
|
setup_environment() {
|
||||||
|
if [[ "$PORTABLE" == true ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "🔧 配置环境变量..."
|
||||||
|
|
||||||
|
# 确定 shell 配置文件
|
||||||
|
local shell_rc=""
|
||||||
|
case "$SHELL" in
|
||||||
|
*/zsh)
|
||||||
|
shell_rc="$HOME/.zshrc"
|
||||||
|
;;
|
||||||
|
*/bash)
|
||||||
|
shell_rc="$HOME/.bashrc"
|
||||||
|
;;
|
||||||
|
*/fish)
|
||||||
|
shell_rc="$HOME/.config/fish/config.fish"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shell_rc="$HOME/.profile"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 添加到 PATH
|
||||||
|
local path_line="export PATH=\"$INSTALL_DIR:\$PATH\""
|
||||||
|
|
||||||
|
if [[ "$SHELL" == */fish ]]; then
|
||||||
|
path_line="set -gx PATH $INSTALL_DIR \$PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$shell_rc" ]] && grep -q "$INSTALL_DIR" "$shell_rc"; then
|
||||||
|
info "💡 PATH 已包含安装目录"
|
||||||
|
else
|
||||||
|
echo "" >>"$shell_rc"
|
||||||
|
echo "# HostSync" >>"$shell_rc"
|
||||||
|
echo "$path_line" >>"$shell_rc"
|
||||||
|
success "✅ 已添加到 PATH ($shell_rc)"
|
||||||
|
warning "⚠️ 请重新加载终端或运行: source $shell_rc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主安装函数
|
||||||
|
install_hostsync() {
|
||||||
|
cat <<'EOF'
|
||||||
|
🚀 HostSync 快速安装脚本
|
||||||
|
===============================
|
||||||
|
强大的 Hosts 文件管理工具
|
||||||
|
|
||||||
|
功能特点:
|
||||||
|
- 🎯 分块管理 Hosts 配置
|
||||||
|
- 🌐 智能 DNS 解析
|
||||||
|
- ⏰ 定时自动更新
|
||||||
|
- 🔧 后台服务运行
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 检查依赖
|
||||||
|
if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then
|
||||||
|
error "❌ 需要 curl 或 wget 来下载文件"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 检查并卸载已有服务
|
||||||
|
remove_existing_service
|
||||||
|
|
||||||
|
# 检查权限
|
||||||
|
check_root
|
||||||
|
|
||||||
|
# 检测平台
|
||||||
|
detect_platform
|
||||||
|
|
||||||
|
# 获取版本
|
||||||
|
get_latest_version
|
||||||
|
|
||||||
|
# 设置安装目录
|
||||||
|
setup_install_dir
|
||||||
|
|
||||||
|
# 构建下载 URL
|
||||||
|
local filename="hostsync-$FILE_VERSION-$OS-$ARCH"
|
||||||
|
local download_url="https://git.xykqyy.com/ljp/hostSync/releases/download/$TAG_VERSION/$filename"
|
||||||
|
local output_file="$INSTALL_DIR/hostsync"
|
||||||
|
|
||||||
|
# 下载文件
|
||||||
|
if ! download_file "$download_url" "$output_file"; then
|
||||||
|
error "❌ 下载失败,请检查网络连接或手动下载"
|
||||||
|
info "🌐 手动下载地址: https://git.xykqyy.com/ljp/hostSync/releases"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 设置执行权限
|
||||||
|
chmod +x "$output_file" || {
|
||||||
|
error "❌ 设置执行权限失败"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
success "✅ HostSync 下载完成!"
|
||||||
|
|
||||||
|
# 配置环境
|
||||||
|
setup_environment
|
||||||
|
|
||||||
|
# 运行初始化
|
||||||
|
if [[ "$NO_INIT" != true ]]; then
|
||||||
|
info "🚀 开始初始化 HostSync..."
|
||||||
|
|
||||||
|
if "$output_file" init; then
|
||||||
|
success "✅ 初始化完成!"
|
||||||
|
else
|
||||||
|
warning "⚠️ 初始化可能遇到问题"
|
||||||
|
info "💡 你可以稍后手动运行: sudo hostsync init"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 显示完成信息
|
||||||
|
success "
|
||||||
|
🎉 HostSync 安装完成!
|
||||||
|
=====================
|
||||||
|
|
||||||
|
📁 安装位置: $output_file
|
||||||
|
📖 配置目录: $HOME/.hostsync/
|
||||||
|
|
||||||
|
🚀 快速开始:"
|
||||||
|
|
||||||
|
info " hostsync list # 查看配置"
|
||||||
|
info " hostsync add test example.com # 添加域名"
|
||||||
|
|
||||||
|
cat <<'EOF'
|
||||||
|
hostsync update # 更新解析
|
||||||
|
hostsync service status # 查看服务状态
|
||||||
|
|
||||||
|
📚 更多帮助:
|
||||||
|
hostsync help # 查看帮助
|
||||||
|
hostsync version # 查看版本
|
||||||
|
|
||||||
|
🌐 项目地址: https://git.xykqyy.com/ljp/hostSync
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# 错误处理
|
||||||
|
trap 'error "❌ 安装过程中发生错误"; exit 1' ERR
|
||||||
|
|
||||||
|
# 执行安装
|
||||||
|
install_hostsync
|
56
main.go
Normal file
56
main.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/cmd"
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 只在版本命令时显示banner
|
||||||
|
if isVersionCommand() {
|
||||||
|
showBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
config.Init()
|
||||||
|
// 初始化日志系统
|
||||||
|
if err := utils.InitLogger(); err != nil {
|
||||||
|
// 日志系统初始化失败时,直接输出到stderr,不使用日志系统
|
||||||
|
fmt.Fprintf(os.Stderr, "警告: 初始化日志系统失败: %v\n", err)
|
||||||
|
}
|
||||||
|
defer utils.CloseLogger()
|
||||||
|
// 记录用户完整的命令输入(排除程序名,只记录有意义的命令)
|
||||||
|
if len(os.Args) > 1 && !isVersionCommand() {
|
||||||
|
commandLine := strings.Join(os.Args[1:], " ")
|
||||||
|
utils.LogFileOnly("用户执行: %s", commandLine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
cmd.Execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isVersionCommand 检查是否是版本命令
|
||||||
|
func isVersionCommand() bool {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
arg := os.Args[1]
|
||||||
|
return arg == "version" || arg == "--version" || arg == "-v"
|
||||||
|
}
|
||||||
|
|
||||||
|
func showBanner() {
|
||||||
|
banner := `
|
||||||
|
_ _
|
||||||
|
| |__ ___ ___| |_ ___ _ _ _ __ ___
|
||||||
|
| '_ \ / _ \/ __| __/ __| | | | '_ \ / __|
|
||||||
|
| | | | (_) \__ \ |_\__ \ |_| | | | | (__
|
||||||
|
|_| |_|\___/|___/\__|___/\__, |_| |_|\___|
|
||||||
|
by evil7@deepwn |___/
|
||||||
|
`
|
||||||
|
fmt.Println(banner)
|
||||||
|
}
|
248
readme.md
Normal file
248
readme.md
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
# HostSync
|
||||||
|
|
||||||
|
强大的命令行 Hosts 文件管理工具,支持分块管理、智能 DNS 解析和定时自动更新。
|
||||||
|
|
||||||
|
## ✨ 功能特点
|
||||||
|
|
||||||
|
- 🎯 **分块管理** - 按名称组织 Hosts 配置,支持独立启用/禁用
|
||||||
|
- 🌐 **智能解析** - 支持 DNS、DoH、预设服务器多种解析方式
|
||||||
|
- 🔄 **自动更新** - 一键更新域名 IP,支持定时任务
|
||||||
|
- 🔧 **服务模式** - 后台服务运行,跨平台支持
|
||||||
|
- 📦 **安全可靠** - 自动备份,权限管理,格式化清理
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 🎯 一键安装(推荐)
|
||||||
|
|
||||||
|
**Windows (PowerShell):**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iex (irm "https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.ps1")
|
||||||
|
```
|
||||||
|
|
||||||
|
**Linux/macOS:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.xykqyy.com/ljp/hostSync/raw/branch/main/install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
|
> 一键安装会自动下载最新版本、配置环境变量并完成初始化。支持多种参数,详见 [安装指南](INSTALL.md)。
|
||||||
|
|
||||||
|
### 📦 手动安装
|
||||||
|
|
||||||
|
1. **下载程序**
|
||||||
|
|
||||||
|
从 [Releases](./releases) 下载对应平台的可执行文件。
|
||||||
|
|
||||||
|
2. **初始化系统**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows (管理员权限)
|
||||||
|
hostsync init
|
||||||
|
|
||||||
|
# Linux/macOS (sudo权限)
|
||||||
|
sudo hostsync init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 基本使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看配置
|
||||||
|
hostsync list
|
||||||
|
|
||||||
|
# 添加域名块
|
||||||
|
hostsync add github github.com
|
||||||
|
|
||||||
|
# 启用/禁用块
|
||||||
|
hostsync enable github
|
||||||
|
hostsync disable github
|
||||||
|
|
||||||
|
# 更新域名解析
|
||||||
|
hostsync update github
|
||||||
|
|
||||||
|
# 设置定时任务
|
||||||
|
hostsync cron github "0 0 2 * * *" # 每天凌晨2点更新
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📋 命令参考
|
||||||
|
|
||||||
|
### 基础管理
|
||||||
|
|
||||||
|
| 命令 | 说明 | 示例 |
|
||||||
|
| --------------------------- | ----------- | ---------------------------------- |
|
||||||
|
| `list [block]` | 查看配置 | `hostsync list --raw` |
|
||||||
|
| `add <block> <domain> [ip]` | 添加记录 | `hostsync add dev api.test.com` |
|
||||||
|
| `remove <block> [domain]` | 删除记录/块 | `hostsync remove dev api.test.com` |
|
||||||
|
| `enable <block>` | 启用块 | `hostsync enable dev` |
|
||||||
|
| `disable <block>` | 禁用块 | `hostsync disable dev` |
|
||||||
|
|
||||||
|
### DNS 解析与更新
|
||||||
|
|
||||||
|
| 命令 | 说明 | 示例 |
|
||||||
|
| ---------------- | ------------- | ------------------------------------------------- |
|
||||||
|
| `update [block]` | 更新解析 | `hostsync update --dns 1.1.1.1` |
|
||||||
|
| `--dns <server>` | 指定 DNS | `hostsync update --dns 8.8.8.8` |
|
||||||
|
| `--doh <url>` | 使用 DoH | `hostsync update --doh https://1.1.1.1/dns-query` |
|
||||||
|
| `--srv <name>` | 预设服务器 | `hostsync update --srv Cloudflare` |
|
||||||
|
| `--save` | 保存 DNS 配置 | `hostsync update github --save` |
|
||||||
|
|
||||||
|
### 定时任务
|
||||||
|
|
||||||
|
| 命令 | 说明 | 示例 |
|
||||||
|
| --------------------- | ------------ | ------------------------------------ |
|
||||||
|
| `cron [block] [expr]` | 管理定时任务 | `hostsync cron github "0 0 2 * * *"` |
|
||||||
|
| `cron list` | 列出任务 | `hostsync cron list` |
|
||||||
|
|
||||||
|
**常用 cron 表达式 (6 字段格式: 秒 分 时 日 月 星期):**
|
||||||
|
|
||||||
|
- `0 0 0 * * *` - 每天午夜
|
||||||
|
- `0 0 */4 * * *` - 每 4 小时
|
||||||
|
- `0 0 9 * * 1` - 每周一上午 9 点
|
||||||
|
- `*/30 * * * * *` - 每 30 秒
|
||||||
|
- `0 */10 * * * *` - 每 10 分钟
|
||||||
|
|
||||||
|
### 系统服务
|
||||||
|
|
||||||
|
| 命令 | 说明 | 示例 |
|
||||||
|
| -------------------- | -------- | -------------------------- |
|
||||||
|
| `service install` | 安装服务 | `hostsync service install` |
|
||||||
|
| `service start/stop` | 启停服务 | `hostsync service start` |
|
||||||
|
| `service status` | 查看状态 | `hostsync service status` |
|
||||||
|
|
||||||
|
### 其他工具
|
||||||
|
|
||||||
|
| 命令 | 说明 | 示例 |
|
||||||
|
| -------------------- | -------- | --------------------------------- |
|
||||||
|
| `format [block]` | 格式化 | `hostsync format` |
|
||||||
|
| `server test [name]` | 测试 DNS | `hostsync server test Cloudflare` |
|
||||||
|
| `version` | 版本信息 | `hostsync version` |
|
||||||
|
|
||||||
|
## ⚙️ 配置说明
|
||||||
|
|
||||||
|
### 块格式示例
|
||||||
|
|
||||||
|
```text
|
||||||
|
# github:
|
||||||
|
140.82.114.3 github.com
|
||||||
|
185.199.108.133 avatars.githubusercontent.com
|
||||||
|
# useSrv: Cloudflare
|
||||||
|
# cronJob: 0 0 2 * * *
|
||||||
|
# updateAt: 2024-01-15 10:00:00
|
||||||
|
# github;
|
||||||
|
```
|
||||||
|
|
||||||
|
### DNS 配置选项
|
||||||
|
|
||||||
|
```text
|
||||||
|
# useDns: 1.1.1.1 # 直接DNS
|
||||||
|
# useDoh: https://1.1.1.1/dns-query # DoH服务器
|
||||||
|
# useSrv: Cloudflare # 预设服务器
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 使用场景
|
||||||
|
|
||||||
|
### 开发环境管理
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建开发环境
|
||||||
|
hostsync add dev api.example.com 192.168.1.100
|
||||||
|
hostsync add dev web.example.com 192.168.1.101
|
||||||
|
hostsync enable dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### GitHub 加速
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 添加GitHub相关域名
|
||||||
|
hostsync add github github.com
|
||||||
|
hostsync add github api.github.com
|
||||||
|
hostsync update github --srv Cloudflare
|
||||||
|
hostsync cron github "0 0 2 * * *" # 每天更新
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多环境切换
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 生产环境
|
||||||
|
hostsync add prod api.myapp.com 1.2.3.4
|
||||||
|
|
||||||
|
# 测试环境
|
||||||
|
hostsync add test api.myapp.com 192.168.1.10
|
||||||
|
|
||||||
|
# 切换环境
|
||||||
|
hostsync disable prod && hostsync enable test
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚠️ 注意事项
|
||||||
|
|
||||||
|
### 权限要求
|
||||||
|
|
||||||
|
- **Windows**: 必须以管理员权限运行
|
||||||
|
- **Linux/macOS**: 需要 sudo 权限访问 `/etc/hosts`
|
||||||
|
|
||||||
|
### 系统支持
|
||||||
|
|
||||||
|
- Windows 7/8/10/11 (Windows Service)
|
||||||
|
- Linux (systemd)
|
||||||
|
- macOS (LaunchD)
|
||||||
|
|
||||||
|
### 安全说明
|
||||||
|
|
||||||
|
- 程序仅在本地运行,不收集用户数据
|
||||||
|
- 修改前自动备份 hosts 文件
|
||||||
|
- 支持配置文件热重载
|
||||||
|
|
||||||
|
## 🔧 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
1. **权限不足**: 确保以管理员/sudo 权限运行
|
||||||
|
2. **服务无法启动**: 检查系统服务支持 (systemd/LaunchD)
|
||||||
|
3. **DNS 解析失败**: 尝试更换 DNS 服务器 `hostsync server test`
|
||||||
|
4. **定时任务不执行**: 检查服务状态 `hostsync service status`
|
||||||
|
|
||||||
|
### 调试模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hostsync update --debug # 启用详细日志
|
||||||
|
```
|
||||||
|
|
||||||
|
### 重置配置
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hostsync init --force # 强制重新初始化
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📁 配置文件位置
|
||||||
|
|
||||||
|
- **Windows**: `%USERPROFILE%\.hostsync\`
|
||||||
|
- **Linux/macOS**: `~/.hostsync/`
|
||||||
|
|
||||||
|
包含:配置文件、日志文件、备份文件、服务器配置
|
||||||
|
|
||||||
|
## 🛠️ 开发构建
|
||||||
|
|
||||||
|
### 构建要求
|
||||||
|
|
||||||
|
- Go 1.19+
|
||||||
|
- Git
|
||||||
|
- UPX (可选,用于压缩)
|
||||||
|
- GoReleaser (可选,用于发布)
|
||||||
|
|
||||||
|
### 构建命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
.\build.ps1
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
./build.sh
|
||||||
|
|
||||||
|
# 手动构建
|
||||||
|
go build -o hostsync main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
92
servers.json
Normal file
92
servers.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"Name": "Cloudflare",
|
||||||
|
"Dns": "1.1.1.1",
|
||||||
|
"Doh": "https://1.1.1.1/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Google",
|
||||||
|
"Dns": "8.8.8.8",
|
||||||
|
"Doh": "https://8.8.4.4/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OneDNS",
|
||||||
|
"Dns": "117.50.10.10",
|
||||||
|
"Doh": "https://doh.onedns.net/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AdGuard",
|
||||||
|
"Dns": "94.140.14.14",
|
||||||
|
"Doh": "https://94.140.14.14/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Yandex",
|
||||||
|
"Dns": "77.88.8.8",
|
||||||
|
"Doh": "https://77.88.8.8/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "DNSPod",
|
||||||
|
"Dns": "119.29.29.29",
|
||||||
|
"Doh": "https://doh.pub/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "dns.sb",
|
||||||
|
"Dns": "185.222.222.222",
|
||||||
|
"Doh": "https://185.222.222.222/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Quad101",
|
||||||
|
"Dns": "101.101.101.101",
|
||||||
|
"Doh": "https://101.101.101.101/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Quad9",
|
||||||
|
"Dns": "9.9.9.9",
|
||||||
|
"Doh": "https://9.9.9.9/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OpenDNS",
|
||||||
|
"Dns": "208.67.222.222",
|
||||||
|
"Doh": "https://208.67.222.222/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "AliDNS",
|
||||||
|
"Dns": "223.5.5.5",
|
||||||
|
"Doh": "https://223.5.5.5/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Applied",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://doh.applied-privacy.net/query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "cira.ca",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://private.canadianshield.cira.ca/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "ControlD",
|
||||||
|
"Dns": "76.76.2.0",
|
||||||
|
"Doh": "https://dns.controld.com/p0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "switch.ch",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://dns.switch.ch/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Dnswarden",
|
||||||
|
"Dns": "",
|
||||||
|
"Doh": "https://dns.dnswarden.com/uncensored"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "OSZX",
|
||||||
|
"Dns": "217.160.156.119",
|
||||||
|
"Doh": "https://dns.oszx.co/dns-query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "Tiarap",
|
||||||
|
"Dns": "174.138.21.128",
|
||||||
|
"Doh": "https://doh.tiar.app/dns-query"
|
||||||
|
}
|
||||||
|
]
|
248
service/darwin.go
Normal file
248
service/darwin.go
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServiceManager 创建macOS服务管理器实例
|
||||||
|
func NewServiceManager() ServiceManager {
|
||||||
|
return &DarwinServiceManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DarwinServiceManager macOS LaunchD服务管理器
|
||||||
|
type DarwinServiceManager struct{}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Install(execPath string) error {
|
||||||
|
// 检查是否有足够权限操作系统服务
|
||||||
|
if err := m.checkPermissions(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureLogDir(); err != nil {
|
||||||
|
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plistContent := m.generateLaunchDPlist(execPath)
|
||||||
|
|
||||||
|
// 获取plist文件路径
|
||||||
|
plistPath, err := m.getPlistPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取plist路径失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建目录
|
||||||
|
if err := os.MkdirAll(filepath.Dir(plistPath), 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建plist目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入plist文件
|
||||||
|
if err := os.WriteFile(plistPath, []byte(plistContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("写入plist文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载服务
|
||||||
|
cmd := exec.Command("launchctl", "load", plistPath)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("加载服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Uninstall() error {
|
||||||
|
|
||||||
|
// 停止服务
|
||||||
|
m.Stop()
|
||||||
|
|
||||||
|
// 获取plist文件路径
|
||||||
|
plistPath, err := m.getPlistPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取plist路径失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卸载服务
|
||||||
|
cmd := exec.Command("launchctl", "unload", plistPath)
|
||||||
|
cmd.CombinedOutput() // 忽略错误
|
||||||
|
|
||||||
|
// 删除plist文件
|
||||||
|
if err := os.Remove(plistPath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("删除plist文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Start() error {
|
||||||
|
serviceName := m.getServiceIdentifier()
|
||||||
|
|
||||||
|
cmd := exec.Command("launchctl", "start", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("启动服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Stop() error {
|
||||||
|
serviceName := m.getServiceIdentifier()
|
||||||
|
|
||||||
|
cmd := exec.Command("launchctl", "stop", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("停止服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Restart() error {
|
||||||
|
if err := m.Stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) Status() (ServiceStatus, error) {
|
||||||
|
serviceName := m.getServiceIdentifier()
|
||||||
|
|
||||||
|
// 检查plist文件是否存在
|
||||||
|
plistPath, err := m.getPlistPath()
|
||||||
|
if err != nil {
|
||||||
|
return StatusUnknown, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(plistPath); os.IsNotExist(err) {
|
||||||
|
return StatusNotInstalled, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("launchctl", "list", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
// 如果服务不在列表中,可能是未运行
|
||||||
|
return StatusStopped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单检查输出中是否包含PID
|
||||||
|
outputStr := string(output)
|
||||||
|
if strings.Contains(outputStr, serviceName) && !strings.Contains(outputStr, "-") {
|
||||||
|
return StatusRunning, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusStopped, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) getServiceIdentifier() string {
|
||||||
|
return fmt.Sprintf("com.deepwn.%s", strings.ToLower(getServiceName()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) getPlistPath() (string, error) {
|
||||||
|
serviceName := m.getServiceIdentifier()
|
||||||
|
|
||||||
|
// 检查是否以root权限运行
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentUser.Uid == "0" {
|
||||||
|
// 系统级服务
|
||||||
|
return fmt.Sprintf("/Library/LaunchDaemons/%s.plist", serviceName), nil
|
||||||
|
} else {
|
||||||
|
// 用户级服务
|
||||||
|
homeDir, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", homeDir, serviceName), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *DarwinServiceManager) generateLaunchDPlist(execPath string) string {
|
||||||
|
serviceName := m.getServiceIdentifier()
|
||||||
|
logPath := getLogPath()
|
||||||
|
|
||||||
|
// 获取用户配置目录
|
||||||
|
userConfigDir, err := getUserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
// 如果获取失败,使用默认路径
|
||||||
|
currentUser, _ := user.Current()
|
||||||
|
if currentUser != nil {
|
||||||
|
userConfigDir = filepath.Join(currentUser.HomeDir, ".hostsync")
|
||||||
|
} else {
|
||||||
|
userConfigDir = "/Users/Shared/.hostsync"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>%s</string>
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>%s</string>
|
||||||
|
<string>service</string>
|
||||||
|
<string>run</string>
|
||||||
|
<string>--config</string>
|
||||||
|
<string>%s</string>
|
||||||
|
</array>
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>%s</string>
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>%s</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
`, serviceName, execPath, userConfigDir, logPath, logPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPermissions 检查是否有足够权限操作系统服务
|
||||||
|
func (m *DarwinServiceManager) checkPermissions() error {
|
||||||
|
// 检查是否为 root 用户
|
||||||
|
if os.Getuid() != 0 {
|
||||||
|
return fmt.Errorf("安装系统服务需要 root 权限,请使用 sudo 运行")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 launchctl 命令是否可用
|
||||||
|
if _, err := exec.LookPath("launchctl"); err != nil {
|
||||||
|
return fmt.Errorf("launchctl 命令不可用: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取 plist 文件路径并检查目录权限
|
||||||
|
plistPath, err := m.getPlistPath()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取 plist 路径失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plistDir := filepath.Dir(plistPath)
|
||||||
|
// 检查目录是否存在,如果不存在尝试创建来测试权限
|
||||||
|
if _, err := os.Stat(plistDir); os.IsNotExist(err) {
|
||||||
|
if err := os.MkdirAll(plistDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("无法创建 plist 目录 %s: %v", plistDir, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试创建一个测试文件来验证写权限
|
||||||
|
testFile := filepath.Join(plistDir, ".hostsync-permission-test")
|
||||||
|
if file, err := os.Create(testFile); err != nil {
|
||||||
|
return fmt.Errorf("无法写入 plist 目录 %s: %v", plistDir, err)
|
||||||
|
} else {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(testFile) // 清理测试文件
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
289
service/linux.go
Normal file
289
service/linux.go
Normal file
@ -0,0 +1,289 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServiceManager 创建服务管理器 (Linux)
|
||||||
|
func NewServiceManager() ServiceManager {
|
||||||
|
return &LinuxServiceManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinuxServiceManager Linux systemd服务管理器
|
||||||
|
type LinuxServiceManager struct{}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Install(execPath string) error {
|
||||||
|
// 检查是否有足够权限操作系统服务
|
||||||
|
if err := m.checkPermissions(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ensureLogDir(); err != nil {
|
||||||
|
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
serviceContent := m.generateSystemdUnit(execPath)
|
||||||
|
// 写入systemd服务文件
|
||||||
|
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", strings.ToLower(serviceName))
|
||||||
|
if err := os.WriteFile(servicePath, []byte(serviceContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("写入服务文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理 SELinux 上下文,确保可执行文件可以被 systemd 执行
|
||||||
|
if err := m.handleSELinuxContext(execPath); err != nil {
|
||||||
|
// SELinux 处理失败不算致命错误,只记录警告
|
||||||
|
fmt.Printf("警告: SELinux 上下文设置失败: %v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载systemd配置
|
||||||
|
cmd := exec.Command("systemctl", "daemon-reload")
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("重新加载systemd配置失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用服务自动启动
|
||||||
|
cmd = exec.Command("systemctl", "enable", strings.ToLower(serviceName))
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("启用服务自动启动失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Uninstall() error {
|
||||||
|
serviceName := strings.ToLower(getServiceName())
|
||||||
|
|
||||||
|
// 停止服务
|
||||||
|
m.Stop()
|
||||||
|
|
||||||
|
// 禁用服务
|
||||||
|
cmd := exec.Command("systemctl", "disable", serviceName)
|
||||||
|
cmd.CombinedOutput() // 忽略错误
|
||||||
|
|
||||||
|
// 删除服务文件
|
||||||
|
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", serviceName)
|
||||||
|
if err := os.Remove(servicePath); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("删除服务文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新加载systemd配置
|
||||||
|
cmd = exec.Command("systemctl", "daemon-reload")
|
||||||
|
cmd.CombinedOutput() // 忽略错误
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Start() error {
|
||||||
|
serviceName := strings.ToLower(getServiceName())
|
||||||
|
|
||||||
|
cmd := exec.Command("systemctl", "start", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("启动服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Stop() error {
|
||||||
|
serviceName := strings.ToLower(getServiceName())
|
||||||
|
|
||||||
|
cmd := exec.Command("systemctl", "stop", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("停止服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Restart() error {
|
||||||
|
serviceName := strings.ToLower(getServiceName())
|
||||||
|
|
||||||
|
cmd := exec.Command("systemctl", "restart", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("重启服务失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) Status() (ServiceStatus, error) {
|
||||||
|
serviceName := strings.ToLower(getServiceName())
|
||||||
|
|
||||||
|
// 检查服务是否存在
|
||||||
|
servicePath := fmt.Sprintf("/etc/systemd/system/%s.service", serviceName)
|
||||||
|
if _, err := os.Stat(servicePath); os.IsNotExist(err) {
|
||||||
|
return StatusNotInstalled, nil
|
||||||
|
}
|
||||||
|
cmd := exec.Command("systemctl", "is-active", serviceName)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
outputStr := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
// 处理不同的退出状态
|
||||||
|
if err != nil {
|
||||||
|
if exitError, ok := err.(*exec.ExitError); ok {
|
||||||
|
// systemctl is-active 的退出码含义:
|
||||||
|
// 0: active, 1: inactive, 3: activating/deactivating
|
||||||
|
switch exitError.ExitCode() {
|
||||||
|
case 1:
|
||||||
|
// 服务未运行
|
||||||
|
switch outputStr {
|
||||||
|
case "inactive":
|
||||||
|
return StatusStopped, nil
|
||||||
|
case "failed":
|
||||||
|
return StatusStopped, nil
|
||||||
|
default:
|
||||||
|
return StatusStopped, nil
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
// 服务正在启动或停止中
|
||||||
|
switch outputStr {
|
||||||
|
case "activating":
|
||||||
|
return StatusRunning, nil // 启动中视为运行状态
|
||||||
|
case "deactivating":
|
||||||
|
return StatusStopped, nil // 停止中视为停止状态
|
||||||
|
default:
|
||||||
|
return StatusStopped, nil // 其他转换状态默认为停止
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if strings.Contains(outputStr, "could not be found") {
|
||||||
|
return StatusNotInstalled, nil
|
||||||
|
}
|
||||||
|
return StatusUnknown, fmt.Errorf("获取服务状态失败: %v, 输出: %s", err, outputStr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return StatusUnknown, fmt.Errorf("执行systemctl命令失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没有错误的情况下,服务应该是active状态
|
||||||
|
switch outputStr {
|
||||||
|
case "active":
|
||||||
|
return StatusRunning, nil
|
||||||
|
case "activating":
|
||||||
|
return StatusRunning, nil // 正在启动中,视为运行状态
|
||||||
|
case "inactive":
|
||||||
|
return StatusStopped, nil
|
||||||
|
case "failed":
|
||||||
|
return StatusStopped, nil
|
||||||
|
case "deactivating":
|
||||||
|
return StatusStopped, nil // 正在停止中,视为停止状态
|
||||||
|
default:
|
||||||
|
return StatusUnknown, fmt.Errorf("未知的服务状态: %s", outputStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LinuxServiceManager) generateSystemdUnit(execPath string) string {
|
||||||
|
description := getServiceDescription()
|
||||||
|
|
||||||
|
// 获取用户配置目录
|
||||||
|
userConfigDir, err := getUserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
// 如果获取失败,使用默认路径
|
||||||
|
userConfigDir = "/root/.hostsync"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`[Unit]
|
||||||
|
Description=%s
|
||||||
|
After=network.target network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%s service run -c %s
|
||||||
|
Restart=always
|
||||||
|
RestartSec=10
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
StandardOutput=journal
|
||||||
|
StandardError=journal
|
||||||
|
SyslogIdentifier=hostsync
|
||||||
|
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
`, description, execPath, userConfigDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPermissions 检查是否有足够权限操作系统服务
|
||||||
|
func (m *LinuxServiceManager) checkPermissions() error {
|
||||||
|
// 检查是否为 root 用户
|
||||||
|
if os.Getuid() != 0 {
|
||||||
|
return fmt.Errorf("安装系统服务需要 root 权限,请使用 sudo 运行")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 /etc/systemd/system 目录是否可写
|
||||||
|
testPath := "/etc/systemd/system"
|
||||||
|
if _, err := os.Stat(testPath); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("systemd 系统目录不存在: %s", testPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试创建一个测试文件来验证写权限
|
||||||
|
testFile := fmt.Sprintf("%s/.hostsync-permission-test", testPath)
|
||||||
|
if file, err := os.Create(testFile); err != nil {
|
||||||
|
return fmt.Errorf("无法写入系统服务目录 %s: %v", testPath, err)
|
||||||
|
} else {
|
||||||
|
file.Close()
|
||||||
|
os.Remove(testFile) // 清理测试文件
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 systemctl 命令是否可用
|
||||||
|
if _, err := exec.LookPath("systemctl"); err != nil {
|
||||||
|
return fmt.Errorf("系统未安装 systemd 或 systemctl 命令不可用: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSELinuxContext 处理 SELinux 上下文设置
|
||||||
|
func (m *LinuxServiceManager) handleSELinuxContext(execPath string) error {
|
||||||
|
// 检查是否启用了 SELinux
|
||||||
|
if !m.isSELinuxEnabled() {
|
||||||
|
return nil // SELinux 未启用,无需处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 chcon 命令是否可用
|
||||||
|
if _, err := exec.LookPath("chcon"); err != nil {
|
||||||
|
return fmt.Errorf("chcon 命令不可用: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为可执行文件设置正确的 SELinux 上下文
|
||||||
|
// bin_t 类型允许程序被 systemd 执行
|
||||||
|
cmd := exec.Command("chcon", "-t", "bin_t", execPath)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("设置 SELinux 上下文失败: %v, 输出: %s", err, string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSELinuxEnabled 检查 SELinux 是否启用
|
||||||
|
func (m *LinuxServiceManager) isSELinuxEnabled() bool {
|
||||||
|
// 检查 /selinux/enforce 文件是否存在
|
||||||
|
if _, err := os.Stat("/selinux/enforce"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 /sys/fs/selinux/enforce 文件是否存在
|
||||||
|
if _, err := os.Stat("/sys/fs/selinux/enforce"); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试运行 getenforce 命令
|
||||||
|
if _, err := exec.LookPath("getenforce"); err == nil {
|
||||||
|
cmd := exec.Command("getenforce")
|
||||||
|
if output, err := cmd.CombinedOutput(); err == nil {
|
||||||
|
status := strings.TrimSpace(string(output))
|
||||||
|
return status == "Enforcing" || status == "Permissive"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
93
service/service.go
Normal file
93
service/service.go
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceStatus 服务状态
|
||||||
|
type ServiceStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusUnknown ServiceStatus = iota
|
||||||
|
StatusRunning
|
||||||
|
StatusStopped
|
||||||
|
StatusNotInstalled
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceManager 服务管理器接口
|
||||||
|
type ServiceManager interface {
|
||||||
|
Install(execPath string) error
|
||||||
|
Uninstall() error
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
Restart() error
|
||||||
|
Status() (ServiceStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenericServiceManager 通用服务管理器(不支持系统服务)
|
||||||
|
type GenericServiceManager struct{}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Install(execPath string) error {
|
||||||
|
return fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Uninstall() error {
|
||||||
|
return fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Start() error {
|
||||||
|
return fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Stop() error {
|
||||||
|
return fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Restart() error {
|
||||||
|
return fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *GenericServiceManager) Status() (ServiceStatus, error) {
|
||||||
|
return StatusNotInstalled, fmt.Errorf("当前操作系统不支持系统服务")
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceName 获取服务名称
|
||||||
|
func getServiceName() string {
|
||||||
|
return "HostSync"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceDisplayName 获取服务显示名称
|
||||||
|
func getServiceDisplayName() string {
|
||||||
|
return "HostSync - Hosts File Manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getServiceDescription 获取服务描述
|
||||||
|
func getServiceDescription() string {
|
||||||
|
return "HostSync hosts file management service with automatic updates and cron jobs"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLogPath 获取日志文件路径
|
||||||
|
func getLogPath() string {
|
||||||
|
execPath, _ := os.Executable()
|
||||||
|
execDir := filepath.Dir(execPath)
|
||||||
|
return filepath.Join(execDir, "logs", "service.log")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureLogDir 确保日志目录存在
|
||||||
|
func ensureLogDir() error {
|
||||||
|
logPath := getLogPath()
|
||||||
|
logDir := filepath.Dir(logPath)
|
||||||
|
return os.MkdirAll(logDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserConfigDir 获取用户配置目录
|
||||||
|
func getUserConfigDir() (string, error) {
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return filepath.Join(currentUser.HomeDir, ".hostsync"), nil
|
||||||
|
}
|
322
service/windows.go
Normal file
322
service/windows.go
Normal file
@ -0,0 +1,322 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"github.com/evil7/hostsync/core"
|
||||||
|
"github.com/evil7/hostsync/utils"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"golang.org/x/sys/windows/svc"
|
||||||
|
"golang.org/x/sys/windows/svc/mgr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewServiceManager 创建服务管理器 (Windows)
|
||||||
|
func NewServiceManager() ServiceManager {
|
||||||
|
return &WindowsServiceManager{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowsServiceManager Windows服务管理器
|
||||||
|
type WindowsServiceManager struct{}
|
||||||
|
|
||||||
|
// WindowsService 实现 Windows 服务接口
|
||||||
|
type WindowsService struct {
|
||||||
|
stopChan chan struct{}
|
||||||
|
cronManager *core.CronManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WindowsService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||||
|
const cmdsAccepted = svc.AcceptStop | svc.AcceptShutdown | svc.AcceptPauseAndContinue
|
||||||
|
|
||||||
|
changes <- svc.Status{State: svc.StartPending}
|
||||||
|
// 启动服务逻辑
|
||||||
|
if err := ws.startServiceLogic(); err != nil {
|
||||||
|
utils.LogError("启动服务逻辑失败: %v", err)
|
||||||
|
return true, 1
|
||||||
|
}
|
||||||
|
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
for c := range r {
|
||||||
|
switch c.Cmd {
|
||||||
|
case svc.Interrogate:
|
||||||
|
changes <- c.CurrentStatus
|
||||||
|
case svc.Stop, svc.Shutdown:
|
||||||
|
changes <- svc.Status{State: svc.StopPending}
|
||||||
|
ws.stopServiceLogic()
|
||||||
|
return false, 0
|
||||||
|
case svc.Pause:
|
||||||
|
changes <- svc.Status{State: svc.Paused, Accepts: cmdsAccepted}
|
||||||
|
case svc.Continue:
|
||||||
|
changes <- svc.Status{State: svc.Running, Accepts: cmdsAccepted}
|
||||||
|
default:
|
||||||
|
utils.LogWarning("unexpected control request #%d", c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WindowsService) startServiceLogic() error {
|
||||||
|
utils.LogInfo("启动 HostSync 定时任务服务...")
|
||||||
|
|
||||||
|
// 初始化配置
|
||||||
|
config.Init()
|
||||||
|
|
||||||
|
// 创建定时任务管理器
|
||||||
|
ws.cronManager = core.NewCronManager()
|
||||||
|
ws.cronManager.Start()
|
||||||
|
|
||||||
|
// 加载hosts文件中的定时任务
|
||||||
|
hostsManager := core.NewHostsManager()
|
||||||
|
if err := hostsManager.Load(); err != nil {
|
||||||
|
return fmt.Errorf("加载hosts文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ws.cronManager.LoadFromHosts(hostsManager); err != nil {
|
||||||
|
return fmt.Errorf("加载定时任务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.LogSuccess("HostSync 定时任务服务已启动,共加载 %d 个任务", len(ws.cronManager.ListJobs()))
|
||||||
|
|
||||||
|
// 启动状态监控
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ws.stopChan:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
utils.LogDebug("HostSync 定时任务服务运行正常")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WindowsService) stopServiceLogic() {
|
||||||
|
utils.LogInfo("正在停止 HostSync 定时任务服务...")
|
||||||
|
|
||||||
|
if ws.cronManager != nil {
|
||||||
|
ws.cronManager.Stop()
|
||||||
|
utils.LogInfo("定时任务管理器已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
close(ws.stopChan)
|
||||||
|
utils.LogSuccess("HostSync 定时任务服务已停止")
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunAsWindowsService 作为 Windows 服务运行
|
||||||
|
func RunAsWindowsService() error {
|
||||||
|
service := &WindowsService{
|
||||||
|
stopChan: make(chan struct{}),
|
||||||
|
}
|
||||||
|
return svc.Run(getServiceName(), service)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Install(execPath string) error {
|
||||||
|
if err := ensureLogDir(); err != nil {
|
||||||
|
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
manager, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接服务管理器失败: %v", err)
|
||||||
|
}
|
||||||
|
defer manager.Disconnect()
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
displayName := getServiceDisplayName()
|
||||||
|
description := getServiceDescription()
|
||||||
|
|
||||||
|
// 检查服务是否已存在
|
||||||
|
service, err := manager.OpenService(serviceName)
|
||||||
|
if err == nil {
|
||||||
|
service.Close()
|
||||||
|
return fmt.Errorf("服务 %s 已存在", serviceName)
|
||||||
|
}
|
||||||
|
// 获取用户配置目录
|
||||||
|
userConfigDir, err := getUserConfigDir()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取用户配置目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务启动参数 - 包含配置目录参数以确保服务以系统权限运行时能找到配置文件
|
||||||
|
binPath := fmt.Sprintf(`"%s"`, execPath)
|
||||||
|
args := []string{"service", "run", "--config", userConfigDir}
|
||||||
|
config := mgr.Config{
|
||||||
|
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
||||||
|
StartType: windows.SERVICE_AUTO_START,
|
||||||
|
ErrorControl: windows.SERVICE_ERROR_NORMAL,
|
||||||
|
BinaryPathName: binPath,
|
||||||
|
DisplayName: displayName,
|
||||||
|
Description: description,
|
||||||
|
Dependencies: []string{"Tcpip", "Dnscache"},
|
||||||
|
}
|
||||||
|
|
||||||
|
service, err = manager.CreateService(serviceName, execPath, config, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建服务失败: %v", err)
|
||||||
|
}
|
||||||
|
defer service.Close() // 设置服务故障恢复策略
|
||||||
|
err = service.SetRecoveryActions([]mgr.RecoveryAction{
|
||||||
|
{Type: windows.SC_ACTION_RESTART, Delay: 5 * time.Second},
|
||||||
|
{Type: windows.SC_ACTION_RESTART, Delay: 5 * time.Second},
|
||||||
|
{Type: windows.SC_ACTION_RESTART, Delay: 5 * time.Second}}, uint32((24 * time.Hour).Seconds()))
|
||||||
|
if err != nil {
|
||||||
|
utils.LogWarning("设置服务恢复策略失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Uninstall() error {
|
||||||
|
manager, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接服务管理器失败: %v", err)
|
||||||
|
}
|
||||||
|
defer manager.Disconnect()
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
service, err := manager.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
// 服务不存在,认为卸载成功
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
|
||||||
|
// 先停止服务
|
||||||
|
status, err := service.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询服务状态失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.State != svc.Stopped {
|
||||||
|
_, err = service.Control(svc.Stop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("停止服务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待服务停止
|
||||||
|
timeout := time.Now().Add(30 * time.Second)
|
||||||
|
for status.State != svc.Stopped {
|
||||||
|
if time.Now().After(timeout) {
|
||||||
|
return fmt.Errorf("等待服务停止超时")
|
||||||
|
}
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
status, err = service.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询服务状态失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除服务
|
||||||
|
err = service.Delete()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("删除服务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Start() error {
|
||||||
|
manager, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接服务管理器失败: %v", err)
|
||||||
|
}
|
||||||
|
defer manager.Disconnect()
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
service, err := manager.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开服务失败: %v", err)
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
|
||||||
|
err = service.Start()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("启动服务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Stop() error {
|
||||||
|
manager, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("连接服务管理器失败: %v", err)
|
||||||
|
}
|
||||||
|
defer manager.Disconnect()
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
service, err := manager.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("打开服务失败: %v", err)
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
|
||||||
|
status, err := service.Query()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询服务状态失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.State == svc.Stopped {
|
||||||
|
return nil // 服务已经停止
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = service.Control(svc.Stop)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("停止服务失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Restart() error {
|
||||||
|
if err := m.Stop(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待一下再启动
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
return m.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *WindowsServiceManager) Status() (ServiceStatus, error) {
|
||||||
|
manager, err := mgr.Connect()
|
||||||
|
if err != nil {
|
||||||
|
return StatusUnknown, fmt.Errorf("连接服务管理器失败: %v", err)
|
||||||
|
}
|
||||||
|
defer manager.Disconnect()
|
||||||
|
|
||||||
|
serviceName := getServiceName()
|
||||||
|
service, err := manager.OpenService(serviceName)
|
||||||
|
if err != nil {
|
||||||
|
return StatusNotInstalled, nil
|
||||||
|
}
|
||||||
|
defer service.Close()
|
||||||
|
|
||||||
|
status, err := service.Query()
|
||||||
|
if err != nil {
|
||||||
|
return StatusUnknown, fmt.Errorf("查询服务状态失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch status.State {
|
||||||
|
case svc.Running:
|
||||||
|
return StatusRunning, nil
|
||||||
|
case svc.Stopped:
|
||||||
|
return StatusStopped, nil
|
||||||
|
case svc.StartPending, svc.ContinuePending:
|
||||||
|
return StatusRunning, nil
|
||||||
|
case svc.StopPending, svc.PausePending, svc.Paused:
|
||||||
|
return StatusStopped, nil
|
||||||
|
default:
|
||||||
|
return StatusUnknown, nil
|
||||||
|
}
|
||||||
|
}
|
57
test_hosts
Normal file
57
test_hosts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# custom:
|
||||||
|
127.0.0.1 localhost
|
||||||
|
192.168.15.5 dev.server
|
||||||
|
192.168.15.99 bi.server
|
||||||
|
192.168.15.250 web.server
|
||||||
|
# custom;
|
||||||
|
|
||||||
|
# gfont:
|
||||||
|
120.253.253.34 fonts.gstatic.com
|
||||||
|
120.253.253.98 fonts.googleapis.com
|
||||||
|
# useDns: 1.1.1.1
|
||||||
|
# updateAt: 2025-04-25 09:17:15
|
||||||
|
# gfont;
|
||||||
|
|
||||||
|
# github:
|
||||||
|
140.82.113.25 alive.github.com
|
||||||
|
140.82.116.6 api.github.com
|
||||||
|
185.199.109.133 avatars.githubusercontent.com
|
||||||
|
185.199.108.133 avatars0.githubusercontent.com
|
||||||
|
185.199.111.133 avatars1.githubusercontent.com
|
||||||
|
185.199.108.133 avatars2.githubusercontent.com
|
||||||
|
185.199.108.133 avatars3.githubusercontent.com
|
||||||
|
185.199.108.133 avatars4.githubusercontent.com
|
||||||
|
185.199.111.133 avatars5.githubusercontent.com
|
||||||
|
185.199.110.133 camo.githubusercontent.com
|
||||||
|
140.82.114.21 central.github.com
|
||||||
|
185.199.111.133 cloud.githubusercontent.com
|
||||||
|
140.82.116.9 codeload.github.com
|
||||||
|
140.82.114.22 collector.github.com
|
||||||
|
185.199.108.133 desktop.githubusercontent.com
|
||||||
|
185.199.109.133 favicons.githubusercontent.com
|
||||||
|
78.16.49.15 gist.github.com
|
||||||
|
3.5.29.52 github-cloud.s3.amazonaws.com
|
||||||
|
3.5.17.32 github-com.s3.amazonaws.com
|
||||||
|
52.216.142.4 github-production-release-asset-2e65be.s3.amazonaws.com
|
||||||
|
3.5.27.137 github-production-repository-file-5c1aeb.s3.amazonaws.com
|
||||||
|
52.216.152.180 github-production-user-asset-6210df.s3.amazonaws.com
|
||||||
|
192.0.66.2 github.blog
|
||||||
|
140.82.116.3 github.com
|
||||||
|
140.82.113.17 github.community
|
||||||
|
185.199.111.154 github.githubassets.com
|
||||||
|
208.101.60.87 github.global.ssl.fastly.net
|
||||||
|
185.199.110.153 github.io
|
||||||
|
185.199.109.133 github.map.fastly.net
|
||||||
|
185.199.110.153 githubstatus.com
|
||||||
|
140.82.113.25 live.github.com
|
||||||
|
185.199.109.133 media.githubusercontent.com
|
||||||
|
185.199.111.133 objects.githubusercontent.com
|
||||||
|
13.107.42.16 pipelines.actions.githubusercontent.com
|
||||||
|
185.199.111.133 raw.githubusercontent.com
|
||||||
|
185.199.109.133 user-images.githubusercontent.com
|
||||||
|
140.82.114.21 education.github.com
|
||||||
|
185.199.110.133 private-user-images.githubusercontent.com
|
||||||
|
# useDns: 185.222.222.222
|
||||||
|
# cronJob: */30 * * * *
|
||||||
|
# updateAt: 2025-04-25 09:17:31
|
||||||
|
# github;
|
227
utils/logger.go
Normal file
227
utils/logger.go
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/evil7/hostsync/config"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fileLogger *zap.Logger
|
||||||
|
fileSugar *zap.SugaredLogger
|
||||||
|
consoleLogger *zap.Logger
|
||||||
|
consoleSugar *zap.SugaredLogger
|
||||||
|
debugMode bool
|
||||||
|
logMutex sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitLogger 初始化日志系统
|
||||||
|
func InitLogger() error {
|
||||||
|
if config.AppConfig == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据配置设置日志级别
|
||||||
|
level := zapcore.InfoLevel
|
||||||
|
if config.AppConfig.LogLevel != "" {
|
||||||
|
switch config.AppConfig.LogLevel {
|
||||||
|
case "debug":
|
||||||
|
level = zapcore.DebugLevel
|
||||||
|
case "info":
|
||||||
|
level = zapcore.InfoLevel
|
||||||
|
case "warning", "warn":
|
||||||
|
level = zapcore.WarnLevel
|
||||||
|
case "error":
|
||||||
|
level = zapcore.ErrorLevel
|
||||||
|
case "silent":
|
||||||
|
level = zapcore.ErrorLevel + 1 // 静默模式,不记录任何日志
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建控制台logger配置(只用于错误输出)
|
||||||
|
consoleConfig := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(zapcore.ErrorLevel), // 只显示错误
|
||||||
|
Development: false,
|
||||||
|
Encoding: "console",
|
||||||
|
EncoderConfig: zapcore.EncoderConfig{
|
||||||
|
MessageKey: "msg",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
},
|
||||||
|
OutputPaths: []string{"stderr"},
|
||||||
|
ErrorOutputPaths: []string{"stderr"},
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
consoleLogger, err = consoleConfig.Build(zap.AddStacktrace(zapcore.DPanicLevel))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建控制台logger失败: %v", err)
|
||||||
|
}
|
||||||
|
consoleSugar = consoleLogger.Sugar()
|
||||||
|
|
||||||
|
// 如果配置了文件路径,创建文件logger
|
||||||
|
if config.AppConfig.LogPath != "" {
|
||||||
|
// 确保日志目录存在
|
||||||
|
if err := os.MkdirAll(config.AppConfig.LogPath, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建日志目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logFileName := filepath.Join(config.AppConfig.LogPath, "hostsync.log")
|
||||||
|
|
||||||
|
// 检查并轮转日志文件
|
||||||
|
if err := rotateLogIfNeeded(logFileName); err != nil {
|
||||||
|
return fmt.Errorf("轮转日志文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建文件logger配置
|
||||||
|
fileConfig := zap.Config{
|
||||||
|
Level: zap.NewAtomicLevelAt(level),
|
||||||
|
Development: false,
|
||||||
|
Encoding: "console",
|
||||||
|
EncoderConfig: zapcore.EncoderConfig{
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
MessageKey: "msg",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder,
|
||||||
|
EncodeTime: func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
|
||||||
|
enc.AppendString(t.Format("2006-01-02 15:04:05"))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
OutputPaths: []string{logFileName},
|
||||||
|
ErrorOutputPaths: []string{logFileName},
|
||||||
|
}
|
||||||
|
|
||||||
|
fileLogger, err = fileConfig.Build(zap.AddStacktrace(zapcore.DPanicLevel))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("创建文件logger失败: %v", err)
|
||||||
|
}
|
||||||
|
fileSugar = fileLogger.Sugar()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotateLogIfNeeded 检查并轮转日志文件
|
||||||
|
func rotateLogIfNeeded(logFileName string) error {
|
||||||
|
info, err := os.Stat(logFileName)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件大小是否超过10MB
|
||||||
|
if info.Size() < 10*1024*1024 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮转日志文件
|
||||||
|
backupName := fmt.Sprintf("%s.%s", logFileName, time.Now().Format("20060102_150405"))
|
||||||
|
if err := os.Rename(logFileName, backupName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异步清理旧日志文件
|
||||||
|
go cleanOldLogs(filepath.Dir(logFileName))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanOldLogs 清理旧的日志备份文件,保留最近5个
|
||||||
|
func cleanOldLogs(logDir string) {
|
||||||
|
files, err := filepath.Glob(filepath.Join(logDir, "hostsync.log.*"))
|
||||||
|
if err != nil || len(files) <= 5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除最旧的文件
|
||||||
|
for i := 0; i < len(files)-5; i++ {
|
||||||
|
os.Remove(files[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetDebugMode 设置调试模式
|
||||||
|
func SetDebugMode(debug bool) {
|
||||||
|
debugMode = debug
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseLogger 关闭日志系统
|
||||||
|
func CloseLogger() {
|
||||||
|
if fileLogger != nil {
|
||||||
|
fileLogger.Sync()
|
||||||
|
}
|
||||||
|
if consoleLogger != nil {
|
||||||
|
consoleLogger.Sync()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogInfo 记录信息日志(只记录到文件)
|
||||||
|
func LogInfo(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Infof(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogError 记录错误日志(记录到文件和控制台)
|
||||||
|
func LogError(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
if consoleSugar != nil {
|
||||||
|
consoleSugar.Errorf(format, args...)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogDebug 记录调试日志(只记录到文件)
|
||||||
|
func LogDebug(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Debugf(format, args...)
|
||||||
|
} else if debugMode {
|
||||||
|
fmt.Printf("DEBUG: "+format+"\n", args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogWarning 记录警告日志(只记录到文件)
|
||||||
|
func LogWarning(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Warnf(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogSuccess 记录成功日志(只记录到文件)
|
||||||
|
func LogSuccess(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Infof(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogFatal 记录致命错误并退出(记录到文件和控制台)
|
||||||
|
func LogFatal(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Fatalf(format, args...)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogFileOnly 只记录到文件,不显示在控制台
|
||||||
|
func LogFileOnly(format string, args ...interface{}) {
|
||||||
|
if fileSugar != nil {
|
||||||
|
fileSugar.Infof(format, args...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogResult 用户界面结果输出(直接输出,不使用日志系统)
|
||||||
|
func LogResult(format string, args ...interface{}) {
|
||||||
|
fmt.Printf(format, args...)
|
||||||
|
}
|
385
utils/utils.go
Normal file
385
utils/utils.go
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetHostsPath 获取系统hosts文件路径
|
||||||
|
func GetHostsPath() string {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
return "C:\\Windows\\System32\\drivers\\etc\\hosts"
|
||||||
|
case "linux", "darwin":
|
||||||
|
return "/etc/hosts"
|
||||||
|
default:
|
||||||
|
return "/etc/hosts"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAdmin 检查是否以管理员权限运行
|
||||||
|
func CheckAdmin() {
|
||||||
|
if !isRunningAsAdmin() {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
LogError("需要管理员权限运行")
|
||||||
|
LogInfo("请右键点击命令提示符或PowerShell,选择'以管理员身份运行'")
|
||||||
|
} else {
|
||||||
|
LogError("需要root权限,请使用sudo运行")
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
LogSuccess("已以管理员权限运行")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateBlockName 验证块名称是否有效
|
||||||
|
func ValidateBlockName(name string) bool {
|
||||||
|
// 块名称只能包含字母、数字、下划线和连字符
|
||||||
|
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_-]+$`, name)
|
||||||
|
return matched && len(name) > 0 && len(name) <= 50
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateDomain 验证域名格式
|
||||||
|
func ValidateDomain(domain string) bool {
|
||||||
|
// 简单的域名格式验证
|
||||||
|
matched, _ := regexp.MatchString(`^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](\.[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9])*$`, domain)
|
||||||
|
return matched
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIP 验证IP地址格式
|
||||||
|
func ValidateIP(ip string) bool {
|
||||||
|
// 简单的IP地址格式验证
|
||||||
|
parts := strings.Split(ip, ".")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, part := range parts {
|
||||||
|
if len(part) == 0 || len(part) > 3 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为数字且在0-255范围内
|
||||||
|
num := 0
|
||||||
|
for _, c := range part {
|
||||||
|
if c < '0' || c > '9' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
num = num*10 + int(c-'0')
|
||||||
|
}
|
||||||
|
|
||||||
|
if num > 255 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrimString 去除字符串前后空白字符
|
||||||
|
func TrimString(s string) string {
|
||||||
|
return strings.TrimSpace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsComment 检查行是否为注释
|
||||||
|
func IsComment(line string) bool {
|
||||||
|
trimmed := TrimString(line)
|
||||||
|
return strings.HasPrefix(trimmed, "#")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileExists 检查文件是否存在
|
||||||
|
func FileExists(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return !os.IsNotExist(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile 写入文件
|
||||||
|
func WriteFile(path string, data []byte) error {
|
||||||
|
// 创建目录
|
||||||
|
dir := strings.ReplaceAll(path, "\\", "/")
|
||||||
|
if idx := strings.LastIndex(dir, "/"); idx != -1 {
|
||||||
|
if err := os.MkdirAll(dir[:idx], 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return os.WriteFile(path, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile 读取文件
|
||||||
|
func ReadFile(path string) ([]byte, error) {
|
||||||
|
return os.ReadFile(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackupFile 备份文件到用户备份目录
|
||||||
|
func BackupFile(path string) error {
|
||||||
|
data, err := ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入config包获取备份目录
|
||||||
|
// 注意:这里需要确保config已经初始化
|
||||||
|
backupDir := getBackupDirectory()
|
||||||
|
|
||||||
|
// 确保备份目录存在
|
||||||
|
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("创建备份目录失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成备份文件名(包含时间戳)
|
||||||
|
filename := filepath.Base(path)
|
||||||
|
timestamp := time.Now().Format("20060102_150405")
|
||||||
|
backupFileName := fmt.Sprintf("%s.%s.backup", filename, timestamp)
|
||||||
|
backupPath := filepath.Join(backupDir, backupFileName)
|
||||||
|
|
||||||
|
// 写入备份文件
|
||||||
|
if err := WriteFile(backupPath, data); err != nil {
|
||||||
|
return fmt.Errorf("写入备份文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理旧备份文件,只保留最新的几个
|
||||||
|
if err := cleanOldBackups(backupDir, filename, 5); err != nil {
|
||||||
|
// 清理失败不影响备份操作,只记录日志
|
||||||
|
LogWarning("清理旧备份文件失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmptyLine 检查是否为空行
|
||||||
|
func IsEmptyLine(line string) bool {
|
||||||
|
return len(TrimString(line)) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseHostsLine 解析hosts文件行
|
||||||
|
func ParseHostsLine(line string) (ip, domain, comment string) {
|
||||||
|
line = TrimString(line)
|
||||||
|
|
||||||
|
// 检查是否为注释行
|
||||||
|
if IsComment(line) {
|
||||||
|
return "", "", line
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分离注释部分
|
||||||
|
commentIndex := strings.Index(line, "#")
|
||||||
|
if commentIndex != -1 {
|
||||||
|
comment = TrimString(line[commentIndex:])
|
||||||
|
line = TrimString(line[:commentIndex])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分离IP和域名
|
||||||
|
parts := strings.Fields(line)
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
ip = parts[0]
|
||||||
|
domain = parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ip, domain, comment
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatHostsLine 格式化hosts文件行
|
||||||
|
func FormatHostsLine(ip, domain, comment string) string {
|
||||||
|
if ip == "" || domain == "" {
|
||||||
|
if comment != "" {
|
||||||
|
return comment
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
line := fmt.Sprintf("%-15s %s", ip, domain)
|
||||||
|
if comment != "" && !strings.HasPrefix(comment, "#") {
|
||||||
|
line += " # " + comment
|
||||||
|
} else if comment != "" {
|
||||||
|
line += " " + comment
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBackupDirectory 获取备份目录
|
||||||
|
func getBackupDirectory() string {
|
||||||
|
// 获取用户配置目录
|
||||||
|
currentUser, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "backup" // 回退到相对路径
|
||||||
|
}
|
||||||
|
return filepath.Join(currentUser.HomeDir, ".hostsync", "backup")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanOldBackups 清理旧备份文件,只保留最新的count个
|
||||||
|
func cleanOldBackups(backupDir, originalFileName string, keepCount int) error {
|
||||||
|
// 读取备份目录中的文件
|
||||||
|
files, err := os.ReadDir(backupDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 筛选出匹配原文件名的备份文件
|
||||||
|
var backupFiles []os.DirEntry
|
||||||
|
prefix := originalFileName + "."
|
||||||
|
suffix := ".backup"
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() && strings.HasPrefix(file.Name(), prefix) && strings.HasSuffix(file.Name(), suffix) {
|
||||||
|
backupFiles = append(backupFiles, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果备份文件数量不超过保留数量,直接返回
|
||||||
|
if len(backupFiles) <= keepCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按修改时间排序(最新的在前)
|
||||||
|
sort.Slice(backupFiles, func(i, j int) bool {
|
||||||
|
infoI, _ := backupFiles[i].Info()
|
||||||
|
infoJ, _ := backupFiles[j].Info()
|
||||||
|
return infoI.ModTime().After(infoJ.ModTime())
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除多余的旧备份文件
|
||||||
|
for i := keepCount; i < len(backupFiles); i++ {
|
||||||
|
oldBackupPath := filepath.Join(backupDir, backupFiles[i].Name())
|
||||||
|
if err := os.Remove(oldBackupPath); err != nil {
|
||||||
|
LogWarning("删除旧备份文件失败: %s, %v", oldBackupPath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisplayWidth 计算字符串的显示宽度(中文字符占2个宽度)
|
||||||
|
func DisplayWidth(s string) int {
|
||||||
|
width := 0
|
||||||
|
for _, r := range s {
|
||||||
|
if r > 127 { // 非ASCII字符
|
||||||
|
width += 2
|
||||||
|
} else {
|
||||||
|
width += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
}
|
||||||
|
|
||||||
|
// PadString 根据显示宽度填充字符串
|
||||||
|
func PadString(s string, width int) string {
|
||||||
|
currentWidth := DisplayWidth(s)
|
||||||
|
if currentWidth >= width {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
padding := strings.Repeat(" ", width-currentWidth)
|
||||||
|
return s + padding
|
||||||
|
}
|
||||||
|
|
||||||
|
// TruncateWithWidth 根据显示宽度截断字符串
|
||||||
|
func TruncateWithWidth(s string, maxWidth int) string {
|
||||||
|
if DisplayWidth(s) <= maxWidth {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
width := 0
|
||||||
|
var result []rune
|
||||||
|
for _, r := range s {
|
||||||
|
charWidth := 1
|
||||||
|
if r > 127 {
|
||||||
|
charWidth = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if width+charWidth > maxWidth-3 { // 为"..."预留3个字符
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, r)
|
||||||
|
width += charWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(result) + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatTable 格式化表格输出
|
||||||
|
func FormatTable(headers []string, rows [][]string, columnWidths []int) {
|
||||||
|
// 如果没有指定列宽,自动计算
|
||||||
|
if len(columnWidths) == 0 {
|
||||||
|
columnWidths = make([]int, len(headers))
|
||||||
|
|
||||||
|
// 计算标题宽度
|
||||||
|
for i, header := range headers {
|
||||||
|
columnWidths[i] = DisplayWidth(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算数据行宽度
|
||||||
|
for _, row := range rows {
|
||||||
|
for i, cell := range row {
|
||||||
|
if i < len(columnWidths) {
|
||||||
|
width := DisplayWidth(cell)
|
||||||
|
if width > columnWidths[i] {
|
||||||
|
columnWidths[i] = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置最小和最大宽度
|
||||||
|
for i := range columnWidths {
|
||||||
|
if columnWidths[i] < 8 {
|
||||||
|
columnWidths[i] = 8
|
||||||
|
}
|
||||||
|
if columnWidths[i] > 40 {
|
||||||
|
columnWidths[i] = 40
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印顶部边框
|
||||||
|
printTableBorder(columnWidths, "┌", "┬", "┐")
|
||||||
|
|
||||||
|
// 打印标题行
|
||||||
|
fmt.Print("│")
|
||||||
|
for i, header := range headers {
|
||||||
|
paddedHeader := PadString(" "+header+" ", columnWidths[i]+2)
|
||||||
|
fmt.Print(paddedHeader + "│")
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// 打印标题分隔线
|
||||||
|
printTableBorder(columnWidths, "├", "┼", "┤")
|
||||||
|
|
||||||
|
// 打印数据行
|
||||||
|
for _, row := range rows {
|
||||||
|
fmt.Print("│")
|
||||||
|
for i, cell := range row {
|
||||||
|
if i < len(columnWidths) {
|
||||||
|
// 截断过长的内容
|
||||||
|
truncatedCell := TruncateWithWidth(cell, columnWidths[i])
|
||||||
|
paddedCell := PadString(" "+truncatedCell+" ", columnWidths[i]+2)
|
||||||
|
fmt.Print(paddedCell + "│")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打印底部边框
|
||||||
|
printTableBorder(columnWidths, "└", "┴", "┘")
|
||||||
|
}
|
||||||
|
|
||||||
|
// printTableBorder 打印表格边框
|
||||||
|
func printTableBorder(columnWidths []int, left, middle, right string) {
|
||||||
|
fmt.Print(left)
|
||||||
|
for i, width := range columnWidths {
|
||||||
|
fmt.Print(strings.Repeat("─", width+2))
|
||||||
|
if i < len(columnWidths)-1 {
|
||||||
|
fmt.Print(middle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(right)
|
||||||
|
}
|
12
utils/utils_unix.go
Normal file
12
utils/utils_unix.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// isRunningAsAdmin 检查当前进程是否以管理员权限运行 (Unix/Linux/macOS)
|
||||||
|
func isRunningAsAdmin() bool {
|
||||||
|
// 在Unix系统中,检查是否为root用户
|
||||||
|
return os.Getuid() == 0
|
||||||
|
}
|
35
utils/utils_windows.go
Normal file
35
utils/utils_windows.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
//go:build windows
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
// isRunningAsAdmin 检查当前进程是否以管理员权限运行 (Windows)
|
||||||
|
func isRunningAsAdmin() bool {
|
||||||
|
var sid *windows.SID
|
||||||
|
|
||||||
|
// 获取内置管理员组的SID
|
||||||
|
err := windows.AllocateAndInitializeSid(
|
||||||
|
&windows.SECURITY_NT_AUTHORITY,
|
||||||
|
2,
|
||||||
|
windows.SECURITY_BUILTIN_DOMAIN_RID,
|
||||||
|
windows.DOMAIN_ALIAS_RID_ADMINS,
|
||||||
|
0, 0, 0, 0, 0, 0,
|
||||||
|
&sid)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer windows.FreeSid(sid)
|
||||||
|
|
||||||
|
// 获取当前进程的访问令牌
|
||||||
|
token := windows.Token(0)
|
||||||
|
member, err := token.IsMember(sid)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return member
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user