533 lines
14 KiB
Go
533 lines
14 KiB
Go
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
|
||
}
|