8 Commits

Author SHA1 Message Date
lzx
68b00962d3 v-advance delete exe file 2025-12-05 14:03:09 +08:00
lzx
b58580044d v-advance update 2025-12-05 13:59:39 +08:00
lzx
b9aff707bd update README 2025-12-01 15:52:58 +08:00
lzx
1198159832 remove .gitignore 2025-12-01 15:48:22 +08:00
lzx
b57ec89e75 update v-advance 2025-12-01 15:46:04 +08:00
lzx
de010e39ee update v-advance 2025-12-01 15:45:05 +08:00
lzx
16173b7ccd clear caches 2025-12-01 15:38:44 +08:00
lzx
c7077f33d1 advance version complete 2025-12-01 15:34:12 +08:00
33 changed files with 1992 additions and 1009 deletions

1
.gitignore vendored
View File

@@ -1 +0,0 @@
/test/

View File

@@ -16,39 +16,36 @@
## 编译方法
### Windows 系统
在项目根目录下运行:
在项目根目录下运行Windows / Linux 通用):
```bash
cmd\windows.bat
go build -o bin/client ./cmd
```
编译后的可执行文件将位于 `bin/client.exe`
### Linux 系统
在项目根目录下运行:
```bash
chmod +x cmd/linux.sh
./cmd/linux.sh
```
编译后的可执行文件将位于 `bin/client`
- Windows 下会生成 `bin/client.exe`
- Linux 下会生成 `bin/client`
## 使用方法
1. **准备身份文件** `bin` 目录下创建 `auth` 文件,包含你的卖方身份信息
1. **身份文件(auth)**确认 `bin` 目录下存在 `auth` 文件,该文件会在下载客户端时自动生成,请勿删除、修改、移动该文件
2. **运行客户端**
- Windows: 运行 `bin\client.exe`
- Linux: 运行 `bin/client`
2. **运行客户端(需管理员/root 权限)**
- Windows: 以“管理员身份运行”命令行或终端,然后执行:
```bash
./client.exe
```
- Linux: 使用 root 或 sudo 启动:
```bash
sudo ./bin/client
```
3. 客户端将自动:
- 读取身份信息
- 获取主机MAC地址和GPU信息
- 进行权限自检(非 root / 非管理员会直接退出并给出提示)
- 获取主机 UUID作为机器码和 GPU 信息
- 连接到云算力平台服务器
- 进行 TCP 心跳保活和自动重连
- 自动检查远程版本并执行更新(需要配置远程更新服务)
- 等待并处理挖矿任务
## 重要注意事项
@@ -77,14 +74,19 @@ chmod +x cmd/linux.sh
- **重要**如果在相关GPU有租约且没有在平台申请故障处理的情况下直接更换GPU可能会导致产生罚没
- **建议**在有租约的情况下要更换故障GPU请第一时间前往平台申请故障处理在平台确认后再进行更换GPU的操作
### 持续挖矿(可选)
- 在 `bin/mining.linux.conf` 或 `bin/mining.windows.conf` 中配置 `[sustain]` 段,可以在**没有租约时自动挖矿**。
- 当有租约任务下发时,客户端会自动停止持续挖矿,执行租约任务;租约结束后会自动恢复持续挖矿。
## 项目结构
```
cloud-client/
├── bin/ # 编译输出目录
│ ├── auth # 身份认证文件(需手动创建)
│ ├── mining.linux.conf # Linux 挖矿配置
│ └── mining.windows.conf # Windows 挖矿配置
│ ├── auth # 身份认证文件(需手动创建)
│ ├── mining.linux.conf # Linux 挖矿配置(含持续挖矿配置)
│ └── mining.windows.conf # Windows 挖矿配置(含持续挖矿配置)
├── cmd/ # 主程序目录
│ ├── main.go # 程序入口
│ ├── windows.bat # Windows 编译脚本
@@ -93,19 +95,11 @@ cloud-client/
│ ├── client.go # 客户端主逻辑
│ ├── msg/ # 消息处理
│ ├── src/ # 系统相关实现
│ │ ├── linux/ # Linux 系统实现
│ │ └── windows/ # Windows 系统实现
── utils/ # 工具函数
│ │ ├── linux/ # Linux 系统实现GPU、挖矿进程管理、权限检测等
│ │ └── windows/ # Windows 系统实现GPU、挖矿进程管理、权限检测等
── sustain/ # 持续挖矿管理
│ ├── updater/ # 自动更新模块
│ └── utils/ # 工具函数文件读写、UUID 等)
├── go.mod # Go 模块定义
└── README.md # 本文件
```
## 依赖项
- `github.com/google/uuid` v1.6.0
- `gopkg.in/ini.v1` v1.67.0
## 许可证
[根据实际情况填写]

View File

@@ -2,7 +2,7 @@
[bzminer]
# path=/path/bzminer
[lolminer]
# path=/home/a11/桌面/lolminer/1.98
# path=/path/lolminer
[rigel]
# path=/path/rigel
@@ -10,3 +10,17 @@
#打开此注释后会使用各大矿池的专用网络每笔订单额外增加1%的网络费用
[proxy]
# proxy=true
#持续挖矿开关,即在矿机没有租约期间是否自行挖矿
#开启此选项启动客户端后客户端会自动根据下面配置开启挖矿任务直到云算力平台有人租赁本台GPU主机
#当该租约结束后本台GPU主机会自动切回下方配置的挖矿任务
[sustain]
#enabled=true
#algo="算法"
#coin="币种"
#miner="挖矿软件名此处使用的挖矿软件要使用上方已经配置路径的挖矿软件名即bzminer/lolminer/rigel只能填一个自行选择"
#pool_url="挖矿地址"
#wallet="挖矿钱包"
#worker_id="矿工号"
#pool_user="挖矿账号名f2pool/m2pool等不支持钱包挖矿的矿池需配置其余支持钱包挖矿的矿池无需配置"
#wallet_mining=true #pool_user打开时同时打开本配置

View File

@@ -11,3 +11,17 @@
#打开此注释后会使用各大矿池的专用网络每笔订单额外增加1%的网络费用
[proxy]
# proxy=true
#持续挖矿开关,即在矿机没有租约期间是否自行挖矿
#开启此选项启动客户端后客户端会自动根据下面配置开启挖矿任务直到云算力平台有人租赁本台GPU主机
#当该租约结束后本台GPU主机会自动切回下方配置的挖矿任务
[sustain]
#enabled=true
#algo="算法"
#coin="币种"
#miner="挖矿软件名此处使用的挖矿软件要使用上方已经配置路径的挖矿软件名即bzminer/lolminer/rigel只能填一个自行选择"
#pool_url="挖矿地址"
#wallet="挖矿钱包"
#worker_id="矿工号"
#pool_user="挖矿账号名f2pool/m2pool等不支持钱包挖矿的矿池需配置其余支持钱包挖矿的矿池无需配置"
#wallet_mining=true #pool_user打开时同时打开本配置

22
bin/user-manual.txt Normal file
View File

@@ -0,0 +1,22 @@
This program is an automated mining program for cloud computing platforms. After launching this client, the seller no longer needs to manually configure mining according to the buyer's requirements.
Please read the following precautions carefully before using this program.
Very important:
1. Please download the correct system version of the client, Windows or Linux (Ubuntu, etc.), based on the operating system of the host where your GPU is located.
2. You must download this client through the official website and under the correct seller account, as it contains your seller information. If you download the client through unofficial channels or the wrong seller account, your GPU information cannot be synchronized to the cloud computing platform.
3. After downloading and extracting, please ensure that your client contains the following files:
Execute file (client. exe or client)
auth
mining.linux.conf
mining.windows.conf
If there are missing files, please contact customer service for Customer-Service.
4. If you have multiple GPU hosts, you can download the client compressed file on one host and copy it to other hosts in any way, or download the compressed file on different hosts through step 2.
5. Before starting the client, please check that your computer meets the following conditions:
1. GPUs can be correctly recognized, which usually requires installing the corresponding driver for the GPU. NVIDIA series graphics cards can view the graphics card information through the nvidia smi command. If the command line prints the graphics card information, it means that the graphics card driver has been installed
2. At least one mining software from lolminer, rigel, and bzminer has been installed
3. The network is smooth, and different mining pools can be directly connected through the host where the GPU is located for mining
We suggest that you conduct mining tests on each mining pool before starting this client. If you can obtain mining tasks, it indicates that the network connection is normal. If your network conditions are poor, it will affect your rental income in the end.
Other matters:
1. Please ensure that the current user who starts the client has sufficient permissions. It is recommended to use the admin user for Windows and the root user for Linux. This client will automatically detect the permissions of relevant files at startup, and if the permissions are insufficient, the client will automatically exit.
2. If the GPU cannot be synchronized to the cloud computing platform after starting the client normally, please contact customer service for Customer-Service.
3. If your network conditions are not good, you can configure a proxy address on the cloud computing platform yourself. We will automatically forward the buyer's mining requests to the proxy address you have configured. If you have other needs, you can contact our customer service for Customer-Service.

1
bin/version Normal file
View File

@@ -0,0 +1 @@
version advanced

23
bin/用户手册.txt Normal file
View File

@@ -0,0 +1,23 @@
本程序是云算力平台的自动化挖矿程序,启动本客户端后,卖方不再需要手动针对买方的要求进行手动配置挖矿
使用本程序前请您仔细阅读以下注意事项
非常重要:
1请您根据您的GPU所在主机的操作系统下载正确系统版本的客户端windows或linux(ubuntu等)
2必须通过官网且在正确的卖家账号下下载本客户端因为里面包含您的卖家信息如果通过非官方渠道或错误的卖家账号下载了客户端您的GPU信息无法同步到云算力平台
3下载并解压后请确定您的客户端包含以下文件
执行文件(client.exe或client)
auth
mining.linux.conf
mining.windows.conf
如果有文件缺失,请联系客服获得帮助
4如果您有多个GPU主机可在某一台主机下载客户端压缩包后将压缩包通过任意方式复制给其他主机或在不同主机上通过步骤2下载压缩包
5在启动客户端之前请检查您的电脑具备以下条件
1GPU可以被正确识别这通常需要安装GPU对应的驱动NVIDIA系列显卡可以通过 nvidia-smi 命令查看显卡信息,如果命令行打印了显卡信息,则表示显卡驱动已经安装
2已经安装lolminer、rigel、bzminer中至少一个挖矿软件
3网络通畅可以直接通过GPU所在主机连接不同的矿池挖矿
建议您启动本客户端之前,自行对各个矿池进行挖矿测试,能获取到挖矿任务表示网络连接正常。如果您的网络条件不好,会影响最终您的租赁收益
其他事项:
1请确保启动客户端的当前用户拥有足够的权限建议windows使用admin用户linux使用root用户本客户端会在启动时自动检测相关文件的权限如果权限不够客户端会自动退出
2如果正常启动客户端后未能正常将GPU同步到云算力平台请联系客服获得帮助
3如果您的网络条件不好可以在云算力平台自行配置代理地址我们会自动将买家的挖矿请求自动转发到您配置的代理地址如果有其他需求可以联系我们的客服获得帮助

View File

@@ -1,7 +1,38 @@
package main
import client "client/internal"
import (
client "client/internal"
"client/internal/updater"
"log"
"os"
"os/signal"
"syscall"
)
const VERSION string = "version advanced"
const PROXY_URL string = "18.183.240.108:2345" // 服务地址
const REMOTE_UPDATE_URL = "https://test.m2pool.com/api/lease" // 远程更新服务器地址
func main() {
client.Star()
// 启动前检查更新
log.Println("检查更新...")
needRestart, err := updater.CheckAndUpdate(REMOTE_UPDATE_URL, VERSION)
if err != nil {
log.Printf("更新检查失败,继续运行当前版本:%v", err)
} else if needRestart {
log.Println("更新完成!请重启程序以使用新版本。")
os.Exit(0)
}
// 在单独的 goroutine 中启动客户端逻辑
go client.Star(PROXY_URL)
// 监听系统信号,实现优雅退出
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
sig := <-sigCh
log.Printf("收到退出信号: %v客户端准备退出...", sig)
// 停止客户端(包括持续挖矿)
client.StopClient()
}

