devops/tools/main.go
2025-12-12 11:40:38 +08:00

697 lines
19 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 main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/json"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
"github.com/gookit/color"
"gopkg.in/ini.v1"
)
const (
SECRET_KEY = ")VQbB(vpy=U(wcp)"
CLEAN_KEY = "MergePet2024!"
)
// GOOS=linux GOARCH=amd64 go build -o /data/devops/MergePet/tool/tool main.go
var help = `
Usage: app.ini [options]
start start the server
stop stop the server
restart restart the server
reload reload the config
status get the server status
install install the server
uninstall uninstall the server
kill kill the server
backup backup the database
clean clean the database
`
var cfg *ini.File
var FuncMap map[string]func() error
var dirPath string
var app_path string
var (
infoColor = color.New(color.FgGreen)
warn = color.New(color.FgYellow)
err = color.New(color.FgRed)
)
func info(format string, a ...interface{}) {
time := time.Now().Format("2006-01-02 15:04:05")
format = fmt.Sprintf("[%s] %s\n", time, format)
infoColor.Printf(format, a...)
}
func main() {
defer func() {
if err := recover(); err != nil {
info("%s", help)
}
}()
FuncMap = make(map[string]func() error)
// 检查是否提供了命令行参数
if len(os.Args) < 2 {
log.Fatal(help)
}
var err error
// 获取当前文件的绝对路径
execPath, err := os.Executable()
if err != nil {
log.Fatal(err)
}
absPath, err := filepath.Abs(execPath)
if err != nil {
log.Fatal(err)
}
// 获取当前文件的绝对路径的文件夹路径
dirPath = filepath.Dir(absPath)
// 加载 app.ini 文件
cfg, err = ini.Load(dirPath + "/app.ini")
if err != nil {
log.Fatal(err)
}
app_path = cfg.Section("app").Key("app_path").String()
register("start", start)
register("stop", stop)
register("install", install)
register("status", status)
register("restart", restart)
register("uninstall", uninstall)
register("reload", reload)
register("kill", kill)
register("backup", backup)
register("clean", clean)
funcName := os.Args[1]
if f, ok := FuncMap[funcName]; ok {
e := f()
if e != nil {
log.Fatal(e)
}
} else {
log.Fatal(help)
}
}
func register(name string, f func() error) {
FuncMap[name] = f
}
func kill() error {
if len(os.Args) < 4 {
log.Fatal("请输入要停止的服务类型和区号 kill [center|node] 区号")
}
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
processName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
// 示例进程名称
// 获取进程号
pid, err := getPidByArgs(processName)
if err != nil {
log.Fatal(err)
}
// 使用命令行命令 kill -9 杀死进程
cmd := exec.Command("kill", "-9", strconv.Itoa(pid))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
info("server %s_%s 已被 kill -9 杀死, 进程号:%d", NodeType, Zone, pid)
return nil
}
func install() error {
// log.Println("install")
if len(os.Args) < 4 {
err.Println("请输入要安装的服务类型和区号 install [center|node] 区号")
}
serverType := os.Args[2]
zone := os.Args[3]
if serverType != "center" && serverType != "node" {
err.Println("请输入正确的服务类型 center|node")
}
AppName := cfg.Section("app").Key("app_name").String()
// 生成服务名
serviceName := fmt.Sprintf("%s_%s_%s", AppName, serverType, zone)
// 判断文件夹是否存在
folderPath := fmt.Sprintf("%s/zone/%s", app_path, serviceName)
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
err := os.MkdirAll(folderPath, os.ModePerm)
if err != nil {
log.Fatal(err)
}
info("文件夹 %s 创建成功\n", folderPath)
} else {
info("文件夹 %s 已存在\n", folderPath)
}
// 生成配置文件
configPath := fmt.Sprintf("%s/server.json", folderPath)
file, err := os.Create(configPath)
if err != nil {
log.Fatal(err)
}
defer file.Close()
zoneId, _ := strconv.Atoi(zone)
cf, confs := createConfigFile(zoneId, serverType)
file.WriteString(confs)
// 读取 SQL 文件内容并替换 %database% 字符串
sqlFilePath := fmt.Sprintf("%s/tool/Merge_Pet.sql", app_path)
sqlContent, err := os.ReadFile(sqlFilePath)
if err != nil {
log.Fatal(err)
}
dbName := cf["DbName"].(string)
modifiedSQL := strings.ReplaceAll(string(sqlContent), "%database%", dbName)
// 将修改后的内容写入临时文件
tempSQLFile, err := os.CreateTemp("", "modified_*.sql")
if err != nil {
log.Fatal(err)
}
_, err = tempSQLFile.WriteString(modifiedSQL)
if err != nil {
log.Fatal(err)
}
tempSQLFile.Close()
// 创建数据库
dbUser := cfg.Section("mysql").Key("mysql_user").String()
dbPassword, _ := Decrypt(cfg.Section("mysql").Key("mysql_password").String())
dbHost := cfg.Section("mysql").Key("mysql_host").String()
dbPort := cfg.Section("mysql").Key("mysql_port").String()
// log.Println("mysql", "-u"+dbUser, "-p"+dbPassword, "-h"+dbHost, "-P"+dbPort, "-e", fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", dbName))
createDBCmd := exec.Command("mysql", "-u"+dbUser, "-p"+dbPassword, "-h"+dbHost, "-P"+dbPort, "-e", fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", dbName))
createDBCmd.Stdout = os.Stdout
createDBCmd.Stderr = os.Stderr
err = createDBCmd.Run()
if err != nil {
log.Fatal(err)
}
// log.Println("mysql", "-u"+dbUser, "-p"+dbPassword, "-h"+dbHost, "-P"+dbPort, dbName, "-e", fmt.Sprintf("source %s", tempSQLFile.Name()))
// 执行修改后的 SQL 文件
cmd := exec.Command("mysql", "-u"+dbUser, "-p"+dbPassword, "-h"+dbHost, "-P"+dbPort, dbName, "-e", fmt.Sprintf("source %s", tempSQLFile.Name()))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
cmd = exec.Command("mkdir", cf["LogPath"].(string))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err = cmd.Run()
if err != nil {
log.Fatal(err)
}
info("SQL 文件 %s 执行成功\n", sqlFilePath)
info("配置文件 %s 创建成功\n", configPath)
return nil
}
func uninstall() error {
stop()
if len(os.Args) < 4 {
log.Fatal("请输入要卸载的服务类型和区号 uninstall [center|node] 区号")
}
serverType := os.Args[2]
zone := os.Args[3]
if serverType != "center" && serverType != "node" {
log.Fatal("请输入正确的服务类型 center|node")
}
AppName := cfg.Section("app").Key("app_name").String()
// 生成服务名
serviceName := fmt.Sprintf("%s_%s_%s", AppName, serverType, zone)
// 判断文件夹是否存在
folderPath := fmt.Sprintf("%s/zone/%s", app_path, serviceName)
if _, err := os.Stat(folderPath); os.IsNotExist(err) {
info("文件夹 %s 不存在\n", folderPath)
return nil
} else {
info("文件夹 %s 存在\n", folderPath)
}
// 删除文件夹
err := os.RemoveAll(folderPath)
if err != nil {
log.Fatal(err)
}
info("文件夹 %s 删除成功\n", folderPath)
return nil
}
func createConfigFile(Id int, Type string) (map[string]interface{}, string) {
conf := make(map[string]interface{})
conf["LogLevel"] = cfg.Section("log").Key("log_level").String()
TcpStartAddr, _ := cfg.Section("server").Key("tcp_addr").Int()
conf["TcpAddr"] = fmt.Sprintf(":%d", TcpStartAddr+Id)
WsStartAddr, _ := cfg.Section("server").Key("ws_addr").Int()
conf["WsAddr"] = fmt.Sprintf(":%d", WsStartAddr+Id)
ListenAddr, _ := cfg.Section("server").Key("listen_addr").Int()
conf["MySqlAddr"] = cfg.Section("mysql").Key("mysql_host").String()
conf["MySqlUsr"] = cfg.Section("mysql").Key("mysql_user").String()
conf["MySqlPwd"] = cfg.Section("mysql").Key("mysql_password").String()
conf["MySqlPort"] = cfg.Section("mysql").Key("mysql_port").String()
MaxConnNum, _ := cfg.Section("server").Key("max_conn").Int()
conf["MaxConnNum"] = MaxConnNum
app_name := cfg.Section("app").Key("app_name").String()
conf["LogPath"] = fmt.Sprintf("%s/zone/%s_%s_%d/log", app_path, app_name, Type, Id)
conf["DbName"] = fmt.Sprintf("%s_%d", app_name, Id)
conf["TELOGDIR"] = cfg.Section("log").Key("te_log_path").String()
conf["GameName"] = app_name
AppId, _ := cfg.Section("app").Key("app_id").Int()
conf["AppID"] = AppId
conf["AppPath"] = app_path
conf["ServerType"] = Type
conf["ServerID"] = Id
conf["KafkaHost"] = cfg.Section("kafka").Key("kafka_host").String()
conf["KafkaPort"] = cfg.Section("kafka").Key("kafka_port").String
conf["ServerOpenTime"] = "2028-01-01 00:00:00"
conf["ServerName"] = fmt.Sprintf("%s_%d", app_name, Id)
conf["ServerStatus"] = 1
CenterId, _ := cfg.Section("app").Key("center_id").Int()
conf["ServerCenter"] = CenterId
conf["CountryCode"] = cfg.Section("app").Key("country_code").String()
conf["RedisAddr"] = cfg.Section("redis").Key("redis_host").String()
conf["RedisPort"] = cfg.Section("redis").Key("redis_port").String()
conf["GameConfPath"] = app_path + "/config/"
conf["RemoteAddr"] = fmt.Sprintf("%s:%d", cfg.Section("server").Key("remote").String(), ListenAddr+Id) // 服务器地址
conf["ListenAddr"] = fmt.Sprintf(":%d", ListenAddr+Id)
conf["CenterAddr"] = fmt.Sprintf("%s:%s", cfg.Section("cluster").Key("center_host").String(), cfg.Section("cluster").Key("center_port").String()) // 服务器地址
b, _ := json.MarshalIndent(conf, "", " ")
return conf, string(b)
}
func backup() error {
if len(os.Args) < 4 {
log.Fatal("请输入要启动的服务类型和区号 start [center|node] 区号")
}
err := status()
if err == nil {
log.Fatal("节点启动中,请先关闭节点")
}
info("开始备份server %s_%s ...", os.Args[2], os.Args[3])
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
configName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
jsonData, err := os.ReadFile(configName)
if err != nil {
log.Fatal(err)
}
var config map[string]interface{}
if err := json.Unmarshal(jsonData, &config); err != nil {
log.Fatal(err)
}
info("读取配置文件 %s 成功", configName)
dbUser := cfg.Section("mysql").Key("mysql_user").String()
dbPassword, _ := Decrypt(cfg.Section("mysql").Key("mysql_password").String())
dbHost := cfg.Section("mysql").Key("mysql_host").String()
dbPort := cfg.Section("mysql").Key("mysql_port").String()
dbName := config["DbName"].(string)
// 生成备份文件名
backupFile := fmt.Sprintf("%s/zone/%s_%s_%s/%s_backup_%s.sql",
app_path, app_name, NodeType, Zone, dbName, time.Now().Format("20060102_150405"))
// 执行 mysqldump 命令
cmd := exec.Command("mysqldump",
"-u"+dbUser,
"-p"+dbPassword,
"-h"+dbHost,
"-P"+dbPort,
dbName,
)
outFile, err := os.Create(backupFile)
if err != nil {
log.Fatal(err)
}
defer outFile.Close()
cmd.Stdout = outFile
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal(err)
}
info("数据库 %s 备份成功,文件:%s", dbName, backupFile)
return nil
}
func clean() error {
if len(os.Args) < 4 {
log.Fatal("请输入要清空的服务类型和区号 clean [center|node] 区号")
}
err := status()
if err == nil {
log.Fatal("节点启动中,请先关闭节点")
}
fmt.Print("请输入操作密钥: ")
var inputKey string
fmt.Scanln(&inputKey)
if inputKey != CLEAN_KEY {
log.Fatal("密钥错误,操作终止")
}
info("开始清空数据库server %s_%s ...", os.Args[2], os.Args[3])
NodeType := os.Args[2]
Zone := os.Args[3]
app_name := cfg.Section("app").Key("app_name").String()
configName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
jsonData, err := os.ReadFile(configName)
if err != nil {
log.Fatal(err)
}
var config map[string]interface{}
if err := json.Unmarshal(jsonData, &config); err != nil {
log.Fatal(err)
}
dbUser := cfg.Section("mysql").Key("mysql_user").String()
dbPassword, _ := Decrypt(cfg.Section("mysql").Key("mysql_password").String())
dbHost := cfg.Section("mysql").Key("mysql_host").String()
dbPort := cfg.Section("mysql").Key("mysql_port").String()
dbName := config["DbName"].(string)
// 获取所有表名
showTablesCmd := exec.Command("mysql",
"-u"+dbUser,
"-p"+dbPassword,
"-h"+dbHost,
"-P"+dbPort,
"-N", "-e", "SHOW TABLES IN "+dbName)
output, err := showTablesCmd.Output()
if err != nil {
log.Fatal(err)
}
tables := strings.Fields(string(output))
if len(tables) == 0 {
info("数据库 %s 无表,无需清空", dbName)
return nil
}
// 拼接 TRUNCATE 语句
var stmts []string
for _, t := range tables {
stmts = append(stmts, fmt.Sprintf("TRUNCATE TABLE `%s`;", t))
}
truncateSQL := strings.Join(stmts, " ")
// 执行清空
truncateCmd := exec.Command("mysql",
"-u"+dbUser,
"-p"+dbPassword,
"-h"+dbHost,
"-P"+dbPort,
dbName,
"-e", truncateSQL)
truncateCmd.Stdout = os.Stdout
truncateCmd.Stderr = os.Stderr
if err := truncateCmd.Run(); err != nil {
log.Fatal(err)
}
info("数据库 %s 所有表已清空", dbName)
redisHost := cfg.Section("redis").Key("redis_host").String()
redisPort := cfg.Section("redis").Key("redis_port").String()
cmd := exec.Command("redis-cli", "-h", redisHost, "-p", redisPort, "FLUSHALL")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
log.Fatal("清空 Redis 失败: ", err)
}
info("Redis 所有数据已清空")
return nil
}
func start() error {
// log.Println("start")
if len(os.Args) < 4 {
log.Fatal("请输入要启动的服务类型和区号 start [center|node] 区号")
}
err := status()
if err == nil {
log.Fatal("节点启动中")
}
info("正在启动服务server %s_%s ...", os.Args[2], os.Args[3])
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
cmdName := app_path + "/main"
cmdArgs := []string{fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)}
// 创建命令
cmd := exec.Command(cmdName, cmdArgs...)
cmd.SysProcAttr = &syscall.SysProcAttr{}
// 创建管道
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
cmd.Env = os.Environ()
// 打开输出文件
outfile, err := os.OpenFile(fmt.Sprintf("%s/zone/%s_%s_%s/output.log", app_path, app_name, NodeType, Zone), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatal(err)
}
defer outfile.Close()
// 重定向标准输出和标准错误到文件
cmd.Stdout = outfile
cmd.Stderr = outfile
err = cmd.Start()
if err != nil {
log.Fatal(err)
}
// 打印进程号
info("server %s_%s已启动, 进程号:%d", os.Args[2], os.Args[3], cmd.Process.Pid)
// 发送命令到子进程
go func() {
time.Sleep(2 * time.Second) // 等待子进程启动
_, err := stdin.Write([]byte("your_command\n"))
if err != nil {
log.Fatal(err)
}
stdin.Close()
}()
// 释放与子进程相关的资源
err = cmd.Process.Release()
if err != nil {
log.Fatal(err)
}
return nil
}
func reload() error {
if len(os.Args) < 4 {
log.Fatal("请输入要重启的服务类型和区号 reload [center|node] 区号")
}
app_name := cfg.Section("app").Key("app_name").String()
NodeType := os.Args[2]
Zone := os.Args[3]
args := fmt.Sprintf("%s_%s_%s", app_name, NodeType, Zone)
pid, err := getPidByArgs(args)
if err != nil {
log.Fatal(err)
}
// 向进程发送SIGTERM信号
process, err := os.FindProcess(pid)
if err != nil {
log.Fatal(err)
}
err = process.Signal(syscall.SIGINT)
if err != nil {
log.Fatal(err)
}
fmt.Printf("server %s_%s 已发送SIGINT信号, 进程号: %d\n", NodeType, Zone, pid)
return nil
}
func stop() error {
if len(os.Args) < 4 {
log.Fatal("请输入要停止的服务类型和区号 stop [center|node] 区号")
}
err := statusInfo()
if err != nil {
return err
}
info("正在关闭服务server %s_%s ...", os.Args[2], os.Args[3])
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
processName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
// 示例进程名称
// 获取进程号
pid, err := getPidByArgs(processName)
if err != nil {
log.Fatal(err)
}
// 查找进程
process, err := os.FindProcess(pid)
if err != nil {
log.Fatal(err)
}
// 关闭进程
err = process.Signal(syscall.SIGTERM)
if err != nil {
log.Fatal(err)
}
info("server %s_%s 已关闭,进程号:%d", NodeType, Zone, pid)
return nil
}
func getPidByArgs(args string) (int, error) {
cmd := exec.Command("pgrep", "-f", args)
output, err := cmd.Output()
if err != nil {
return 0, err
}
// 解析输出,获取第一个匹配的进程号
outputStr := strings.TrimSpace(string(output))
pidStr := strings.Split(outputStr, "\n")[0]
pid, err := strconv.Atoi(pidStr)
if err != nil {
return 0, err
}
return pid, nil
}
func status() error {
if len(os.Args) < 4 {
log.Fatal("请输入要查询的服务类型和区号 status [center|node] 区号")
}
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
processName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
// 示例进程名称
// 获取进程号
pid, err := getPidByArgs(processName)
if err != nil {
return fmt.Errorf("进程 %s_%s 未启动", NodeType, Zone)
}
// 查找进程
_, err = os.FindProcess(pid)
if err != nil {
return fmt.Errorf("进程 %s_%s 未启动", NodeType, Zone)
}
info("节点 %s_%s 启动中, 进程号 %d\n", NodeType, Zone, pid)
return nil
}
func statusInfo() error {
NodeType := os.Args[2]
Zone := os.Args[3]
// 示例命令
app_name := cfg.Section("app").Key("app_name").String()
processName := fmt.Sprintf("%s/zone/%s_%s_%s/server.json", app_path, app_name, NodeType, Zone)
// 示例进程名称
// 获取进程号
pid, err := getPidByArgs(processName)
if err != nil {
return fmt.Errorf("进程 %s_%s 未启动", NodeType, Zone)
}
// 查找进程
_, err = os.FindProcess(pid)
if err != nil {
return fmt.Errorf("进程 %s_%s 未启动", NodeType, Zone)
}
// info.Println("节点 %s_%s 启动中, 进程号 %d\n", NodeType, Zone, pid)
return nil
}
func restart() error {
if len(os.Args) < 4 {
log.Fatal("请输入要重启的服务类型和区号 restart [center|node] 区号")
}
err := stop()
if err != nil {
log.Println(err)
}
for statusInfo() == nil {
time.Sleep(1 * time.Second)
}
err = start()
if err != nil {
return err
}
return nil
}
// 解密字符串
func Decrypt(cipherText string) (string, error) {
cipherTextBytes, err := base64.URLEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
block, err := aes.NewCipher([]byte(SECRET_KEY))
if err != nil {
return "", err
}
if len(cipherTextBytes) < aes.BlockSize {
return "", fmt.Errorf("cipherText too short")
}
iv := cipherTextBytes[:aes.BlockSize]
cipherTextBytes = cipherTextBytes[aes.BlockSize:]
stream := cipher.NewCFBDecrypter(block, iv)
stream.XORKeyStream(cipherTextBytes, cipherTextBytes)
return string(cipherTextBytes), nil
}