Files
mining-client/internal/src/linux/linux.go
2025-12-01 15:45:05 +08:00

605 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = &currentProcess{
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 = &currentProcess{
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 = &currentProcess{
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("当前挖矿任务已被手动停止")
}