1
go.mod
View File

@@ -4,5 +4,6 @@ go 1.25.4
require (
github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.32
gopkg.in/ini.v1 v1.67.0
)

2
go.sum
View File

@@ -1,4 +1,6 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

View File

@@ -17,6 +17,8 @@ import (
message "client/internal/msg"
"client/internal/src"
"client/internal/src/linux"
"client/internal/src/windows"
"client/internal/sustain"
"client/internal/utils"
"encoding/json"
"fmt"
@@ -24,15 +26,31 @@ import (
"net"
"runtime"
"strings"
"sync"
"time"
"os"
"gopkg.in/ini.v1"
)
type Client struct {
Auth string
MachineCode string
ServerConn net.Conn // 服务连接
GPUs map[int]message.GPU // {"gpu编号": message.GPU{}, ...}
os *src.SystemServer
osName string
Auth string
MachineCode string
ServerConn net.Conn // 服务连接
GPUs map[int]message.GPU // {"gpu编号": message.GPU{}, ...}
os *src.SystemServer
osName string
serverURL string // 服务器地址
mu sync.Mutex // 保护连接操作的互斥锁
lastPong time.Time // 最后一次收到pong的时间
stopHeartbeat chan struct{} // 停止心跳的通道
sustainMiner *sustain.SustainMiner // 持续挖矿管理器
}
// osExit 用于在权限检查失败时退出程序,便于测试时覆盖
var osExit = func(code int) {
os.Exit(code)
}
func newClient(url string) *Client {
@@ -46,45 +64,111 @@ func newClient(url string) *Client {
client.Auth = auth
os := src.NewSystemServer()
systemServer := src.NewSystemServer()
sys := runtime.GOOS
if sys == "linux" {
linux_ := linux.NewLinuxClient(auth)
os.ResiterSystem("linux", linux_)
} else {
log.Printf("主机操作系统:%s", sys)
// 启动前进行权限自检(不同系统分别检查)
switch sys {
case "linux":
if err := linux.CheckPermission(); err != nil {
log.Println(err.Error())
log.Println("权限不足,客户端已退出。")
osExit(1)
}
linux_ := linux.NewLinuxClient(auth)
systemServer.ResiterSystem("linux", linux_)
case "windows":
if err := windows.CheckPermission(); err != nil {
log.Println(err.Error())
log.Println("权限不足,客户端已退出。")
osExit(1)
}
windows_ := windows.NewWindowsClient(auth)
systemServer.ResiterSystem("windows", windows_)
default:
log.Printf("不支持的操作系统:%s客户端已退出。", sys)
osExit(1)
}
client.os = os
client.os = systemServer
client.osName = sys
// 初始化持续挖矿管理器(从配置文件读取挖矿软件路径)
var miningConfig message.MiningConfig
var confFile string
if sys == "windows" {
confFile = "mining.windows.conf"
} else {
confFile = "mining.linux.conf"
}
// 读取挖矿配置
cfg, err := ini.Load(confFile)
if err == nil {
sectionBzMiner := cfg.Section("bzminer")
miningConfig.BzMinerPath = sectionBzMiner.Key("path").String()
sectionLolMiner := cfg.Section("lolminer")
miningConfig.LolMinerPath = sectionLolMiner.Key("path").String()
sectionRigel := cfg.Section("rigel")
miningConfig.RigelPath = sectionRigel.Key("path").String()
sectionProxy := cfg.Section("proxy")
miningConfig.ProxyEnabled, _ = sectionProxy.Key("proxy").Bool()
}
client.sustainMiner = sustain.NewSustainMiner(systemServer, sys, miningConfig)
// 读取主机MAC地址信息
var machine_code string
machine_code, err = os.GetMACAddress(sys)
machine_code, err = systemServer.GetMACAddress(sys)
if err != nil {
log.Fatalln(err)
panic("获取当前主机信息失败,程序已退出,请检查网络后重新启动本客户端。")
}
utils.WirteFile("./machinecode", machine_code)
// utils.WirteFile("./machinecode", machine_code)
client.MachineCode = machine_code
gpus, err := os.GetGPUInfo(sys)
gpus, err := systemServer.GetGPUInfo(sys)
if err != nil {
log.Fatalln(err)
panic("获取当前主机GPU数据失败程序已退出请检查GPU驱动等程序后重新启动本客户端。")
}
client.GPUs = gpus
client.serverURL = url
client.stopHeartbeat = make(chan struct{})
client.lastPong = time.Now()
// 连接服务端
server_conn, err := net.Dial("tcp", url)
if err != nil {
log.Fatalf("客户端连接到服务器失败:%v", err)
return nil
}
defer server_conn.Close()
client.ServerConn = server_conn
return client
}
// Stop 停止客户端(包括持续挖矿)
func (c *Client) Stop() {
// 停止持续挖矿
if c.sustainMiner != nil {
c.sustainMiner.Stop()
}
// 关闭连接
c.mu.Lock()
if c.ServerConn != nil {
c.ServerConn.Close()
c.ServerConn = nil
}
close(c.stopHeartbeat)
c.mu.Unlock()
}
func (c *Client) sendMachineCode() {
var msg message.ServerMsg
msg.ID = c.Auth + "." + c.MachineCode
@@ -99,17 +183,44 @@ func (c *Client) sendMachineCode() {
}
func (c *Client) receiveMsg() {
defer func() {
c.mu.Lock()
if c.ServerConn != nil {
c.ServerConn.Close()
c.ServerConn = nil
}
c.mu.Unlock()
}()
buffer := make([]byte, 1024)
for {
n, err := c.ServerConn.Read(buffer)
c.mu.Lock()
conn := c.ServerConn
c.mu.Unlock()
if conn == nil {
log.Println("连接已断开,退出接收循环")
return
}
// 设置读取超时,用于检测连接是否存活
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
n, err := conn.Read(buffer)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
log.Println("读取超时,连接可能已断开")
c.reconnect()
return
}
if err.Error() == "EOF" {
// 服务端关闭连接时,退出接收循环
log.Println("服务端关闭了连接")
c.reconnect()
return
}
log.Println("接收数据失败:", err)
log.Printf("接收数据失败: %v", err)
c.reconnect()
return
}
@@ -118,12 +229,20 @@ func (c *Client) receiveMsg() {
}
}
func (c *Client) send(msg []byte) {
func (c *Client) send(msg []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
if c.ServerConn == nil {
return fmt.Errorf("连接已断开")
}
_, err := c.ServerConn.Write(msg)
if err != nil {
log.Fatalf("发送消息失败消息内容:%s", string(msg))
return
log.Printf("发送消息失败%v\n消息内容:%s", err, string(msg))
return err
}
return nil
}
func (c *Client) handleReceiveMsg(msg []byte) {
@@ -144,64 +263,239 @@ func (c *Client) handleReceiveMsg(msg []byte) {
return
}
switch data.Method {
case "pong":
// 收到心跳响应更新最后pong时间
c.mu.Lock()
c.lastPong = time.Now()
c.mu.Unlock()
log.Println("收到心跳响应")
return
case "mining.req":
mining_msg, ok := data.Params.(message.ConfigurationMiningMsg)
if ok {
// 这里开始挖矿
err := c.os.Mining(c.osName, mining_msg)
if err != nil {
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: false,
Data: err,
}
sendMsg_byte, err := json.Marshal(sendMsg_str)
if err != nil {
log.Fatalf("序列化%v失败%v", sendMsg_str, err)
break
}
c.send(sendMsg_byte) // 返回失败消息
}
// 挖矿开始
data := message.ConfigurationMiningResp{
Coin: mining_msg.Coin,
Algo: mining_msg.Algo,
Pool: mining_msg.Pool,
PoolUrl: mining_msg.PoolUrl,
WorkerID: mining_msg.WorkerID,
WalletAddress: mining_msg.WalletAddress,
WatchUrl: "", // 这里需要根据矿池自动生成
}
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: true,
Data: data,
}
sendMsg_byte, err := json.Marshal(sendMsg_str)
if err != nil {
log.Fatalf("序列化%v失败%v", sendMsg_str, err)
break
}
c.send(sendMsg_byte) // 返回成功消息
} else {
// 将 data.Params 重新序列化为 JSON然后反序列化为 ConfigurationMiningMsg
// 因为 data.Params 是 any 类型JSON 反序列化后是 map[string]interface{}
paramsJSON, err := json.Marshal(data.Params)
if err != nil {
log.Printf("序列化 Params 失败:%v", err)
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: false,
Data: fmt.Errorf("错误的params数据结构%v", mining_msg),
Data: fmt.Errorf("序列化 Params 失败%v", err),
}
sendMsg_byte, _ := json.Marshal(sendMsg_str)
c.send(sendMsg_byte)
return
}
var mining_msg message.ConfigurationMiningMsg
err = json.Unmarshal(paramsJSON, &mining_msg)
if err != nil {
log.Printf("解析挖矿配置消息失败:%v, Params: %s", err, string(paramsJSON))
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: false,
Data: fmt.Errorf("解析挖矿配置消息失败:%v", err),
}
sendMsg_byte, _ := json.Marshal(sendMsg_str)
c.send(sendMsg_byte)
return
}
// 暂停持续挖矿(如果有新任务)
if c.sustainMiner != nil && c.sustainMiner.IsRunning() {
c.sustainMiner.Pause()
}
// 这里开始挖矿
err = c.os.Mining(c.osName, mining_msg)
if err != nil {
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: false,
Data: err.Error(),
}
sendMsg_byte, err := json.Marshal(sendMsg_str)
if err != nil {
log.Fatalf("序列化%v失败%v", sendMsg_str, err)
break
return
}
c.send(sendMsg_byte) // 返回失败消息
return
}
// 挖矿开始
respData := message.ConfigurationMiningResp{
Coin: mining_msg.Coin,
Algo: mining_msg.Algo,
Pool: mining_msg.Pool,
PoolUrl: mining_msg.PoolUrl,
WorkerID: mining_msg.WorkerID,
WalletAddress: mining_msg.WalletAddress,
WatchUrl: "", // 这里需要根据矿池自动生成
}
sendMsg_str := message.ServerMsgResp{
ID: c.Auth + "." + c.MachineCode,
Result: true,
Data: respData,
}
sendMsg_byte, err := json.Marshal(sendMsg_str)
if err != nil {
log.Fatalf("序列化%v失败%v", sendMsg_str, err)
return
}
c.send(sendMsg_byte) // 返回成功消息
// 启动任务结束监控,任务结束后恢复持续挖矿
go c.monitorMiningTask(mining_msg)
case "mining.end":
c.os.StopMining(c.osName)
default:
log.Printf("未知的方法:%s", data.Method)
}
}
// monitorMiningTask 监控挖矿任务,任务结束后恢复持续挖矿
func (c *Client) monitorMiningTask(cfg message.ConfigurationMiningMsg) {
endTimestamp := int64(cfg.EndTimestamp)
currentTimestamp := time.Now().Unix()
// 如果任务已经结束,直接恢复持续挖矿
if endTimestamp <= currentTimestamp {
if c.sustainMiner != nil {
c.sustainMiner.Resume()
}
return
}
// 计算等待时间
waitDuration := time.Second * time.Duration(endTimestamp-currentTimestamp)
// 等待任务结束
time.Sleep(waitDuration)
log.Println("挖矿任务已结束,恢复持续挖矿")
// 恢复持续挖矿
if c.sustainMiner != nil {
c.sustainMiner.Resume()
}
}
// startHeartbeat 启动心跳检查
func (c *Client) startHeartbeat() {
ticker := time.NewTicker(30 * time.Second) // 每30秒发送一次心跳
defer ticker.Stop()
for {
select {
case <-ticker.C:
// 检查是否超过60秒未收到pong响应
c.mu.Lock()
lastPong := c.lastPong
conn := c.ServerConn
c.mu.Unlock()
if time.Since(lastPong) > 60*time.Second {
log.Println("超过60秒未收到心跳响应连接可能已断开")
c.reconnect()
return
}
// 发送心跳
if conn != nil {
pingMsg := message.ServerMsg{
ID: c.Auth + "." + c.MachineCode,
Method: "ping",
Params: nil,
}
msgByte, err := json.Marshal(pingMsg)
if err != nil {
log.Printf("序列化心跳消息失败:%v", err)
continue
}
if err := c.send(msgByte); err != nil {
log.Printf("发送心跳失败:%v", err)
c.reconnect()
return
}
log.Println("发送心跳")
}
case <-c.stopHeartbeat:
return
}
}
}
func Star() {
url := "xxxx"
client := newClient(url)
client.sendMachineCode()
go client.receiveMsg() // 开始接收服务端消息
// reconnect 重连服务器
func (c *Client) reconnect() {
c.mu.Lock()
if c.ServerConn != nil {
c.ServerConn.Close()
c.ServerConn = nil
}
close(c.stopHeartbeat)
c.stopHeartbeat = make(chan struct{})
serverURL := c.serverURL
c.mu.Unlock()
log.Println("尝试重新连接服务器...")
// 重连逻辑
for {
time.Sleep(5 * time.Second) // 等待5秒后重连
conn, err := net.Dial("tcp", serverURL)
if err != nil {
log.Printf("重连失败:%v5秒后重试...", err)
continue
}
log.Println("重连成功")
c.mu.Lock()
c.ServerConn = conn
c.lastPong = time.Now()
c.mu.Unlock()
// 重新发送机器码
c.sendMachineCode()
// 重新启动心跳和接收消息
go c.startHeartbeat()
go c.receiveMsg()
return
}
}
func Star(url string) {
// url := "10.168.2.249:8080"
// url := "47.108.221.51:23456"
client := newClient(url)
globalClient = client
client.sendMachineCode()
// 初始化并启动持续挖矿(如果配置启用)
if client.sustainMiner != nil {
err := client.sustainMiner.LoadConfig()
if err != nil {
log.Printf("加载持续挖矿配置失败:%v", err)
} else {
err = client.sustainMiner.Start()
if err != nil {
log.Printf("启动持续挖矿失败:%v", err)
}
}
}
// 启动心跳检查
go client.startHeartbeat()
// 开始接收服务端消息
client.receiveMsg()
}
var globalClient *Client
// StopClient 停止客户端(供外部调用)
func StopClient() {
if globalClient != nil {
globalClient.Stop()
}
}

145
internal/db/db.go Normal file
View File

@@ -0,0 +1,145 @@
package db
import (
message "client/internal/msg"
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3" // Import the sqlite3 driver
)
type SQLiteServer struct {
DB *sql.DB
}
// NewSQLiteServer creates a new SQLite server instance with an open database connection
func NewSQLiteServer() *SQLiteServer {
// Open (or create) the SQLite database
db, err := sql.Open("sqlite3", "./mining_task.db")
if err != nil {
log.Fatal(err)
return nil
}
// Create an SQLiteServer instance with the database connection
s := &SQLiteServer{
DB: db,
}
// Initialize the 'task' table
if err := s.initTaskTable(); err != nil {
log.Fatal(err)
return nil
}
return s
}
// initTaskTable creates the 'task' table if it doesn't already exist
func (s *SQLiteServer) initTaskTable() error {
// SQL statement to create the 'task' table if it doesn't exist
str := `
CREATE TABLE IF NOT EXISTS task (
id INTEGER PRIMARY KEY AUTOINCREMENT,
coin TEXT,
algo TEXT,
pool TEXT,
wallet_mining TEXT,
pool_url TEXT,
wallet_address TEXT,
pool_user TEXT,
worker_id TEXT,
end_timestamp INTEGER,
state INTEGER
);`
// Execute the query to create the table
_, err := s.DB.Exec(str)
if err != nil {
return fmt.Errorf("failed to create table: %w", err)
}
return nil
}
// Close closes the database connection manually when done with it
func (s *SQLiteServer) Close() error {
// Close the database connection
if err := s.DB.Close(); err != nil {
return fmt.Errorf("failed to close database: %w", err)
}
return nil
}
// LoadMiningTask loads a mining task from the database
func (s *SQLiteServer) LoadMiningTask() (bool, message.ConfigurationMiningMsg) {
str := "SELECT coin, algo, pool, wallet_mining, pool_url, wallet_address, pool_user, worker_id, end_timestamp FROM task WHERE state = ?;"
params := []any{0}
// Query the database
rows, err := s.DB.Query(str, params...)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
// Check if exactly one row is returned
var task message.ConfigurationMiningMsg
if rows.Next() {
// Scan the row into the task structure
err := rows.Scan(&task.Coin, &task.Algo, &task.Pool, &task.WalletMining, &task.PoolUrl, &task.WalletAddress, &task.PoolUser, &task.WorkerID, &task.EndTimestamp)
if err != nil {
log.Fatal(err)
}
// Check if there's more than one row
if rows.Next() {
// If there are multiple rows, return false with an empty structure
return false, message.ConfigurationMiningMsg{}
}
// If exactly one row is found, return true with the data
return true, task
}
// No rows found, return false with an empty structure
return false, message.ConfigurationMiningMsg{}
}
// InsertMiningTask inserts a new mining task into the database
func (s *SQLiteServer) InsertMiningTask(msg message.ConfigurationMiningMsg) error {
// SQL INSERT statement
str := `
INSERT INTO task (coin, algo, pool, wallet_mining, pool_url, wallet_address, pool_user, worker_id, end_timestamp, state)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
`
// Set the state to 0 by default (active or pending)
state := 0
// Execute the query
_, err := s.DB.Exec(str, msg.Coin, msg.Algo, msg.Pool, msg.WalletMining, msg.PoolUrl, msg.WalletAddress, msg.PoolUser, msg.WorkerID, msg.EndTimestamp, state)
if err != nil {
return fmt.Errorf("failed to insert mining task: %w", err)
}
return nil
}
// FinishMiningTask updates the state of a mining task to 'finished' (state = 1)
func (s *SQLiteServer) FinishMiningTask(msg message.ConfigurationMiningMsg) error {
// SQL UPDATE statement, matching all fields
str := `
UPDATE task
SET state = 1
WHERE coin = ? AND algo = ? AND pool = ? AND wallet_mining = ? AND pool_url = ? AND wallet_address = ? AND pool_user = ? AND worker_id = ? AND end_timestamp = ?;
`
// Execute the query
_, err := s.DB.Exec(str, msg.Coin, msg.Algo, msg.Pool, msg.WalletMining, msg.PoolUrl, msg.WalletAddress, msg.PoolUser, msg.WorkerID, msg.EndTimestamp)
if err != nil {
return fmt.Errorf("failed to finish mining task: %w", err)
}
return nil
}

View File

@@ -14,6 +14,7 @@ type ServerMsg struct {
type ServerMsgResp struct {
ID string `json:"id"`
Method string `json:"method"`
Result bool `json:"result"`
Data any `json:"data,omitempty"`
}

View File

@@ -2,12 +2,14 @@ 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"
@@ -17,12 +19,32 @@ import (
)
type LinuxClient struct {
mu sync.Mutex
Auth string
MachineCode string
ID string
MiningConfig message.MiningConfig
Status int // 当前client状态1正在工作2空闲
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 {
@@ -80,6 +102,12 @@ func NewLinuxClient(auth string) *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)
@@ -87,9 +115,29 @@ func NewLinuxClient(auth string) *LinuxClient {
}
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
@@ -189,24 +237,33 @@ func (l *LinuxClient) GetGPUInfo() (map[int]message.GPU, error) {
}
// 获取 MAC 地址
// 获取“机器码”Linux 下改为使用主机 UUID
// 优先从 /sys/class/dmi/id/product_uuid 读取,失败时再尝试 dmidecode
func (l *LinuxClient) GetMACAddress() (string, error) {
// 执行 ifconfig 或 ip link 命令
cmd := exec.Command("ifconfig", "-a")
out, err := cmd.Output()
if err != nil {
return "", err
}
// 查找 MAC 地址
lines := strings.Split(string(out), "\n")
for _, line := range lines {
if strings.Contains(line, "ether") {
parts := strings.Fields(line)
return parts[1], nil
// 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
}
}
return "", fmt.Errorf("MAC address not found")
// 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
}
/*
@@ -249,27 +306,47 @@ func (l *LinuxClient) lolminer(cfg message.ConfigurationMiningMsg) {
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("目标时间已经到达,直接执行操作")
// 如果目标时间已经到达,立即结束挖矿进程
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("目标时间到达,开始执行操作")
// 杀掉进程
err = cmd.Process.Kill()
if err != nil {
log.Fatalf("Error killing lolMiner: %v", err)
}
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.")
}
@@ -277,6 +354,7 @@ func (l *LinuxClient) lolminer(cfg message.ConfigurationMiningMsg) {
l.mu.Lock()
l.Status = 2
l.mu.Unlock()
}
/*
@@ -317,27 +395,46 @@ func (l *LinuxClient) bzminer(cfg message.ConfigurationMiningMsg) {
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("目标时间已经到达,直接执行操作")
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("目标时间到达,开始执行操作")
// 杀掉进程
err = cmd.Process.Kill()
if err != nil {
log.Fatalf("Error killing bzminer: %v", err)
}
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.")
}
@@ -374,7 +471,8 @@ func (l *LinuxClient) rigel(cfg message.ConfigurationMiningMsg) {
}
dir := l.MiningConfig.RigelPath
name := filepath.Join(dir, "rigel")
args := []string{"-a", strings.ToLower(cfg.Algo), "-o", cfg.PoolUrl, "-u", address, "-w", cfg.WorkerID, "--log-file", "logs/miner.log"}
// 禁用 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
@@ -386,28 +484,48 @@ func (l *LinuxClient) rigel(cfg message.ConfigurationMiningMsg) {
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 {
fmt.Println("目标时间已经到达,直接执行操作")
// 如果目标时间已经到达,立即结束 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("目标时间到达,开始执行操作")
// 杀掉进程
err = cmd.Process.Kill()
if err != nil {
log.Fatalf("Error killing rigel: %v", err)
}
// 输出进程被杀死的信息
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)
@@ -445,3 +563,42 @@ func (l *LinuxClient) Mining(cfg message.ConfigurationMiningMsg) error {
}
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("当前挖矿任务已被手动停止")
}

View File

@@ -3,12 +3,14 @@ package src
import (
message "client/internal/msg"
"fmt"
"log"
"sync"
)
type OS interface {
GetGPUInfo() (map[int]message.GPU, error)
Mining(cfg message.ConfigurationMiningMsg) error
StopMining()
GetMACAddress() (string, error)
}
@@ -61,3 +63,11 @@ func (s *SystemServer) GetMACAddress(osName string) (string, error) {
}
return "", fmt.Errorf("错误的操作系统:%s", osName)
}
func (s *SystemServer) StopMining(osName string) {
if srv, ok := s.systems[osName]; ok {
srv.StopMining()
} else {
log.Fatalf("错误的操作系统:%s", osName)
}
}

View File

@@ -1,13 +1,139 @@
package windows
// Windows 版本的客户端实现
import (
"client/internal/db"
message "client/internal/msg"
"client/internal/utils"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"path/filepath"
"strings"
"sync"
"time"
"gopkg.in/ini.v1"
)
type WindowsClient 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 检查当前进程是否具有管理员权限Windows
// 通过执行 `net session` 命令来判断:该命令只有在管理员权限下才会成功。
func CheckPermission() error {
cmd := exec.Command("net", "session")
if err := cmd.Run(); err != nil {
return fmt.Errorf("当前用户不是管理员,请以管理员身份运行本程序(右键以管理员运行)")
}
return nil
}
func NewWindowsClient(auth string) *WindowsClient {
cfg, err := ini.Load("mining.windows.conf")
if err != nil {
log.Fatalf("获取挖矿配置失败: %v", err)
log.Printf("客户端已退出,请重新检查配置文件(%s)是否存在", "mining.windows.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 = &WindowsClient{}
client.Auth = auth
client.MiningConfig = miningConfig
client.Status = 2
// 初始化sqlite3数据库
db := db.NewSQLiteServer()
client.db = db
mac, err := client.GetMACAddress()
if err != nil {
log.Fatalln(err)
panic("获取当前主机信息失败,程序已退出,请检查网络后重新启动本客户端。")
}
client.MachineCode = mac
client.ID = auth + "." + mac
return client
}
func (w *WindowsClient) initHistoryTask() {
exsits, task := w.db.LoadMiningTask()
if exsits {
currentTs := time.Now().Unix()
if currentTs < int64(task.EndTimestamp) {
w.mu.Lock()
w.Status = 1
w.mu.Unlock()
err := w.Mining(task)
if err != nil {
log.Fatalf("重新开启挖矿任务失败,请手动检查:%v", err)
return
}
} else {
w.db.FinishMiningTask(task)
}
}
}
// 获取 NVIDIA 显卡信息
func getNvidiaGPUInfo() (map[int]message.GPU, error) {
// 使用 nvidia-smi 列出所有 GPU
@@ -56,10 +182,10 @@ func parseMemory(memStr string) (float64, error) {
return mem, nil
}
// 获取 AMD 显卡信息
func getAmdGPUInfo() (map[int]message.GPU, error) {
// 使用 wmic 列出所有显卡信息
cmd := exec.Command("wmic", "path", "Win32_VideoController", "get", "Name,AdapterRAM")
// 获取其他显卡信息(如 Intel 或 AMD
func getOtherGPUInfo() (map[int]message.GPU, error) {
// Windows 下使用 wmic 命令获取显卡信息
cmd := exec.Command("wmic", "path", "win32_VideoController", "get", "name")
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("无法执行 wmic 命令: %v", err)
@@ -72,27 +198,29 @@ func getAmdGPUInfo() (map[int]message.GPU, error) {
gpus := make(map[int]message.GPU)
gpuIndex := 0
for _, line := range lines[1:] { // 跳过标题行
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
parts := strings.Fields(line)
if len(parts) >= 2 {
// 获取显存大小并转换为 MB
memStr := parts[1]
mem, err := parseMemoryFromBytes(memStr)
if err != nil {
mem = 0 // 如果显存无法解析,则设置为 0
}
// 用正确的品牌名填充
gpus[gpuIndex] = message.GPU{
Brand: "AMD",
Model: parts[0],
Mem: mem,
}
gpuIndex++
}
// 跳过标题行和空行
if line == "" || line == "Name" {
continue
}
// 判断显卡品牌
brand := "Unknown"
if strings.Contains(line, "NVIDIA") {
brand = "NVIDIA"
} else if strings.Contains(line, "AMD") || strings.Contains(line, "Radeon") {
brand = "AMD"
} else if strings.Contains(line, "Intel") {
brand = "Intel"
}
// 将 GPU 信息存储到 map 中
gpus[gpuIndex] = message.GPU{
Brand: brand,
Model: line,
Mem: 0, // wmic 无法直接获取显存,需要其他方法
}
gpuIndex++
}
if len(gpus) == 0 {
@@ -102,48 +230,351 @@ func getAmdGPUInfo() (map[int]message.GPU, error) {
return gpus, nil
}
// 从字节转换显存为 MB
func parseMemoryFromBytes(memStr string) (float64, error) {
// 将字节转换为 MB
memBytes, err := strconv.Atoi(memStr)
if err != nil {
return 0, fmt.Errorf("字节转显存失败: %v", err)
}
// 显存从字节转为MB
memMB := float64(memBytes) / (1024 * 1024)
return memMB, nil
}
// 获取所有 GPU 信息
func GetGPUInfo() (map[int]message.GPU, error) {
func (w *WindowsClient) GetGPUInfo() (map[int]message.GPU, error) {
// 尝试获取 NVIDIA GPU 信息
gpus, err := getNvidiaGPUInfo()
if err == nil {
return gpus, nil
}
// 尝试获取 AMD GPU 信息
gpus, err = getAmdGPUInfo()
if err == nil {
return gpus, nil
// 如果没有 NVIDIA 显卡,尝试获取其他类型显卡信息
return getOtherGPUInfo()
}
// Windows 版本:使用主机 UUID 作为“机器码”返回给上层(仍然复用 GetMACAddress 接口名)
// 通过 PowerShell 命令Get-WmiObject -Class Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID
func (w *WindowsClient) GetMACAddress() (string, error) {
// 注意:在 Go 中调用 PowerShell
cmd := exec.Command("powershell", "-Command", "Get-WmiObject -Class Win32_ComputerSystemProduct | Select-Object -ExpandProperty UUID")
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("获取主机 UUID 失败: %v", err)
}
return nil, fmt.Errorf("未找到任何显卡信息")
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
}
/*
{
"coin-algo": {
"lolminer": {
"pool_name": "url"
},
"bzminer": {
"pool_name": "url"
},
"rigel": {
"pool_name": "url"
}
}
}
配置lolminer
Windows 下使用 lolMiner.exe
*/
func (w *WindowsClient) lolminer(cfg message.ConfigurationMiningMsg) {
w.mu.Lock()
if w.Status != 2 {
log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp)
return
}
w.Status = 1
w.mu.Unlock()
var address string
if cfg.WalletMining {
address = cfg.WalletAddress
} else {
address = cfg.PoolUser
}
dir := w.MiningConfig.LolMinerPath
name := filepath.Join(dir, "lolMiner.exe")
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 := w.db.InsertMiningTask(cfg)
if err != nil {
log.Fatalf("本次挖矿任务记录失败:%v", err)
}
}()
// 获取 lolMiner 的进程 ID
fmt.Printf("lolMiner started with PID: %d\n", cmd.Process.Pid)
// 记录当前挖矿进程
w.mu.Lock()
w.currentProcess = &currentProcess{
process: cmd,
miner: "lolminer",
}
w.mu.Unlock()
// 获取当前时间戳(秒级)
currentTimestamp := time.Now().Unix()
endTimestamp := int64(cfg.EndTimestamp)
// 计算目标时间戳和当前时间戳之间的差值
if endTimestamp <= currentTimestamp {
// 如果目标时间已经到达,立即结束挖矿进程
fmt.Println("目标时间已经到达,立即结束 lolMiner 挖矿进程")
w.StopMining()
// 修改执行记录
go func() {
err := w.db.FinishMiningTask(cfg)
if err != nil {
log.Fatalf("修改执行记录失败:%v", err)
}
}()
} 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 统一停止挖矿进程
w.StopMining()
// 修改执行记录
go func() {
err := w.db.FinishMiningTask(cfg)
if err != nil {
log.Fatalf("修改执行记录失败:%v", err)
}
}()
}
log.Printf("当前挖矿任务已执行完毕:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp)
w.mu.Lock()
w.Status = 2
w.mu.Unlock()
}
/*
配置bzminer
Windows 下使用 bzminer.exe
*/
func (w *WindowsClient) bzminer(cfg message.ConfigurationMiningMsg) {
w.mu.Lock()
if w.Status != 2 {
log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp)
return
}
w.Status = 1
w.mu.Unlock()
var address string
if cfg.WalletMining {
address = cfg.WalletAddress
} else {
address = cfg.PoolUser
}
dir := w.MiningConfig.BzMinerPath
name := filepath.Join(dir, "bzminer.exe")
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 := w.db.InsertMiningTask(cfg)
if err != nil {
log.Fatalf("本次挖矿任务记录失败:%v", err)
}
}()
// 获取 bzminer 的进程 ID
fmt.Printf("bzminer started with PID: %d\n", cmd.Process.Pid)
// 记录当前挖矿进程
w.mu.Lock()
w.currentProcess = &currentProcess{
process: cmd,
miner: "bzminer",
}
w.mu.Unlock()
// 获取当前时间戳(秒级)
currentTimestamp := time.Now().Unix()
endTimestamp := int64(cfg.EndTimestamp)
// 计算目标时间戳和当前时间戳之间的差值
if endTimestamp <= currentTimestamp {
// 如果目标时间已经到达,立即结束挖矿进程
fmt.Println("目标时间已经到达,立即结束 bzminer 挖矿进程")
w.StopMining()
fmt.Println("bzminer process killed.")
} 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 统一停止挖矿进程
w.StopMining()
// 修改执行记录
go func() {
err := w.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)
w.mu.Lock()
w.Status = 2
w.mu.Unlock()
}
/*
配置rigel
Windows 下使用 rigel.exe
*/
func (w *WindowsClient) rigel(cfg message.ConfigurationMiningMsg) {
w.mu.Lock()
if w.Status != 2 {
log.Fatalf("当前还有挖矿任务正在进行中:币=%s, 算法=%s, 矿池=%s, 截止时间=%d", cfg.Coin, cfg.Algo, cfg.Pool, cfg.EndTimestamp)
return
}
w.Status = 1
w.mu.Unlock()
var address string
if cfg.WalletMining {
address = cfg.WalletAddress
} else {
address = cfg.PoolUser
}
dir := w.MiningConfig.RigelPath
name := filepath.Join(dir, "rigel.exe")
// 禁用 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 := w.db.InsertMiningTask(cfg)
if err != nil {
log.Fatalf("本次挖矿任务记录失败:%v", err)
}
}()
// 获取 rigel 的进程 ID
fmt.Printf("rigel started with PID: %d\n", cmd.Process.Pid)
// 记录当前挖矿进程
w.mu.Lock()
w.currentProcess = &currentProcess{
process: cmd,
miner: "rigel",
}
w.mu.Unlock()
// 获取当前时间戳(秒级)
currentTimestamp := time.Now().Unix()
endTimestamp := int64(cfg.EndTimestamp)
// 计算目标时间戳和当前时间戳之间的差值
if endTimestamp <= currentTimestamp {
// 如果目标时间已经到达,立即结束挖矿进程
fmt.Println("目标时间已经到达,立即结束 rigel 挖矿进程")
w.StopMining()
fmt.Println("rigel process killed.")
} 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 统一停止挖矿进程
w.StopMining()
// 修改执行记录
go func() {
err := w.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)
w.mu.Lock()
w.Status = 2
w.mu.Unlock()
}
// StopMining 主动终止当前挖矿进程
func (w *WindowsClient) StopMining() {
w.mu.Lock()
cp := w.currentProcess
w.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)
// Windows 下统一使用 Kill 结束进程
if err := cp.process.Process.Kill(); err != nil {
log.Printf("停止挖矿进程失败:%v", err)
}
// 清理状态
w.mu.Lock()
w.currentProcess = nil
w.Status = 2
w.mu.Unlock()
log.Println("当前挖矿任务已被手动停止")
}
// 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 (w *WindowsClient) Mining(cfg message.ConfigurationMiningMsg) error {
info := cfg.Coin + "-" + cfg.Algo
switch info {
case "SHA3X-SHA3X":
go w.lolminer(cfg) // SHA3X
case "XNA-KawPow":
go w.bzminer(cfg) // xna
case "CLORE-KawPow":
go w.bzminer(cfg) // clore
case "CFX-Octopus":
go w.rigel(cfg) // octopus
case "IRON-IronFish":
go w.lolminer(cfg)
case "NEXA-NexaPow":
go w.lolminer(cfg)
case "KLS-KarlsenHash":
go w.lolminer(cfg)
case "RVN-KawPow":
go w.bzminer(cfg) // rvn
case "ERG-Autolykos":
go w.bzminer(cfg) // ergo
case "XEL-Xelishashv2":
go w.rigel(cfg)
default:
return fmt.Errorf("不支持%s算法", info)
}
return nil
}

