package linux // lspci | grep -i vga import ( "client/internal/db" message "client/internal/msg" "client/internal/utils" "fmt" "log" "os" "os/exec" "os/user" "path/filepath" "strings" "sync" "time" "gopkg.in/ini.v1" ) type LinuxClient struct { mu sync.Mutex Auth string MachineCode string ID string MiningConfig message.MiningConfig db *db.SQLiteServer Status int // 当前client状态,1正在工作,2空闲 currentProcess *currentProcess // 当前挖矿进程及类型 } type currentProcess struct { process *exec.Cmd miner string } // CheckPermission 检查当前进程是否具备运行本客户端所需的权限(Linux) // 要求使用 root 运行,否则很多命令(如 dmidecode、nvidia-smi 等)可能失败。 func CheckPermission() error { u, err := user.Current() if err != nil { return fmt.Errorf("获取当前用户信息失败: %v", err) } if u.Uid != "0" { return fmt.Errorf("当前用户(%s)不是 root 用户,请使用 sudo 或 root 账号运行本程序", u.Username) } return nil } func NewLinuxClient(auth string) *LinuxClient { cfg, err := ini.Load("mining.linux.conf") if err != nil { log.Fatalf("获取挖矿配置失败: %v", err) log.Printf("客户端已退出,请重新检查配置文件(%s)是否存在", "mining.linux.conf") return nil } // 解析配置 var miningConfig message.MiningConfig // 解析 [bzminer] 部分 sectionBzMiner := cfg.Section("bzminer") miningConfig.BzMinerPath = sectionBzMiner.Key("path").String() // 解析 [lolminer] 部分 sectionLolMiner := cfg.Section("lolminer") miningConfig.LolMinerPath = sectionLolMiner.Key("path").String() // 解析 [rigel] 部分 sectionRigel := cfg.Section("rigel") miningConfig.RigelPath = sectionRigel.Key("path").String() // 解析 [proxy] 部分 sectionProxy := cfg.Section("proxy") miningConfig.ProxyEnabled, _ = sectionProxy.Key("proxy").Bool() if miningConfig.BzMinerPath != "" { result := utils.CheckFileExists(miningConfig.BzMinerPath) if !result { log.Fatalf("未检测到bzminer挖矿软件的存在,请确认是否已经安装bzminer,并核对下列路径:%s", miningConfig.BzMinerPath) log.Println("客户端已退出,确认路径后请重启客户端") return nil } } if miningConfig.LolMinerPath != "" { result := utils.CheckFileExists(miningConfig.LolMinerPath) if !result { log.Fatalf("未检测到lolminer挖矿软件的存在,请确认是否已经安装lolminer,并核对下列路径:%s", miningConfig.LolMinerPath) log.Println("客户端已退出,确认路径后请重启客户端") return nil } } if miningConfig.RigelPath != "" { result := utils.CheckFileExists(miningConfig.RigelPath) if !result { log.Fatalf("未检测到rigel挖矿软件的存在,请确认是否已经安装rigel,并核对下列路径:%s", miningConfig.RigelPath) log.Println("客户端已退出,确认路径后请重启客户端") return nil } } var client = &LinuxClient{} client.Auth = auth client.MiningConfig = miningConfig client.Status = 2 // 初始化sqlite3数据库 db := db.NewSQLiteServer() client.db = db client.initHistoryTask() // 获取主机身份 mac, err := client.GetMACAddress() if err != nil { log.Fatalln(err) panic("获取当前主机信息失败,程序已退出,请检查网络后重新启动本客户端。") } client.MachineCode = mac client.ID = auth + "." + mac return client } func (l *LinuxClient) initHistoryTask() { exsits, task := l.db.LoadMiningTask() if exsits { currentTs := time.Now().Unix() if currentTs < int64(task.EndTimestamp) { l.mu.Lock() l.Status = 1 l.mu.Unlock() err := l.Mining(task) if err != nil { log.Fatalf("重新开启挖矿任务失败,请手动检查:%v", err) return } } else { l.db.FinishMiningTask(task) } } } // 获取 NVIDIA 显卡信息 func getNvidiaGPUInfo() (map[int]message.GPU, error) { // 使用 nvidia-smi 列出所有 GPU cmd := exec.Command("nvidia-smi", "--query-gpu=name,memory.total", "--format=csv,noheader,nounits") output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("无法执行 nvidia-smi 命令: %v", err) } // 解析输出 info := strings.TrimSpace(string(output)) lines := strings.Split(info, "\n") gpus := make(map[int]message.GPU) for i, line := range lines { parts := strings.Split(strings.TrimSpace(line), ", ") if len(parts) < 2 { return nil, fmt.Errorf("nvidia-smi 输出格式错误") } // 解析显存容量 mem, err := parseMemory(parts[1]) if err != nil { return nil, fmt.Errorf("无法解析显存容量: %v", err) } // 将每个 GPU 的信息存储到 map 中 gpus[i] = message.GPU{ Brand: "NVIDIA", Model: parts[0], Mem: mem, } } return gpus, nil } // 解析显存容量字符串并返回 float64 类型 func parseMemory(memStr string) (float64, error) { var mem float64 _, err := fmt.Sscanf(memStr, "%f", &mem) if err != nil { return 0, fmt.Errorf("显存解析失败: %v", err) } return mem, nil } // 获取其他显卡信息(如 Intel 或 AMD) func getOtherGPUInfo() (map[int]message.GPU, error) { // 使用 lspci 列出所有显卡设备 cmd := exec.Command("lspci", "-v") output, err := cmd.Output() if err != nil { return nil, fmt.Errorf("无法执行 lspci 命令: %v", err) } // 解析输出,获取显卡信息 info := strings.TrimSpace(string(output)) lines := strings.Split(info, "\n") gpus := make(map[int]message.GPU) gpuIndex := 0 for _, line := range lines { // 假设输出中包含 Intel 或 AMD GPU if strings.Contains(line, "VGA compatible controller") { if strings.Contains(line, "Intel") || strings.Contains(line, "AMD") { // 将 Intel/AMD GPU 信息存储到 map 中 gpus[gpuIndex] = message.GPU{ Brand: "Intel/AMD", Model: line, // 这里可以根据实际 lspci 输出提取显卡型号 Mem: 0, // 无法通过 lspci 获取显存 } gpuIndex++ } } } if len(gpus) == 0 { return nil, fmt.Errorf("未找到显卡信息") } return gpus, nil } // 获取所有 GPU 信息 func (l *LinuxClient) GetGPUInfo() (map[int]message.GPU, error) { // 尝试获取 NVIDIA GPU 信息 gpus, err := getNvidiaGPUInfo() if err == nil { return gpus, nil } // 如果没有 NVIDIA 显卡,尝试获取其他类型显卡信息 return getOtherGPUInfo() } // 获取 MAC 地址 // 获取“机器码”:Linux 下改为使用主机 UUID // 优先从 /sys/class/dmi/id/product_uuid 读取,失败时再尝试 dmidecode func (l *LinuxClient) GetMACAddress() (string, error) { // 1. 优先读取内核提供的 product_uuid 文件(大多数物理机/虚拟机都支持) const uuidPath = "/sys/class/dmi/id/product_uuid" if data, err := os.ReadFile(uuidPath); err == nil { uuid := strings.TrimSpace(string(data)) if uuid != "" && !strings.EqualFold(uuid, "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") { uuid = strings.Trim(uuid, "{}") uuid = strings.ToUpper(uuid) return uuid, nil } } // 2. 退回使用 dmidecode(需要有权限) cmd := exec.Command("dmidecode", "-s", "system-uuid") out, err := cmd.Output() if err != nil { return "", fmt.Errorf("获取主机 UUID 失败: %v", err) } uuid := strings.TrimSpace(string(out)) if uuid == "" || strings.EqualFold(uuid, "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") { return "", fmt.Errorf("获取到的主机 UUID 无效: %q", uuid) } uuid = strings.Trim(uuid, "{}") uuid = strings.ToUpper(uuid) return uuid, nil } /* 配置lolminer #!/bin/bash POOL=47.108.221.51:3333 WALLET=m2test.11x12 ALGO=NEXA ./lolMiner --algo $ALGO --pool $POOL --user $WALLET $@ */ func (l *LinuxClient) lolminer(cfg message.ConfigurationMiningMsg) { l.mu.Lock() if l.Status != 2 { log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) return } l.Status = 1 l.mu.Unlock() var address string if cfg.WalletMining { address = cfg.WalletAddress } else { address = cfg.PoolUser } dir := l.MiningConfig.LolMinerPath name := filepath.Join(dir, "lolMiner") args := []string{"--algo", cfg.Coin, "--pool", cfg.PoolUrl, "--user", address + "." + cfg.WorkerID} cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Printf("执行命令:%s %s", name, strings.Join(args, " ")) // 启动进程 err := cmd.Start() if err != nil { log.Fatalf("Error starting lolMiner: %v", err) return } // 添加执行记录 go func() { err := l.db.InsertMiningTask(cfg) if err != nil { log.Fatalf("本次挖矿任务记录失败:%v", err) } }() // 获取 lolMiner 的进程 ID fmt.Printf("lolMiner started with PID: %d\n", cmd.Process.Pid) // 记录当前挖矿进程 l.mu.Lock() l.currentProcess = ¤tProcess{ process: cmd, miner: "lolminer", } l.mu.Unlock() // 获取当前时间戳(秒级) currentTimestamp := time.Now().Unix() endTimestamp := int64(cfg.EndTimestamp) // 计算目标时间戳和当前时间戳之间的差值 if endTimestamp <= currentTimestamp { // 如果目标时间已经到达,立即结束挖矿进程 fmt.Println("目标时间已经到达,立即结束 lolMiner 挖矿进程") l.StopMining() } else { // 计算需要等待的秒数 waitDuration := time.Second * time.Duration(endTimestamp-currentTimestamp) fmt.Printf("当前时间戳:%d,目标时间戳:%d,剩余时间:%v\n", currentTimestamp, endTimestamp, waitDuration) // 使用 time.Sleep 等待直到目标时间戳 time.Sleep(waitDuration) fmt.Println("目标时间到达,开始执行操作,停止 lolMiner 挖矿进程") // 通过 StopMining 统一停止挖矿进程 l.StopMining() // 修改执行记录 go func() { err := l.db.FinishMiningTask(cfg) if err != nil { log.Fatalf("修改执行记录失败:%v", err) } }() // 输出进程被杀死的信息 fmt.Println("lolMiner process killed.") } log.Printf("当前挖矿任务已执行完毕:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) l.mu.Lock() l.Status = 2 l.mu.Unlock() } /* 配置bzminer #!/bin/bash POOL=47.108.221.51:3333 WALLET=m2test.11x12 ALGO=NEXA ./bzminer -a $ALGO -w $WALLET -p $POOL */ func (l *LinuxClient) bzminer(cfg message.ConfigurationMiningMsg) { l.mu.Lock() if l.Status != 2 { log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) return } l.Status = 1 l.mu.Unlock() var address string if cfg.WalletMining { address = cfg.WalletAddress } else { address = cfg.PoolUser } dir := l.MiningConfig.BzMinerPath name := filepath.Join(dir, "bzminer") args := []string{"-a", cfg.Coin, "-w", address + "." + cfg.WorkerID, "-p", cfg.PoolUrl} cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Printf("执行命令:%s %s", name, strings.Join(args, " ")) // 启动进程 err := cmd.Start() if err != nil { log.Fatalf("Error starting bzminer: %v", err) return } // 添加执行记录 go func() { err := l.db.InsertMiningTask(cfg) if err != nil { log.Fatalf("本次挖矿任务记录失败:%v", err) } }() // 获取 bzminer 的进程 ID fmt.Printf("bzminer started with PID: %d\n", cmd.Process.Pid) // 记录当前挖矿进程 l.mu.Lock() l.currentProcess = ¤tProcess{ process: cmd, miner: "bzminer", } l.mu.Unlock() // 获取当前时间戳(秒级) currentTimestamp := time.Now().Unix() endTimestamp := int64(cfg.EndTimestamp) // 计算目标时间戳和当前时间戳之间的差值 if endTimestamp <= currentTimestamp { fmt.Println("目标时间已经到达,停止 bzminer 挖矿进程") l.StopMining() } else { // 计算需要等待的秒数 waitDuration := time.Second * time.Duration(endTimestamp-currentTimestamp) fmt.Printf("当前时间戳:%d,目标时间戳:%d,剩余时间:%v\n", currentTimestamp, endTimestamp, waitDuration) // 使用 time.Sleep 等待直到目标时间戳 time.Sleep(waitDuration) fmt.Println("目标时间到达,开始执行操作,停止 bzminer 挖矿进程") // 通过 StopMining 统一停止挖矿进程 l.StopMining() // 修改执行记录 go func() { err := l.db.FinishMiningTask(cfg) if err != nil { log.Fatalf("修改执行记录失败:%v", err) } }() // 输出进程被杀死的信息 fmt.Println("bzminer process killed.") } log.Printf("当前挖矿任务已执行完毕:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) l.mu.Lock() l.Status = 2 l.mu.Unlock() } /* 配置rigel #!/bin/bash POOL=47.108.221.51:3333 WALLET=m2test USER=11x12 ALGO=nexapow ./rigel -a $ALGO -o $POOL -u $WALLET -w $USER --log-file logs/miner.log */ func (l *LinuxClient) rigel(cfg message.ConfigurationMiningMsg) { l.mu.Lock() if l.Status != 2 { log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) return } l.Status = 1 l.mu.Unlock() var address string if cfg.WalletMining { address = cfg.WalletAddress } else { address = cfg.PoolUser } dir := l.MiningConfig.RigelPath name := filepath.Join(dir, "rigel") // 禁用 rigel 内置 watchdog,避免自动拉起子进程 args := []string{"--no-watchdog", "-a", strings.ToLower(cfg.Algo), "-o", cfg.PoolUrl, "-u", address, "-w", cfg.WorkerID, "--log-file", "logs/miner.log"} cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr log.Printf("执行命令:%s %s", name, strings.Join(args, " ")) // 启动进程 err := cmd.Start() if err != nil { log.Fatalf("Error starting rigel: %v", err) return } // 添加执行记录 go func() { err := l.db.InsertMiningTask(cfg) if err != nil { log.Fatalf("本次挖矿任务记录失败:%v", err) } }() // 获取 rigel 的进程 ID fmt.Printf("rigel started with PID: %d\n", cmd.Process.Pid) // 记录当前挖矿进程 l.mu.Lock() l.currentProcess = ¤tProcess{ process: cmd, miner: "rigel", } l.mu.Unlock() // 获取当前时间戳(秒级) currentTimestamp := time.Now().Unix() endTimestamp := int64(cfg.EndTimestamp) // 计算目标时间戳和当前时间戳之间的差值 if endTimestamp <= currentTimestamp { // 如果目标时间已经到达,立即结束 rigel 挖矿进程 fmt.Println("目标时间已经到达,立即结束 rigel 挖矿进程") l.StopMining() } else { // 计算需要等待的秒数 waitDuration := time.Second * time.Duration(endTimestamp-currentTimestamp) fmt.Printf("当前时间戳:%d,目标时间戳:%d,剩余时间:%v\n", currentTimestamp, endTimestamp, waitDuration) // 使用 time.Sleep 等待直到目标时间戳 time.Sleep(waitDuration) fmt.Println("目标时间到达,开始执行操作,结束 rigel 挖矿进程") // 通过 StopMining 统一停止挖矿进程 l.StopMining() // 修改执行记录 go func() { err := l.db.FinishMiningTask(cfg) if err != nil { log.Fatalf("修改执行记录失败:%v", err) } }() // 输出进程被结束的信息 fmt.Println("rigel process killed.") } log.Printf("当前挖矿任务已执行完毕:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp) l.mu.Lock() l.Status = 2 l.mu.Unlock() } // XTM-rigel(1%), XNA-bzminer(1%), CLORE-bzminer(1%), CFX-rigel(2%), IRON-lolminer(1%), NEXA-lolminer(2%), KLS-lolminer(1%), RVN-bzminer(1%), ERG-bzminer(1%), XEL-rigel(2%) func (l *LinuxClient) Mining(cfg message.ConfigurationMiningMsg) error { info := cfg.Coin + "-" + cfg.Algo switch info { case "XTM-SHA3X": go l.lolminer(cfg) case "XNA-KawPow": go l.bzminer(cfg) case "CLORE-KawPow": go l.bzminer(cfg) case "CFX-Octopus": go l.rigel(cfg) case "IRON-IronFish": go l.lolminer(cfg) case "NEXA-NexaPow": go l.lolminer(cfg) case "KLS-KarlsenHash": go l.lolminer(cfg) case "RVN-KawPow": go l.bzminer(cfg) case "ERG-Autolykos": go l.bzminer(cfg) case "XEL-Xelishashv2": go l.rigel(cfg) default: return fmt.Errorf("不支持%s算法", info) } return nil } // 主动终止当前挖矿进程 func (l *LinuxClient) StopMining() { l.mu.Lock() cp := l.currentProcess l.mu.Unlock() if cp == nil || cp.process == nil || cp.process.Process == nil { log.Println("当前没有正在进行的挖矿任务") return } log.Printf("准备停止当前挖矿任务,miner=%s, pid=%d", cp.miner, cp.process.Process.Pid) // 根据挖矿软件类型选择合适的停止方式 switch strings.ToLower(cp.miner) { case "rigel": // 优先尝试优雅退出(Interrupt),失败再 Kill if err := cp.process.Process.Signal(os.Interrupt); err != nil { log.Printf("向 rigel 发送 Interrupt 失败,尝试 Kill: %v", err) if errKill := cp.process.Process.Kill(); errKill != nil { log.Printf("Kill rigel 失败:%v", errKill) } } default: // 其它挖矿软件直接 Kill if err := cp.process.Process.Kill(); err != nil { log.Printf("停止挖矿进程失败:%v", err) } } // 清理状态 l.mu.Lock() l.currentProcess = nil l.Status = 2 l.mu.Unlock() log.Println("当前挖矿任务已被手动停止") }