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