353
internal/sustain/proxy.go Normal file
View File

@@ -0,0 +1,353 @@
/*
持续挖矿
1在主配置文件中选择是否开启
2开启后将读取挖矿配置(mining.conf)
3根据配置启动相应的挖矿软件开始挖矿
4接收到tcp协议传送过来的新挖矿任务后停止当前挖矿并开启新的挖矿
5新挖矿任务到期或结束后重新开启挖矿
配置文件:算法、挖矿软件、挖矿配置(算法、钱包、矿工号、挖矿地址)
*/
package sustain
import (
message "client/internal/msg"
"client/internal/src"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"gopkg.in/ini.v1"
)
// SustainMiningConfig 持续挖矿配置
type SustainMiningConfig struct {
Enabled bool // 是否启用持续挖矿
Algo string // 算法
Coin string // 币种
Miner string // 挖矿软件 (lolminer/bzminer/rigel)
PoolUrl string // 矿池地址
Wallet string // 钱包地址
WorkerID string // 矿工号
PoolUser string // 矿池账号(可选)
WalletMining bool // 是否使用钱包挖矿
}
// SustainMiner 持续挖矿管理器
type SustainMiner struct {
mu sync.Mutex
config SustainMiningConfig
miningConfig message.MiningConfig
os *src.SystemServer
osName string
currentProcess *exec.Cmd // 当前挖矿进程
isRunning bool // 是否正在运行持续挖矿
isPaused bool // 是否被暂停(因为有新任务)
stopChan chan struct{} // 停止信号
}
// NewSustainMiner 创建持续挖矿管理器
func NewSustainMiner(os *src.SystemServer, osName string, miningConfig message.MiningConfig) *SustainMiner {
return &SustainMiner{
os: os,
osName: osName,
miningConfig: miningConfig,
stopChan: make(chan struct{}),
}
}
// LoadConfig 从配置文件加载持续挖矿配置
func (s *SustainMiner) LoadConfig() error {
var confFile string
if runtime.GOOS == "windows" {
confFile = "mining.windows.conf"
} else {
confFile = "mining.linux.conf"
}
cfg, err := ini.Load(confFile)
if err != nil {
return fmt.Errorf("读取配置文件失败: %v", err)
}
// 读取 [sustain] 部分
section := cfg.Section("sustain")
s.config.Enabled, _ = section.Key("enabled").Bool()
if !s.config.Enabled {
log.Println("持续挖矿未启用")
return nil
}
// 读取配置值,并去掉可能的引号
s.config.Algo = strings.Trim(section.Key("algo").String(), `"`)
s.config.Coin = strings.Trim(section.Key("coin").String(), `"`)
s.config.Miner = strings.Trim(section.Key("miner").String(), `"`)
s.config.PoolUrl = strings.Trim(section.Key("pool_url").String(), `"`)
s.config.Wallet = strings.Trim(section.Key("wallet").String(), `"`)
s.config.WorkerID = strings.Trim(section.Key("worker_id").String(), `"`)
s.config.PoolUser = strings.Trim(section.Key("pool_user").String(), `"`)
s.config.WalletMining, _ = section.Key("wallet_mining").Bool()
// 验证配置
if s.config.Algo == "" || s.config.Coin == "" || s.config.Miner == "" ||
s.config.PoolUrl == "" || s.config.Wallet == "" || s.config.WorkerID == "" {
return fmt.Errorf("持续挖矿配置不完整")
}
// 验证挖矿软件路径
switch strings.ToLower(s.config.Miner) {
case "lolminer":
if s.miningConfig.LolMinerPath == "" {
return fmt.Errorf("lolminer 路径未配置")
}
case "bzminer":
if s.miningConfig.BzMinerPath == "" {
return fmt.Errorf("bzminer 路径未配置")
}
case "rigel":
if s.miningConfig.RigelPath == "" {
return fmt.Errorf("rigel 路径未配置")
}
default:
return fmt.Errorf("不支持的挖矿软件: %s", s.config.Miner)
}
log.Printf("持续挖矿配置加载成功: 算法=%s, 币种=%s, 挖矿软件=%s", s.config.Algo, s.config.Coin, s.config.Miner)
return nil
}
// Start 启动持续挖矿
func (s *SustainMiner) Start() error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.config.Enabled {
return nil
}
if s.isRunning {
log.Println("持续挖矿已在运行中")
return nil
}
s.isRunning = true
s.isPaused = false
s.stopChan = make(chan struct{})
log.Println("启动持续挖矿...")
go s.run()
return nil
}
// Stop 停止持续挖矿
func (s *SustainMiner) Stop() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.isRunning {
return
}
log.Println("停止持续挖矿...")
close(s.stopChan)
s.isRunning = false
// 停止当前挖矿进程
if s.currentProcess != nil && s.currentProcess.Process != nil {
log.Println("停止当前挖矿进程...")
if runtime.GOOS == "linux" && s.config.Miner == "rigel" {
// Linux rigel 使用 Interrupt 信号
s.currentProcess.Process.Signal(os.Interrupt)
} else {
s.currentProcess.Process.Kill()
}
s.currentProcess = nil
}
}
// Pause 暂停持续挖矿(当有新任务时调用)
func (s *SustainMiner) Pause() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.isRunning || s.isPaused {
return
}
log.Println("暂停持续挖矿(有新任务)...")
s.isPaused = true
// 停止当前挖矿进程
if s.currentProcess != nil && s.currentProcess.Process != nil {
if runtime.GOOS == "linux" && s.config.Miner == "rigel" {
s.currentProcess.Process.Signal(os.Interrupt)
} else {
s.currentProcess.Process.Kill()
}
s.currentProcess = nil
}
}
// Resume 恢复持续挖矿(当任务结束后调用)
func (s *SustainMiner) Resume() {
s.mu.Lock()
defer s.mu.Unlock()
if !s.isRunning || !s.isPaused {
return
}
log.Println("恢复持续挖矿...")
s.isPaused = false
// 重新启动挖矿
go s.startMining()
}
// run 持续挖矿主循环
func (s *SustainMiner) run() {
for {
select {
case <-s.stopChan:
log.Println("持续挖矿已停止")
return
default:
s.mu.Lock()
paused := s.isPaused
s.mu.Unlock()
if !paused {
s.startMining()
// 等待挖矿进程结束(正常情况下不会结束,除非被停止)
if s.currentProcess != nil {
s.currentProcess.Wait()
}
} else {
// 如果被暂停,等待恢复
time.Sleep(1 * time.Second)
}
}
}
}
// startMining 启动挖矿进程
func (s *SustainMiner) startMining() {
s.mu.Lock()
defer s.mu.Unlock()
if s.isPaused {
return
}
var cmd *exec.Cmd
var err error
address := s.config.Wallet
if !s.config.WalletMining && s.config.PoolUser != "" {
address = s.config.PoolUser
}
switch strings.ToLower(s.config.Miner) {
case "lolminer":
cmd, err = s.startLolMiner(address)
case "bzminer":
cmd, err = s.startBzMiner(address)
case "rigel":
cmd, err = s.startRigel(address)
default:
log.Printf("不支持的挖矿软件: %s", s.config.Miner)
return
}
if err != nil {
log.Printf("启动持续挖矿失败: %v", err)
return
}
s.currentProcess = cmd
log.Printf("持续挖矿已启动: %s (PID: %d)", s.config.Miner, cmd.Process.Pid)
}
// startLolMiner 启动 lolminer
func (s *SustainMiner) startLolMiner(address string) (*exec.Cmd, error) {
dir := s.miningConfig.LolMinerPath
var name string
if runtime.GOOS == "windows" {
name = filepath.Join(dir, "lolMiner.exe")
} else {
name = filepath.Join(dir, "lolMiner")
}
args := []string{"--algo", s.config.Coin, "--pool", s.config.PoolUrl, "--user", address + "." + s.config.WorkerID}
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("启动持续挖矿 lolminer: %s %s", name, strings.Join(args, " "))
err := cmd.Start()
return cmd, err
}
// startBzMiner 启动 bzminer
func (s *SustainMiner) startBzMiner(address string) (*exec.Cmd, error) {
dir := s.miningConfig.BzMinerPath
var name string
if runtime.GOOS == "windows" {
name = filepath.Join(dir, "bzminer.exe")
} else {
name = filepath.Join(dir, "bzminer")
}
args := []string{"-a", s.config.Coin, "-w", address + "." + s.config.WorkerID, "-p", s.config.PoolUrl}
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("启动持续挖矿 bzminer: %s %s", name, strings.Join(args, " "))
err := cmd.Start()
return cmd, err
}
// startRigel 启动 rigel
func (s *SustainMiner) startRigel(address string) (*exec.Cmd, error) {
dir := s.miningConfig.RigelPath
var name string
if runtime.GOOS == "windows" {
name = filepath.Join(dir, "rigel.exe")
} else {
name = filepath.Join(dir, "rigel")
}
args := []string{"--no-watchdog", "-a", strings.ToLower(s.config.Algo), "-o", s.config.PoolUrl, "-u", address, "-w", s.config.WorkerID, "--log-file", "logs/miner.log"}
cmd := exec.Command(name, args...)
cmd.Dir = dir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
log.Printf("启动持续挖矿 rigel: %s %s", name, strings.Join(args, " "))
err := cmd.Start()
return cmd, err
}
// IsRunning 检查是否正在运行
func (s *SustainMiner) IsRunning() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.isRunning && !s.isPaused
}
// IsPaused 检查是否被暂停
func (s *SustainMiner) IsPaused() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.isPaused
}

255
internal/updater/updater.go Normal file
View File

@@ -0,0 +1,255 @@
package updater
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
)
// CheckAndUpdate 检查远程版本并更新
// remoteBaseURL: 远程服务器基础URL例如 "http://example.com/update"
// currentVersion: 当前版本号
// 返回是否需要重启(如果更新了文件)
func CheckAndUpdate(remoteBaseURL string, currentVersion string) (bool, error) {
// 先检查是否有待应用的更新Windows 下上次下载但未应用的新版本)
if runtime.GOOS == "windows" {
exePath, err := os.Executable()
if err == nil {
exeDir := filepath.Dir(exePath)
updateFlag := filepath.Join(exeDir, ".update_pending")
if data, err := os.ReadFile(updateFlag); err == nil {
newPath := strings.TrimSpace(string(data))
if err := applyPendingUpdate(newPath, exePath); err == nil {
os.Remove(updateFlag)
log.Println("已应用待处理的更新")
return true, nil
}
}
}
}
// 获取远程版本号
remoteVersion, err := fetchRemoteVersion(remoteBaseURL)
if err != nil {
log.Printf("获取远程版本失败:%v", err)
return false, err
}
log.Printf("当前版本:%s远程版本%s", currentVersion, remoteVersion)
// 比较版本
if strings.TrimSpace(remoteVersion) == strings.TrimSpace(currentVersion) {
log.Println("版本一致,无需更新")
return false, nil
}
log.Printf("检测到新版本:%s开始下载更新...", remoteVersion)
// 下载新的可执行文件
remoteExeName := getRemoteExecutableName() // 远程文件名
localExeName := getLocalExecutableName() // 本地可执行文件名
remoteExeURL := fmt.Sprintf("%s/%s", remoteBaseURL, remoteExeName)
err = downloadAndReplace(remoteExeURL, localExeName)
if err != nil {
log.Printf("下载更新失败:%v", err)
return false, err
}
log.Printf("更新成功!新版本:%s", remoteVersion)
return true, nil
}
// fetchRemoteVersion 从远程获取版本号
func fetchRemoteVersion(remoteBaseURL string) (string, error) {
versionURL := fmt.Sprintf("%s/current_version", remoteBaseURL)
resp, err := http.Get(versionURL)
if err != nil {
return "", fmt.Errorf("请求远程版本文件失败:%v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("获取远程版本文件失败,状态码:%d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取远程版本文件失败:%v", err)
}
return strings.TrimSpace(string(body)), nil
}
// downloadAndReplace 下载并替换可执行文件
func downloadAndReplace(remoteURL string, exeName string) error {
// 获取当前可执行文件的路径
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取可执行文件路径失败:%v", err)
}
exeDir := filepath.Dir(exePath)
newExePath := filepath.Join(exeDir, exeName)
backupPath := exePath + ".backup"
// 1. 备份当前可执行文件
log.Printf("备份当前可执行文件到:%s", backupPath)
err = copyFile(exePath, backupPath)
if err != nil {
return fmt.Errorf("备份文件失败:%v", err)
}
// 2. 下载新的可执行文件
log.Printf("从 %s 下载新版本...", remoteURL)
resp, err := http.Get(remoteURL)
if err != nil {
// 如果下载失败,恢复备份
restoreBackup(backupPath, exePath)
return fmt.Errorf("下载新版本失败:%v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
restoreBackup(backupPath, exePath)
return fmt.Errorf("下载新版本失败,状态码:%d", resp.StatusCode)
}
// 3. 创建临时文件
tempPath := newExePath + ".tmp"
out, err := os.Create(tempPath)
if err != nil {
restoreBackup(backupPath, exePath)
return fmt.Errorf("创建临时文件失败:%v", err)
}
defer out.Close()
// 4. 写入新文件
_, err = io.Copy(out, resp.Body)
if err != nil {
os.Remove(tempPath)
restoreBackup(backupPath, exePath)
return fmt.Errorf("写入新文件失败:%v", err)
}
out.Close()
// 5. 设置可执行权限Linux
if runtime.GOOS != "windows" {
err = os.Chmod(tempPath, 0755)
if err != nil {
os.Remove(tempPath)
restoreBackup(backupPath, exePath)
return fmt.Errorf("设置可执行权限失败:%v", err)
}
}
// 6. 尝试替换原文件
// Windows 下如果文件正在运行,无法直接替换,需要特殊处理
if runtime.GOOS == "windows" {
// Windows: 先尝试直接替换,如果失败则保存为 .new 文件,下次启动时替换
err = os.Rename(tempPath, exePath)
if err != nil {
// 如果替换失败(文件正在使用),保存为 .new 文件
newPath := exePath + ".new"
err = os.Rename(tempPath, newPath)
if err != nil {
os.Remove(tempPath)
restoreBackup(backupPath, exePath)
return fmt.Errorf("保存新版本文件失败:%v", err)
}
log.Printf("当前程序正在运行,新版本已保存为:%s程序重启后将自动应用更新", newPath)
// 创建更新标记文件,下次启动时检测并应用
updateFlag := filepath.Join(exeDir, ".update_pending")
os.WriteFile(updateFlag, []byte(newPath), 0644)
return nil
}
} else {
// Linux: 直接替换
err = os.Rename(tempPath, exePath)
if err != nil {
os.Remove(tempPath)
restoreBackup(backupPath, exePath)
return fmt.Errorf("替换文件失败:%v", err)
}
}
// 7. 清理备份文件(可选,更新成功后删除)
// os.Remove(backupPath)
log.Println("文件更新完成")
return nil
}
// copyFile 复制文件
func copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return err
}
// 复制文件权限
sourceInfo, err := os.Stat(src)
if err == nil {
os.Chmod(dst, sourceInfo.Mode())
}
return nil
}
// restoreBackup 恢复备份文件
func restoreBackup(backupPath, exePath string) {
if _, err := os.Stat(backupPath); err == nil {
log.Printf("恢复备份文件:%s -> %s", backupPath, exePath)
os.Rename(backupPath, exePath)
}
}
// applyPendingUpdate 应用待处理的更新Windows 下使用)
func applyPendingUpdate(newPath, exePath string) error {
if _, err := os.Stat(newPath); os.IsNotExist(err) {
return fmt.Errorf("新版本文件不存在:%s", newPath)
}
// 尝试替换
err := os.Rename(newPath, exePath)
if err != nil {
return fmt.Errorf("应用更新失败:%v可能程序仍在运行", err)
}
log.Printf("成功应用更新:%s -> %s", newPath, exePath)
return nil
}
// getRemoteExecutableName 根据操作系统获取远程可执行文件名(用于下载)
func getRemoteExecutableName() string {
if runtime.GOOS == "windows" {
return "client_windows.exe"
}
return "client_linux"
}
// getLocalExecutableName 根据操作系统获取本地可执行文件名(用于替换)
func getLocalExecutableName() string {
if runtime.GOOS == "windows" {
return "client.exe"
}
return "client"
}

View File

@@ -0,0 +1,12 @@
//go:build linux
package utils
import "os"
// IsAdmin 检测当前用户是否为 rootUID=0
func IsAdmin() bool {
return os.Geteuid() == 0
}

View File

@@ -0,0 +1,20 @@
//go:build windows
package utils
import (
"os/exec"
)
// IsAdmin 检测当前用户是否为管理员。
// 通过执行 `net session` 命令判断:该命令只有在管理员权限下才会成功。
func IsAdmin() bool {
cmd := exec.Command("net", "session")
// 不关心输出,只关心能否成功执行
if err := cmd.Run(); err != nil {
return false
}
return true
}

View File

@@ -1,475 +0,0 @@
{
"lolminer": {
"Alephium": {
"coins": [
"ALPH"
],
"fee": 0.75
},
"Autolykos V2": {
"coins": [
"ERG"
],
"fee": 1.5
},
"BeamHash III": {
"coins": [
"BEAM"
],
"fee": 1.0
},
"Cuckoo 29": {
"coins": [
"GRIN-C29"
],
"fee": 2.0
},
"CuckarooD 29": {
"coins": [
"GRIN-CD29"
],
"fee": 2.0
},
"CuckarooM 29": {
"coins": [
"GRIN-CM29"
],
"fee": 2.0
},
"Cuckaroo 30 CTXC": {
"coins": [
"CTXC"
],
"fee": 2.5
},
"Cuckatoo 31": {
"coins": [
"GRIN-C31"
],
"fee": 2.0
},
"Cuckatoo 32": {
"coins": [
"GRIN-C32"
],
"fee": 2.0
},
"Cuckaroo 29-32": {
"coins": [
"GRIN-C29-32"
],
"fee": 1.0
},
"Cuckaroo 29-40": {
"coins": [
"GRIN-C29-40"
],
"fee": 1.0
},
"Dual ETC + (KAS/ALEPH)": {
"coins": [
"ETC",
"ALPH/KAS"
],
"fee": "1.0 / 0.0"
},
"Dual ETH (ETHW) + (KAS/ALEPH)": {
"coins": [
"ETHW",
"ALPH/KAS"
],
"fee": "1.0 / 0.0"
},
"Dual RTH + (ALEPH/GRAM/KLS/PYI/RXD)": {
"coins": [
"RTH",
"ALPH/GRAM/KLS/PYI/RXD"
],
"fee": "1.0 / 0.75 - 1.0"
},
"Dual IRON + (ALEPH/GRAM/KLS/PYI/RXD)": {
"coins": [
"IRON",
"ALPH/GRAM/KLS/PYI/RXD"
],
"fee": "1.0 / 0.75 - 1.0"
},
"Equihash 144/5": {
"coins": [
"EQUIHASH144/5"
],
"fee": 1.0
},
"Equihash 192/7": {
"coins": [
"EQUIHASH192/7"
],
"fee": 1.0
},
"Equihash 210/9": {
"coins": [
"EQUIHASH210/9"
],
"fee": 1.0
},
"Etchash": {
"coins": [
"ETC"
],
"fee": 0.7
},
"Ethash (ETHW)": {
"coins": [
"ETHW"
],
"fee": 0.7
},
"Ironfish": {
"coins": [
"IRON"
],
"fee": 1.0
},
"Kaspa": {
"coins": [
"KAS"
],
"fee": 0.75
},
"Karlsen": {
"coins": [
"KLS"
],
"fee": 1.0
},
"Nexa": {
"coins": [
"NEXA"
],
"fee": 2.0
},
"Pyrin": {
"coins": [
"PYI"
],
"fee": 1.0
},
"Radiant": {
"coins": [
"RAD"
],
"fee": 0.75
},
"Rethereum": {
"coins": [
"RTH"
],
"fee": 1.0
},
"TON/GRAM": {
"coins": [
"TON",
"GRAM"
],
"fee": 1.0
},
"ZelHash (Flux)": {
"coins": [
"FLUX"
],
"fee": "1.0 / 1.5"
}
},
"bzminer": {
"aidepin": {
"coins": [
"AI-DEPIN"
],
"fee": 1.0
},
"aipg": {
"coins": [
"AIPG"
],
"fee": 1.0
},
"alph": {
"coins": [
"ALPH"
],
"fee": 0.5
},
"blocx": {
"coins": [
"BLOX"
],
"fee": 1.0
},
"clore": {
"coins": [
"CLORE"
],
"fee": 1.0
},
"canxium": {
"coins": [
"CANXIUM"
],
"fee": 0.5
},
"dynex": {
"coins": [
"DNX"
],
"fee": 2.0
},
"ergo": {
"coins": [
"ERG"
],
"fee": 1.0
},
"etchash": {
"coins": [
"ETC"
],
"fee": 0.5
},
"ethw": {
"coins": [
"ETHW"
],
"fee": 0.5
},
"gamepass": {
"coins": [
"GAMEPASS"
],
"fee": 1.0
},
"ironfish": {
"coins": [
"IRON"
],
"fee": 1.0
},
"ixi": {
"coins": [
"IXI"
],
"fee": 1.0
},
"karlsen": {
"coins": [
"KLS"
],
"fee": 1.0
},
"kaspa": {
"coins": [
"KAS"
],
"fee": 1.0
},
"neoxa": {
"coins": [
"NEOX"
],
"fee": 1.0
},
"nexa": {
"coins": [
"NEXA"
],
"fee": 2.0
},
"novo": {
"coins": [
"NOVO"
],
"fee": 1.0
},
"meowcoin": {
"coins": [
"MEOW"
],
"fee": 1.0
},
"octa": {
"coins": [
"OCTA"
],
"fee": 0.5
},
"olhash": {
"coins": [
"OLHASH"
],
"fee": 1.0
},
"radiant": {
"coins": [
"RAD"
],
"fee": 1.0
},
"rethereum": {
"coins": [
"RTH"
],
"fee": 1.0
},
"rvn": {
"coins": [
"RVN"
],
"fee": 1.0
},
"warthog": {
"coins": [
"WARTHOG"
],
"fee": 2.0
},
"woodcoin": {
"coins": [
"WOOD"
],
"fee": 1.0
},
"xna": {
"coins": [
"XNA"
],
"fee": 1.0
},
"zil": {
"coins": [
"ZIL"
],
"fee": 0.0
}
},
"rigel": {
"abelian": {
"coins": [
"ABEL"
],
"fee": 1.0
},
"alephium": {
"coins": [
"ALPH"
],
"fee": 0.7
},
"autolykos2": {
"coins": [
"ERG"
],
"fee": 1.0
},
"etchash": {
"coins": [
"ETC"
],
"fee": 0.7
},
"ethash": {
"coins": [
"ETHW",
"XPB",
"OCTA"
],
"fee": 0.7
},
"ethashb3": {
"coins": [
"HYP"
],
"fee": 1.0
},
"fishhash": {
"coins": [
"IRON"
],
"fee": 1.0
},
"karlsenhashv2": {
"coins": [
"KLS"
],
"fee": 1.0
},
"kawpow": {
"coins": [
"RVN",
"AIPG",
"XNA",
"CLORE",
"NEOX"
],
"fee": 1.0
},
"nexapow": {
"coins": [
"NEXA"
],
"fee": 2.0
},
"octopus": {
"coins": [
"CFX"
],
"fee": 2.0
},
"progpowz": {
"coins": [
"ZANO"
],
"fee": 1.0
},
"quai": {
"coins": [
"QUAI"
],
"fee": 1.0
},
"sha256ton": {
"coins": [
"GRAM"
],
"fee": 1.0
},
"sha3x": {
"coins": [
"XTM"
],
"fee": 1.0
},
"sha512256d": {
"coins": [
"RXD"
],
"fee": 1.0
},
"xelishash": {
"coins": [
"XEL"
],
"fee": 3.0
},
"xelishashv2": {
"coins": [
"XEL"
],
"fee": 2.0
},
"zil": {
"coins": [
"ZIL"
],
"fee": 0.0
}
}
}

View File

View File

@@ -1,46 +0,0 @@
{
"id": "2060 Super X8\n.03000200-0400-0500-0006-000700080009",
"method": "auth.machineCode",
"params": {
"0": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"1": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"2": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"3": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"4": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"5": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"6": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
},
"7": {
"brand": "NVIDIA",
"model": "NVIDIA GeForce RTX 2060 SUPER",
"mem": 8192
}
}
}

Binary file not shown.

View File

@@ -1,70 +0,0 @@
str := "#!/bin/bash\n"
str += "POOL=" + cfg.PoolUrl + "\n"
str += "WALLET=" + cfg.WalletAddress + "." + cfg.WorkerID + "\n"
str += "ALGO=" + cfg.Algo + "\n"
str += "END_TIMESTAMP=" + strconv.FormatUint(cfg.EndTimestamp, 10) + "\n"
str += "\n"
str += "# 在后台启动挖矿程序\n"
str += "./lolMiner --algo $ALGO --pool $POOL --user $WALLET $@ &\n"
str += "MINER_PID=$!\n"
str += "\n"
str += "# 循环检查时间戳,直到超过结束时间\n"
str += "while true; do\n"
str += " CURRENT_TIMESTAMP=$(date +%s)\n"
str += " if [ $CURRENT_TIMESTAMP -ge $END_TIMESTAMP ]; then\n"
str += " echo \"时间戳已超过结束时间,退出挖矿脚本\"\n"
str += " kill $MINER_PID 2>/dev/null\n"
str += " wait $MINER_PID 2>/dev/null\n"
str += " exit 0\n"
str += " fi\n"
str += " sleep 10\n"
str += "done\n"
str := "#!/bin/bash\n"
str += "POOL=" + cfg.PoolUrl + "\n"
str += "WALLET=" + cfg.WalletAddress + "." + cfg.WorkerID + "\n"
str += "ALGO=" + cfg.Algo + "\n"
str += "END_TIMESTAMP=" + strconv.FormatUint(cfg.EndTimestamp, 10) + "\n"
str += "\n"
str += "# 在后台启动挖矿程序\n"
str += "./bzminer -a $ALGO -w $WALLET -p $POOL\n"
str += "MINER_PID=$!\n"
str += "\n"
str += "# 循环检查时间戳,直到超过结束时间\n"
str += "while true; do\n"
str += " CURRENT_TIMESTAMP=$(date +%s)\n"
str += " if [ $CURRENT_TIMESTAMP -ge $END_TIMESTAMP ]; then\n"
str += " echo \"时间戳已超过结束时间,退出挖矿脚本\"\n"
str += " kill $MINER_PID 2>/dev/null\n"
str += " wait $MINER_PID 2>/dev/null\n"
str += " exit 0\n"
str += " fi\n"
str += " sleep 10\n"
str += "done\n"
str := "#!/bin/bash\n"
str += "POOL=" + cfg.PoolUrl + "\n"
str += "WALLET=" + cfg.WalletAddress + "\n"
str += "USER=" + cfg.WorkerID + "\n"
str += "ALGO=" + cfg.Algo + "\n"
str += "END_TIMESTAMP=" + strconv.FormatUint(cfg.EndTimestamp, 10) + "\n"
str += "\n"
str += "# 在后台启动挖矿程序\n"
str += "./rigel -a $ALGO -o $POOL -u $WALLET -w $USER --log-file logs/miner.log\n"
str += "MINER_PID=$!\n"
str += "\n"
str += "# 循环检查时间戳,直到超过结束时间\n"
str += "while true; do\n"
str += " CURRENT_TIMESTAMP=$(date +%s)\n"
str += " if [ $CURRENT_TIMESTAMP -ge $END_TIMESTAMP ]; then\n"
str += " echo \"时间戳已超过结束时间,退出挖矿脚本\"\n"
str += " kill $MINER_PID 2>/dev/null\n"
str += " wait $MINER_PID 2>/dev/null\n"
str += " exit 0\n"
str += " fi\n"
str += " sleep 10\n"
str += "done\n"

View File

@@ -1,12 +0,0 @@
#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释
[bzminer]
# path=/path/bzminer
[lolminer]
# path=/home/a11/桌面/lolminer/1.98
[rigel]
# path=/path/rigel
#如果您的网络无法直接连通各个矿池需要使用各大矿池专用网咯请打开proxy的注释
#打开此注释后会使用各大矿池的专用网络每笔订单额外增加1%的网络费用
[proxy]
# proxy=true

View File

@@ -1,13 +0,0 @@
#请确认您的主机上安装了下列挖矿软件,确认后可以打开注释,并修改其路径,如果没有安装,请勿打开注释
#请使用双\\,否则可能无法解析出准确的路径
[bzminer]
# path=C:\\path\\bzminer
[lolminer]
# path=C:\\path\\lolminer
[rigel]
# path=C:\\path\\rigel
#如果您的网络无法直接连通各个矿池需要使用各大矿池专用网咯请打开proxy的注释
#打开此注释后会使用各大矿池的专用网络每笔订单额外增加1%的网络费用
[proxy]
# proxy=true

View File

@@ -1,108 +0,0 @@
{
"DxPool": {
"wallet_mining": false,
"coins": {"GPU": ["XTM", "ERG"], "ASIC": []},
"XTM": {"full_name": "Tari(XTM)", "algos": ["SHA3X"], "pay_interval": 24, "min_pay": 100, "model_fee": {"PROP": 0.03}, "mining_url": {"tcp": {"GPU": "xtm.ss.dxpool.com:3301", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos2"], "pay_interval": "10:00~12:00 (UTC+8)", "min_pay": 1, "model_fee": {"PPS": 0.025}, "mining_url": {"tcp": {"GPU": "erg.ss.dxpool.com:8888", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"pool.kryptex": {
"wallet_mining": true,
"coins": {"GPU": ["XTM", "XNA", "CLORE", "CFX", "IRON", "NEXA", "KLS", "RVN", "ERG", "XEL"], "ASIC": []},
"XTM": {"full_name": "Tari(XTM)", "algos": ["SHA3X"], "pay_interval": -1, "min_pay": 200, "model_fee": {"PROP": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "xtm-sha3x-sg.kryptex.network:7039", "ASIC": ""}, "ssl": {"GPU": "xtm-sha3x-sg.kryptex.network:8039", "ASIC": ""}}, "hashrate_url": ""},
"XNA": {"full_name": "Neurai", "algos": ["KawPow"], "pay_interval": -1, "min_pay": 100, "model_fee": {"PROP": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "xna-sg.kryptex.network:7024", "ASIC": ""}, "ssl": {"GPU": "xna-sg.kryptex.network:8024", "ASIC": ""}}, "hashrate_url": ""},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPow"], "pay_interval": -1, "min_pay": 5, "model_fee": {"PROP": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "clore-sg.kryptex.network:7025", "ASIC": ""}, "ssl": {"GPU": "clore-sg.kryptex.network:8025", "ASIC": ""}}, "hashrate_url": ""},
"CFX": {"full_name": "Conflux", "algos": ["Octopus"], "pay_interval": -1, "min_pay": 1, "model_fee": {"PPS+": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "cfx-sg.kryptex.network:7027", "ASIC": ""}, "ssl": {"GPU": "cfx-sg.kryptex.network:8027", "ASIC": ""}}, "hashrate_url": ""},
"IRON": {"full_name": "Iron Fish", "algos": ["IronFish"], "pay_interval": -1, "min_pay": 0.1, "model_fee": {"PPS+": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "iron-sg.kryptex.network:7017", "ASIC": ""}, "ssl": {"GPU": "iron-sg.kryptex.network:8017", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": -1, "min_pay": 20000, "model_fee": {"PPS+": 0.03, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "nexa-sg.kryptex.network:7026", "ASIC": ""}, "ssl": {"GPU": "nexa-sg.kryptex.network:8026", "ASIC": ""}}, "hashrate_url": ""},
"KLS": {"full_name": "Karlsen", "algos": ["KarlsenHash"], "pay_interval": -1, "min_pay": 10, "model_fee": {"PROP": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "kls-sg.kryptex.network:7022", "ASIC": ""}, "ssl": {"GPU": "kls-sg.kryptex.network:8022", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["KawPow"], "pay_interval": -1, "min_pay": 10, "model_fee": {"PPS+": 0.01}, "mining_url": {"tcp": {"GPU": "rvn-sg.kryptex.network:7031", "ASIC": ""}, "ssl": {"GPU": "rvn-sg.kryptex.network:8031", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos"], "pay_interval": -1, "min_pay": 1, "model_fee": {"PPS+": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "erg-sg.kryptex.network:7021", "ASIC": ""}, "ssl": {"GPU": "erg-sg.kryptex.network:8021", "ASIC": ""}}, "hashrate_url": ""},
"XEL": {"full_name": "Xelis", "algos": ["Xelishashv2"], "pay_interval": -1, "min_pay": 0.1, "model_fee": {"PROP": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "xel-sg.kryptex.network:7019", "ASIC": ""}, "ssl": {"GPU": "xel-sg.kryptex.network:8019", "ASIC": ""}}, "hashrate_url": ""}
},
"2miners": {
"wallet_mining": true,
"coins": {"GPU": ["XNA", "CLORE", "NEXA", "RVN", "ERG"], "ASIC": []},
"XNA": {"full_name": "Neurai", "algos": ["KawPOW"], "pay_interval": 2, "min_pay": 1000, "model_fee": {"PPLNS": 0.01, "SOLO": 0.015}, "mining_url": {"tcp": {"GPU": "xna.2miners.com:6060", "ASIC": ""}, "ssl": {"GPU": "xna.2miners.com:16060", "ASIC": ""}}, "hashrate_url": ""},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPOW"], "pay_interval": 2, "min_pay": 10, "model_fee": {"PPLNS": 0.01, "SOLO": 0.015}, "mining_url": {"tcp": {"GPU": "clore.2miners.com:2020", "ASIC": ""}, "ssl": {"GPU": "clore.2miners.com:12020", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": 2, "min_pay": 50000, "model_fee": {"PPLNS": 0.01, "SOLO": 0.015}, "mining_url": {"tcp": {"GPU": "nexa.2miners.com:5050", "ASIC": ""}, "ssl": {"GPU": "nexa.2miners.com:15050", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["KawPOW"], "pay_interval": 2, "min_pay": 10, "model_fee": {"PPLNS": 0.01, "SOLO": 0.015}, "mining_url": {"tcp": {"GPU": "asia-rvn.2miners.com:6060", "ASIC": ""}, "ssl": {"GPU": "asia-rvn.2miners.com:16060", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos"], "pay_interval": 2, "min_pay": 1, "model_fee": {"PPLNS": 0.01, "SOLO": 0.015}, "mining_url": {"tcp": {"GPU": "asia-erg.2miners.com:8888", "ASIC": ""}, "ssl": {"GPU": "asia-erg.2miners.com:18888", "ASIC": ""}}, "hashrate_url": ""}
},
"vipor.net": {
"wallet_mining": true,
"coins": {"GPU": ["XNA", "CLORE", "NEXA", "XEL"], "ASIC": []},
"XNA": {"full_name": "Neurai", "algos": ["KawPOW"], "pay_interval": 1, "min_pay": 1, "model_fee": {"PPLNS": 0.008, "SOLO": 0.008}, "mining_url": {"tcp": {"GPU": "cn.vipor.net:5090", "ASIC": ""}, "ssl": {"GPU": "cn.vipor.net:5190", "ASIC": ""}}, "hashrate_url": ""},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPOW"], "pay_interval": 1, "min_pay": 1, "model_fee": {"PPLNS": 0.008, "SOLO": 0.008}, "mining_url": {"tcp": {"GPU": "cn.vipor.net:5030", "ASIC": ""}, "ssl": {"GPU": "cn.vipor.net:5130", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": 1, "min_pay": 5000, "model_fee": {"PPLNS": 0.01, "SOLO": 0.01}, "mining_url": {"tcp": {"GPU": "cn.vipor.net:5084", "ASIC": ""}, "ssl": {"GPU": "cn.vipor.net:5184", "ASIC": ""}}, "hashrate_url": ""},
"XEL": {"full_name": "Xelis", "algos": ["XelisHashV2"], "pay_interval": 1, "min_pay": 0.05, "model_fee": {"PPLNS": 0.008, "SOLO": 0.008}, "mining_url": {"tcp": {"GPU": "cn.vipor.net:5077", "ASIC": ""}, "ssl": {"GPU": "cn.vipor.net:5177", "ASIC": ""}}, "hashrate_url": ""}
},
"herominers": {
"wallet_mining": true,
"coins": {"GPU": ["CLORE", "CFX", "IRON", "KLS", "RVN", "ERG", "XEL"], "ASIC": []},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPow"], "pay_interval": 1, "min_pay": 10, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.clore.herominers.com:1163", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"CFX": {"full_name": "Conflux", "algos": ["Octopus"], "pay_interval": 1, "min_pay": 1, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.conflux.herominers.com:1170", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"IRON": {"full_name": "Iron Fish", "algos": ["FishHash"], "pay_interval": 1, "min_pay": 0.05, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.ironfish.herominers.com:1145", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"KLS": {"full_name": "Karlsen", "algos": ["KarlsenHashv2"], "pay_interval": 1, "min_pay": 1, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.karlsen.herominers.com:1195", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["KawPow"], "pay_interval": 1, "min_pay": 5, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.ravencoin.herominers.com:1140", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos v2"], "pay_interval": 1, "min_pay": 0.5, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.ergo.herominers.com:1180", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"XEL": {"full_name": "Xelis", "algos": ["Xelishashv2"], "pay_interval": 1, "min_pay": 0.1, "model_fee": {"PROP": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "hk.xelis.herominers.com:1225", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"rplant.xyz": {
"wallet_mining": true,
"coins": {"GPU": ["CLORE", "NEXA"], "ASIC": []},
"CLORE": {"full_name": "Clore.ai", "algos": ["kawpow"], "pay_interval": 1, "min_pay": 1, "model_fee": {"PROP": 0.01, "SOLO": 0.02}, "mining_url": {"tcp": {"GPU": "asia.rplant.xyz:17083", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["nexapow"], "pay_interval": 2, "min_pay": 5000, "model_fee": {"PROP": 0.009, "SOLO": 0.018}, "mining_url": {"tcp": {"GPU": "asia.rplant.xyz:17092", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"f2pool": {
"wallet_mining": false,
"coins": {"GPU": ["CFX", "NEXA"], "ASIC": []},
"CFX": {"full_name": "Conflux", "algos": ["Octopus"], "pay_interval": "00:00--08:00 UTC", "min_pay": 1, "model_fee": {"PPLNS": 0.01}, "mining_url": {"tcp": {"GPU": "cfx.f2pool.com:6800", "ASIC": ""}, "ssl": {"GPU": "cfxssl.f2pool.com:6820", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": "00:00--08:00 UTC", "min_pay": 50000, "model_fee": {"PPLNS": 0.01}, "mining_url": {"tcp": {"GPU": "nexa.f2pool.com:3400", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"ntminerpool": {
"wallet_mining": true,
"coins": {"GPU": ["CLORE", "CFX", "IRON", "NEXA", "KLS", "RVN", "ERG"], "ASIC": []},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPow"], "pay_interval": 24, "min_pay": 10, "model_fee": {"PPLNS": 0}, "mining_url": {"tcp": {"GPU": "clore.ntminer.vip:13688", "ASIC": ""}, "ssl": {"GPU": "clore.ntminer.vip:13699", "ASIC": ""}}, "hashrate_url": ""},
"CFX": {"full_name": "Conflux", "algos": ["Octopus"], "pay_interval": 24, "min_pay": 100, "model_fee": {"PPS+": 0.005}, "mining_url": {"tcp": {"GPU": "cfx.ntminer.vip:26060", "ASIC": ""}, "ssl": {"GPU": "cfx.ntminer.vip:25050", "ASIC": ""}}, "hashrate_url": ""},
"IRON": {"full_name": "IronFish", "algos": ["FishHash"], "pay_interval": 24, "min_pay": 0.05, "model_fee": {"PPS+": 0}, "mining_url": {"tcp": {"GPU": "iron.ntminer.vip:9688", "ASIC": ""}, "ssl": {"GPU": "iron.ntminer.vip:9699", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": 24, "min_pay": 50000, "model_fee": {"PPLNS": 0}, "mining_url": {"tcp": {"GPU": "nexa.ntminer.vip:14688", "ASIC": ""}, "ssl": {"GPU": "nexa.ntminer.vip:14699", "ASIC": ""}}, "hashrate_url": ""},
"KLS": {"full_name": "Karlsen", "algos": ["KarlsenHashv2"], "pay_interval": 24, "min_pay": 10, "model_fee": {"PPLNS": 0}, "mining_url": {"tcp": {"GPU": "kls.ntminer.vip:8699", "ASIC": ""}, "ssl": {"GPU": "kls.ntminer.vip:8688", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["KawPow"], "pay_interval": 24, "min_pay": 10, "model_fee": {"PPS+": 0}, "mining_url": {"tcp": {"GPU": "rvn.ntminer.vip:22020", "ASIC": ""}, "ssl": {"GPU": "rvn.ntminer.vip:21010", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos2"], "pay_interval": 24, "min_pay": 1, "model_fee": {"PPS+": 0}, "mining_url": {"tcp": {"GPU": "ergo.ntminer.vip:24040", "ASIC": ""}, "ssl": {"GPU": "ergo.ntminer.vip:23030", "ASIC": ""}}, "hashrate_url": ""}
},
"woolypooly": {
"wallet_mining": true,
"coins": {"GPU": ["XNA","CLORE","CFX","NEXA","KLS","RVN","ERG","XEL"], "ASIC": []},
"XNA": {"full_name": "Neurai", "algos": ["KawPow"], "pay_interval": -1, "min_pay": 1000, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3128", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3128", "ASIC": ""}}, "hashrate_url": ""},
"CLORE": {"full_name": "Clore.ai", "algos": ["KawPow"], "pay_interval": -1, "min_pay": 10, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3126", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3126", "ASIC": ""}}, "hashrate_url": ""},
"CFX": {"full_name": "Conflux", "algos": ["Octopus"], "pay_interval": -1, "min_pay": 1, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3094", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3094", "ASIC": ""}}, "hashrate_url": ""},
"NEXA": {"full_name": "Nexa", "algos": ["NexaPow"], "pay_interval": -1, "min_pay": 50000, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3124", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3124", "ASIC": ""}}, "hashrate_url": ""},
"KLS": {"full_name": "Karlsen", "algos": ["KarlsenV2"], "pay_interval": -1, "min_pay": 25, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3132", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3132", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["kawpow"], "pay_interval": -1, "min_pay": 5, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:55555", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:55555", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos"], "pay_interval": -1, "min_pay": 1, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3100", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3100", "ASIC": ""}}, "hashrate_url": ""},
"XEL": {"full_name": "Xelis", "algos": ["Xelishash"], "pay_interval": -1, "min_pay": 0.1, "model_fee": {"PPLNS": 0.009, "SOLO": 0.009}, "mining_url": {"tcp": {"GPU": "pool.zh.woolypooly.com:3150", "ASIC": ""}, "ssl": {"GPU": "pool.zh.woolypooly.com:3150", "ASIC": ""}}, "hashrate_url": ""}
},
"K1pool": {
"wallet_mining": false,
"coins": {"GPU": ["CLORE", "RVN", "ERG", "XEL"], "ASIC": []},
"CLORE": {"full_name": "CLORE", "algos": ["KAWPOW"], "pay_interval": 0.3, "min_pay": 100, "model_fee": {"PPLNS": 0.01, "SOLO":0.01}, "mining_url": {"tcp": {"GPU": "cn.clore.k1pool.com:5030", "ASIC": ""}, "ssl": {"GPU": "cn.clore.k1pool.com:5030", "ASIC": ""}}, "hashrate_url": ""},
"RVN": {"full_name": "Ravencoin", "algos": ["KAWPOW"], "pay_interval": 0.3, "min_pay": 100, "model_fee": {"PPLNS": 0.01, "SOLO":0.01}, "mining_url": {"tcp": {"GPU": "cn.rvn.k1pool.com:7861", "ASIC": ""}, "ssl": {"GPU": "cn.rvn.k1pool.com:7861", "ASIC": ""}}, "hashrate_url": ""},
"ERG": {"full_name": "Ergo", "algos": ["Autolykos2"], "pay_interval": 0.3, "min_pay": 2, "model_fee": {"PPLNS": 0.01, "SOLO":0.01}, "mining_url": {"tcp": {"GPU": "cn.erg.k1pool.com:3746", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""},
"XEL": {"full_name": "Xelis", "algos": ["Xelishashv2"], "pay_interval": 4, "min_pay": 1, "model_fee": {"PPLNS": 0.01, "SOLO":0.01}, "mining_url": {"tcp": {"GPU": "cn.xel.k1pool.com:9351", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"hiveon.net": {
"wallet_mining": true,
"coins": {"GPU": ["RVN"], "ASIC": []},
"RVN": {"full_name": "Ravencoin", "algos": ["kawpow"], "pay_interval": "07:30 UTC", "min_pay": 10, "model_fee": {"PPS+": 0.005}, "mining_url": {"tcp": {"GPU": "rvn.hiveon.com:8888", "ASIC": ""}, "ssl": {"GPU": "rvn.hiveon.com:7777", "ASIC": ""}}, "hashrate_url": ""}
},
"ravenminer": {
"wallet_mining": true,
"coins": {"GPU": ["RVN"], "ASIC": []},
"RVN": {"full_name": "Ravencoin", "algos": ["kawpow"], "pay_interval": 3, "min_pay": 5, "model_fee": {"PPLNS":0.005, "PPS+": 0.01, "SOLO":0.005}, "mining_url": {"tcp": {"GPU": "stratum.ravenminer.com:3838", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
},
"antpool": {
"wallet_mining": false,
"coins": {"GPU": ["RVN"], "ASIC": []},
"RVN": {"full_name": "Ravencoin", "algos": ["kawpow"], "pay_interval": "08:00-16:00", "min_pay": 1, "model_fee": {"PPS": 0.03}, "mining_url": {"tcp": {"GPU": "rvn.antpool.com:8003", "ASIC": ""}, "ssl": {"GPU": "", "ASIC": ""}}, "hashrate_url": ""}
}
}

View File

@@ -1,10 +0,0 @@
@echo off
cd /d %~dp0
go build -o .\client.exe test.go
if %errorlevel% equ 0 (
echo 编译成功!可执行文件位于: .\client.exe
) else (
echo 编译失败!
exit /b %errorlevel%
)

View File

@@ -1,52 +0,0 @@
// package main
// import (
// "log"
// "time"
// message "client/internal/msg"
// "client/internal/src/linux"
// )
// func main() {
// auth := "lzx"
// l := linux.NewLinuxClient(auth)
// go func() {
// gpus, err := l.GetGPUInfo()
// if err != nil {
// log.Fatalf("获取GPU信息失败%v", err)
// return
// }
// log.Println(gpus)
// }()
// now_ts := time.Now().Unix()
// cfg := message.ConfigurationMiningMsg{
// Coin: "NEXA",
// Algo: "NexaPow",
// Pool: "m2pool",
// PoolUrl: "47.108.221.51:3333",
// WalletAddress: "m2test",
// PoolUser: "m2test",
// WorkerID: "go",
// EndTimestamp: uint64(now_ts + 600),
// }
// if err := l.Mining(cfg); err != nil {
// log.Fatalf("配置挖矿失败: %v", err)
// }
// endTime := time.Unix(int64(cfg.EndTimestamp), 0)
// waitDuration := time.Until(endTime)
// if waitDuration < 0 {
// waitDuration = 0
// }
// // 等待挖矿任务自然结束,额外多等 5 秒确保清理完成
// time.Sleep(waitDuration + 5*time.Second)
// }
package main
import client "client/internal"
func main() {
client.Star()
}

View File

@@ -1,10 +0,0 @@
#!/bin/bash
cd "$(dirname "$0")"
go build -o ./test/client test.go
if [ $? -eq 0 ]; then
echo "编译成功!可执行文件位于: ./test/client"
else
echo "编译失败!"
exit 1
fi