1068 lines
23 KiB
Markdown
1068 lines
23 KiB
Markdown
# 使用 Go 语言在 Windows 上开发简易 PM2 式进程守护系统
|
||
|
||
下面我将为您详细讲解如何使用 Go 语言在 Windows 上开发一个类似 PM2 的进程守护系统。这个系统将包含核心功能如进程启动、停止、重启、监控和日志管理。
|
||
|
||
## 系统架构设计
|
||
|
||
```
|
||
+-------------------+ +-----------------+ +-----------------+
|
||
| 命令行客户端 | | 守护进程 | | 被守护进程 |
|
||
| (pm2-win-cli) |<--->| (pm2-win-daemon)|---->| (your-app.exe) |
|
||
+-------------------+ +-----------------+ +-----------------+
|
||
↑ ↑ ↑ ↑
|
||
| | | |
|
||
+-------------------+ +-----------------+ +-----------------+
|
||
| 用户交互 | | 进程管理 | | 子进程 |
|
||
| 命令解析 | | 监控重启 | | 执行实际任务 |
|
||
| 状态显示 | | 日志管理 | +-----------------+
|
||
+-------------------+ | RPC服务 |
|
||
+-----------------+
|
||
```
|
||
|
||
## 完整实现代码
|
||
|
||
### 1. 守护进程主程序 (`daemon/main.go`)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
|
||
"golang.org/x/sys/windows/svc"
|
||
"golang.org/x/sys/windows/svc/eventlog"
|
||
)
|
||
|
||
const (
|
||
serviceName = "PM2WinDaemon"
|
||
daemonPort = 9999
|
||
)
|
||
|
||
var (
|
||
installFlag = flag.Bool("install", false, "Install service")
|
||
uninstallFlag = flag.Bool("uninstall", false, "Uninstall service")
|
||
runFlag = flag.Bool("run", false, "Run in console mode")
|
||
)
|
||
|
||
func main() {
|
||
flag.Parse()
|
||
|
||
if *installFlag {
|
||
installService()
|
||
return
|
||
}
|
||
|
||
if *uninstallFlag {
|
||
uninstallService()
|
||
return
|
||
}
|
||
|
||
if *runFlag {
|
||
runConsoleMode()
|
||
return
|
||
}
|
||
|
||
// 检查是否在服务环境中运行
|
||
isService, err := svc.IsWindowsService()
|
||
if err != nil {
|
||
log.Fatalf("Failed to determine if running as service: %v", err)
|
||
}
|
||
|
||
if isService {
|
||
runService()
|
||
} else {
|
||
fmt.Println("Please run as service or with --run flag")
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
|
||
func runConsoleMode() {
|
||
log.Println("Running in console mode")
|
||
startDaemon()
|
||
}
|
||
|
||
func runService() {
|
||
elog, err := eventlog.Open(serviceName)
|
||
if err != nil {
|
||
log.Fatalf("Failed to open event log: %v", err)
|
||
}
|
||
defer elog.Close()
|
||
|
||
elog.Info(1, "Starting service")
|
||
|
||
if err := svc.Run(serviceName, &serviceHandler{elog: elog}); err != nil {
|
||
elog.Error(1, fmt.Sprintf("Service failed: %v", err))
|
||
return
|
||
}
|
||
|
||
elog.Info(1, "Service stopped")
|
||
}
|
||
|
||
func startDaemon() {
|
||
log.Println("PM2Win Daemon starting")
|
||
|
||
// 创建进程管理器
|
||
pm := NewProcessManager()
|
||
|
||
// 启动RPC服务器
|
||
go startRPCServer(pm)
|
||
|
||
// 启动监控器
|
||
go pm.StartMonitor()
|
||
|
||
// 启动Web界面
|
||
go startWebUI(pm)
|
||
|
||
log.Println("Daemon is ready. Listening on port", daemonPort)
|
||
|
||
// 等待退出信号
|
||
quit := make(chan os.Signal, 1)
|
||
<-quit
|
||
pm.StopAll()
|
||
log.Println("Daemon stopped")
|
||
}
|
||
|
||
func installService() {
|
||
exePath, err := filepath.Abs(os.Args[0])
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
mgr, err := svc.Mgr.Connect()
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer mgr.Disconnect()
|
||
|
||
// 创建服务
|
||
config := svc.Config{
|
||
DisplayName: "PM2Win Daemon",
|
||
Description: "Process manager for Windows similar to PM2",
|
||
StartType: svc.StartAutomatic,
|
||
}
|
||
|
||
service, err := mgr.CreateService(serviceName, exePath, config)
|
||
if err != nil {
|
||
log.Fatal("CreateService failed:", err)
|
||
}
|
||
defer service.Close()
|
||
|
||
// 设置恢复策略
|
||
recoveryActions := []svc.RecoveryAction{
|
||
{Type: svc.ServiceRestart, Delay: 60 * time.Second},
|
||
{Type: svc.ServiceRestart, Delay: 120 * time.Second},
|
||
{Type: svc.NoAction},
|
||
}
|
||
|
||
if err := service.SetRecoveryActions(recoveryActions, 86400); err != nil {
|
||
log.Println("Warning: failed to set recovery actions:", err)
|
||
}
|
||
|
||
// 创建事件日志源
|
||
if err := eventlog.InstallAsEventCreate(serviceName, eventlog.Error|eventlog.Warning|eventlog.Info); err != nil {
|
||
log.Println("Warning: failed to create event source:", err)
|
||
}
|
||
|
||
log.Println("Service installed successfully")
|
||
}
|
||
|
||
func uninstallService() {
|
||
mgr, err := svc.Mgr.Connect()
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer mgr.Disconnect()
|
||
|
||
service, err := mgr.OpenService(serviceName)
|
||
if err != nil {
|
||
log.Fatal("Service not installed:", err)
|
||
}
|
||
defer service.Close()
|
||
|
||
// 停止服务
|
||
if status, err := service.Control(svc.Stop); err == nil {
|
||
timeout := time.Now().Add(10 * time.Second)
|
||
for status.State != svc.Stopped && time.Now().Before(timeout) {
|
||
time.Sleep(500 * time.Millisecond)
|
||
status, err = service.Query()
|
||
if err != nil {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// 删除服务
|
||
if err := service.Delete(); err != nil {
|
||
log.Fatal("DeleteService failed:", err)
|
||
}
|
||
|
||
// 移除事件日志源
|
||
if err := eventlog.Remove(serviceName); err != nil {
|
||
log.Println("Warning: failed to remove event source:", err)
|
||
}
|
||
|
||
log.Println("Service uninstalled successfully")
|
||
}
|
||
|
||
type serviceHandler struct {
|
||
elog *eventlog.Log
|
||
}
|
||
|
||
func (h *serviceHandler) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||
changes <- svc.Status{State: svc.StartPending}
|
||
|
||
// 启动守护逻辑
|
||
ctx, cancel := context.WithCancel(context.Background())
|
||
defer cancel()
|
||
|
||
go func() {
|
||
startDaemon()
|
||
cancel()
|
||
}()
|
||
|
||
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
|
||
|
||
for {
|
||
select {
|
||
case c := <-r:
|
||
switch c.Cmd {
|
||
case svc.Stop, svc.Shutdown:
|
||
changes <- svc.Status{State: svc.StopPending}
|
||
cancel()
|
||
return false, 0
|
||
case svc.Interrogate:
|
||
changes <- c.CurrentStatus
|
||
}
|
||
case <-ctx.Done():
|
||
return false, 0
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 进程管理器 (`daemon/process_manager.go`)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// ProcessConfig 进程配置
|
||
type ProcessConfig struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Command string `json:"command"`
|
||
Args []string `json:"args"`
|
||
WorkingDir string `json:"working_dir"`
|
||
Restart bool `json:"restart"`
|
||
MaxRestarts int `json:"max_restarts"`
|
||
LogFile string `json:"log_file"`
|
||
}
|
||
|
||
// ProcessStatus 进程状态
|
||
type ProcessStatus struct {
|
||
ID string `json:"id"`
|
||
Name string `json:"name"`
|
||
Status string `json:"status"` // running, stopped, restarting
|
||
PID int `json:"pid"`
|
||
Restarts int `json:"restarts"`
|
||
StartTime time.Time `json:"start_time"`
|
||
Uptime string `json:"uptime"`
|
||
CPU float64 `json:"cpu"`
|
||
Memory uint64 `json:"memory"`
|
||
}
|
||
|
||
// Process 被守护的进程
|
||
type Process struct {
|
||
Config ProcessConfig
|
||
Status ProcessStatus
|
||
cmd *exec.Cmd
|
||
mu sync.Mutex
|
||
stopCh chan struct{}
|
||
log *os.File
|
||
}
|
||
|
||
// ProcessManager 进程管理器
|
||
type ProcessManager struct {
|
||
processes map[string]*Process
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
func NewProcessManager() *ProcessManager {
|
||
return &ProcessManager{
|
||
processes: make(map[string]*Process),
|
||
}
|
||
}
|
||
|
||
// StartProcess 启动新进程
|
||
func (pm *ProcessManager) StartProcess(cfg ProcessConfig) error {
|
||
pm.mu.Lock()
|
||
defer pm.mu.Unlock()
|
||
|
||
if _, exists := pm.processes[cfg.ID]; exists {
|
||
return fmt.Errorf("process with ID %s already exists", cfg.ID)
|
||
}
|
||
|
||
process := &Process{
|
||
Config: cfg,
|
||
Status: ProcessStatus{
|
||
ID: cfg.ID,
|
||
Name: cfg.Name,
|
||
Status: "starting",
|
||
StartTime: time.Now(),
|
||
},
|
||
stopCh: make(chan struct{}),
|
||
}
|
||
|
||
// 设置日志文件
|
||
if cfg.LogFile != "" {
|
||
logFile, err := os.OpenFile(cfg.LogFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return fmt.Errorf("failed to open log file: %v", err)
|
||
}
|
||
process.log = logFile
|
||
}
|
||
|
||
pm.processes[cfg.ID] = process
|
||
|
||
// 启动进程
|
||
go process.Run()
|
||
|
||
return nil
|
||
}
|
||
|
||
// StopProcess 停止进程
|
||
func (pm *ProcessManager) StopProcess(id string) error {
|
||
pm.mu.RLock()
|
||
defer pm.mu.RUnlock()
|
||
|
||
process, exists := pm.processes[id]
|
||
if !exists {
|
||
return fmt.Errorf("process with ID %s not found", id)
|
||
}
|
||
|
||
return process.Stop()
|
||
}
|
||
|
||
// RestartProcess 重启进程
|
||
func (pm *ProcessManager) RestartProcess(id string) error {
|
||
pm.mu.Lock()
|
||
defer pm.mu.Unlock()
|
||
|
||
process, exists := pm.processes[id]
|
||
if !exists {
|
||
return fmt.Errorf("process with ID %s not found", id)
|
||
}
|
||
|
||
// 先停止
|
||
if err := process.Stop(); err != nil {
|
||
return err
|
||
}
|
||
|
||
// 重置状态
|
||
process.Status.Status = "restarting"
|
||
process.Status.Restarts = 0
|
||
|
||
// 重新启动
|
||
go process.Run()
|
||
|
||
return nil
|
||
}
|
||
|
||
// ListProcesses 列出所有进程
|
||
func (pm *ProcessManager) ListProcesses() []ProcessStatus {
|
||
pm.mu.RLock()
|
||
defer pm.mu.RUnlock()
|
||
|
||
statuses := make([]ProcessStatus, 0, len(pm.processes))
|
||
for _, proc := range pm.processes {
|
||
proc.mu.Lock()
|
||
status := proc.Status
|
||
proc.mu.Unlock()
|
||
|
||
// 更新运行时间
|
||
if status.Status == "running" {
|
||
status.Uptime = time.Since(status.StartTime).Round(time.Second).String()
|
||
}
|
||
|
||
// TODO: 获取CPU和内存使用率
|
||
|
||
statuses = append(statuses, status)
|
||
}
|
||
|
||
return statuses
|
||
}
|
||
|
||
// GetProcessStatus 获取单个进程状态
|
||
func (pm *ProcessManager) GetProcessStatus(id string) (ProcessStatus, error) {
|
||
pm.mu.RLock()
|
||
defer pm.mu.RUnlock()
|
||
|
||
process, exists := pm.processes[id]
|
||
if !exists {
|
||
return ProcessStatus{}, fmt.Errorf("process with ID %s not found", id)
|
||
}
|
||
|
||
process.mu.Lock()
|
||
defer process.mu.Unlock()
|
||
|
||
status := process.Status
|
||
if status.Status == "running" {
|
||
status.Uptime = time.Since(status.StartTime).Round(time.Second).String()
|
||
}
|
||
|
||
return status, nil
|
||
}
|
||
|
||
// StopAll 停止所有进程
|
||
func (pm *ProcessManager) StopAll() {
|
||
pm.mu.Lock()
|
||
defer pm.mu.Unlock()
|
||
|
||
for id, proc := range pm.processes {
|
||
if err := proc.Stop(); err != nil {
|
||
log.Printf("Failed to stop process %s: %v", id, err)
|
||
}
|
||
delete(pm.processes, id)
|
||
}
|
||
}
|
||
|
||
// StartMonitor 启动监控器
|
||
func (pm *ProcessManager) StartMonitor() {
|
||
ticker := time.NewTicker(5 * time.Second)
|
||
defer ticker.Stop()
|
||
|
||
for range ticker.C {
|
||
pm.mu.RLock()
|
||
for _, proc := range pm.processes {
|
||
proc.mu.Lock()
|
||
if proc.Status.Status == "running" {
|
||
// 检查进程是否存活
|
||
if proc.cmd.ProcessState != nil && proc.cmd.ProcessState.Exited() {
|
||
proc.Status.Status = "stopped"
|
||
|
||
// 如果需要重启
|
||
if proc.Config.Restart &&
|
||
(proc.Config.MaxRestarts == 0 || proc.Status.Restarts < proc.Config.MaxRestarts) {
|
||
proc.Status.Restarts++
|
||
go proc.Run()
|
||
}
|
||
}
|
||
}
|
||
proc.mu.Unlock()
|
||
}
|
||
pm.mu.RUnlock()
|
||
}
|
||
}
|
||
|
||
// Run 运行进程
|
||
func (p *Process) Run() {
|
||
p.mu.Lock()
|
||
p.Status.Status = "starting"
|
||
p.Status.StartTime = time.Now()
|
||
p.mu.Unlock()
|
||
|
||
for {
|
||
select {
|
||
case <-p.stopCh:
|
||
return
|
||
default:
|
||
// 创建命令
|
||
cmd := exec.Command(p.Config.Command, p.Config.Args...)
|
||
|
||
// 设置工作目录
|
||
if p.Config.WorkingDir != "" {
|
||
cmd.Dir = p.Config.WorkingDir
|
||
}
|
||
|
||
// 设置输出
|
||
if p.log != nil {
|
||
cmd.Stdout = p.log
|
||
cmd.Stderr = p.log
|
||
}
|
||
|
||
p.mu.Lock()
|
||
p.cmd = cmd
|
||
p.Status.PID = cmd.Process.Pid
|
||
p.Status.Status = "running"
|
||
p.mu.Unlock()
|
||
|
||
// 启动进程
|
||
if err := cmd.Start(); err != nil {
|
||
log.Printf("Failed to start process %s: %v", p.Config.ID, err)
|
||
|
||
p.mu.Lock()
|
||
p.Status.Status = "stopped"
|
||
p.mu.Unlock()
|
||
return
|
||
}
|
||
|
||
log.Printf("Process %s started with PID %d", p.Config.ID, cmd.Process.Pid)
|
||
|
||
// 等待进程退出
|
||
err := cmd.Wait()
|
||
|
||
p.mu.Lock()
|
||
if err != nil {
|
||
log.Printf("Process %s exited with error: %v", p.Config.ID, err)
|
||
} else {
|
||
log.Printf("Process %s exited normally", p.Config.ID)
|
||
}
|
||
|
||
// 检查是否应该重启
|
||
if !p.Config.Restart {
|
||
p.Status.Status = "stopped"
|
||
p.mu.Unlock()
|
||
return
|
||
}
|
||
|
||
if p.Config.MaxRestarts > 0 && p.Status.Restarts >= p.Config.MaxRestarts {
|
||
p.Status.Status = "stopped"
|
||
p.mu.Unlock()
|
||
return
|
||
}
|
||
|
||
p.Status.Restarts++
|
||
p.mu.Unlock()
|
||
|
||
// 延迟重启
|
||
time.Sleep(5 * time.Second)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Stop 停止进程
|
||
func (p *Process) Stop() error {
|
||
close(p.stopCh)
|
||
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
|
||
if p.cmd == nil || p.cmd.Process == nil {
|
||
return nil
|
||
}
|
||
|
||
// 先尝试正常终止
|
||
if err := p.cmd.Process.Signal(os.Interrupt); err != nil {
|
||
log.Printf("Failed to send interrupt to process %s: %v", p.Config.ID, err)
|
||
}
|
||
|
||
// 等待5秒
|
||
timeout := time.After(5 * time.Second)
|
||
done := make(chan error, 1)
|
||
go func() {
|
||
_, err := p.cmd.Process.Wait()
|
||
done <- err
|
||
}()
|
||
|
||
select {
|
||
case <-timeout:
|
||
// 强制终止
|
||
if err := p.cmd.Process.Kill(); err != nil {
|
||
return fmt.Errorf("failed to kill process: %v", err)
|
||
}
|
||
case <-done:
|
||
}
|
||
|
||
p.Status.Status = "stopped"
|
||
|
||
// 关闭日志文件
|
||
if p.log != nil {
|
||
p.log.Close()
|
||
}
|
||
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 3. RPC 服务器 (`daemon/rpc_server.go`)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"log"
|
||
"net"
|
||
"net/http"
|
||
"strings"
|
||
)
|
||
|
||
// RPCRequest RPC请求
|
||
type RPCRequest struct {
|
||
Method string `json:"method"`
|
||
Params json.RawMessage `json:"params"`
|
||
ID int `json:"id"`
|
||
}
|
||
|
||
// RPCResponse RPC响应
|
||
type RPCResponse struct {
|
||
Result interface{} `json:"result,omitempty"`
|
||
Error string `json:"error,omitempty"`
|
||
ID int `json:"id"`
|
||
}
|
||
|
||
// RPC方法处理
|
||
type RPCHandler struct {
|
||
pm *ProcessManager
|
||
}
|
||
|
||
func startRPCServer(pm *ProcessManager) {
|
||
handler := &RPCHandler{pm: pm}
|
||
|
||
http.HandleFunc("/rpc", func(w http.ResponseWriter, r *http.Request) {
|
||
var req RPCRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
var resp RPCResponse
|
||
resp.ID = req.ID
|
||
|
||
switch strings.ToLower(req.Method) {
|
||
case "start":
|
||
var cfg ProcessConfig
|
||
if err := json.Unmarshal(req.Params, &cfg); err != nil {
|
||
resp.Error = "Invalid params"
|
||
} else {
|
||
if err := pm.StartProcess(cfg); err != nil {
|
||
resp.Error = err.Error()
|
||
} else {
|
||
resp.Result = "Process started"
|
||
}
|
||
}
|
||
|
||
case "stop":
|
||
var id string
|
||
if err := json.Unmarshal(req.Params, &id); err != nil {
|
||
resp.Error = "Invalid params"
|
||
} else {
|
||
if err := pm.StopProcess(id); err != nil {
|
||
resp.Error = err.Error()
|
||
} else {
|
||
resp.Result = "Process stopped"
|
||
}
|
||
}
|
||
|
||
case "restart":
|
||
var id string
|
||
if err := json.Unmarshal(req.Params, &id); err != nil {
|
||
resp.Error = "Invalid params"
|
||
} else {
|
||
if err := pm.RestartProcess(id); err != nil {
|
||
resp.Error = err.Error()
|
||
} else {
|
||
resp.Result = "Process restarted"
|
||
}
|
||
}
|
||
|
||
case "list":
|
||
resp.Result = pm.ListProcesses()
|
||
|
||
case "status":
|
||
var id string
|
||
if err := json.Unmarshal(req.Params, &id); err != nil {
|
||
resp.Error = "Invalid params"
|
||
} else {
|
||
status, err := pm.GetProcessStatus(id)
|
||
if err != nil {
|
||
resp.Error = err.Error()
|
||
} else {
|
||
resp.Result = status
|
||
}
|
||
}
|
||
|
||
default:
|
||
resp.Error = "Method not found"
|
||
}
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(resp)
|
||
})
|
||
|
||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", daemonPort))
|
||
if err != nil {
|
||
log.Fatalf("Failed to start RPC server: %v", err)
|
||
}
|
||
|
||
log.Printf("RPC server listening on %s", listener.Addr())
|
||
|
||
if err := http.Serve(listener, nil); err != nil {
|
||
log.Fatalf("RPC server failed: %v", err)
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. Web 管理界面 (`daemon/web_ui.go`)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"text/template"
|
||
)
|
||
|
||
func startWebUI(pm *ProcessManager) {
|
||
// 静态文件服务
|
||
fs := http.FileServer(http.Dir("web/static"))
|
||
http.Handle("/static/", http.StripPrefix("/static/", fs))
|
||
|
||
// 主页
|
||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||
tmpl, err := template.ParseFiles("web/templates/index.html")
|
||
if err != nil {
|
||
http.Error(w, "Template error", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
data := struct {
|
||
Processes []ProcessStatus
|
||
}{
|
||
Processes: pm.ListProcesses(),
|
||
}
|
||
|
||
tmpl.Execute(w, data)
|
||
})
|
||
|
||
// API端点
|
||
http.HandleFunc("/api/processes", func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(pm.ListProcesses())
|
||
})
|
||
|
||
// 启动Web服务器
|
||
port := 8080
|
||
log.Printf("Web UI available at http://localhost:%d", port)
|
||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
|
||
}
|
||
```
|
||
|
||
### 5. 命令行客户端 (`client/pm2-win-cli.go`)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
)
|
||
|
||
type Client struct {
|
||
ServerURL string
|
||
}
|
||
|
||
func NewClient() *Client {
|
||
return &Client{
|
||
ServerURL: "http://localhost:9999/rpc",
|
||
}
|
||
}
|
||
|
||
func (c *Client) Call(method string, params interface{}) (interface{}, error) {
|
||
request := map[string]interface{}{
|
||
"method": method,
|
||
"params": params,
|
||
"id": 1,
|
||
}
|
||
|
||
reqBody, err := json.Marshal(request)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
resp, err := http.Post(c.ServerURL, "application/json", bytes.NewReader(reqBody))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
var rpcResp struct {
|
||
Result interface{} `json:"result"`
|
||
Error string `json:"error"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&rpcResp); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if rpcResp.Error != "" {
|
||
return nil, fmt.Errorf(rpcResp.Error)
|
||
}
|
||
|
||
return rpcResp.Result, nil
|
||
}
|
||
|
||
func main() {
|
||
startCmd := flag.NewFlagSet("start", flag.ExitOnError)
|
||
startID := startCmd.String("id", "", "Process ID")
|
||
startName := startCmd.String("name", "", "Process name")
|
||
startCmd := startCmd.String("cmd", "", "Command to execute")
|
||
startArgs := startCmd.String("args", "", "Command arguments (comma separated)")
|
||
startDir := startCmd.String("dir", "", "Working directory")
|
||
startLog := startCmd.String("log", "", "Log file path")
|
||
|
||
stopCmd := flag.NewFlagSet("stop", flag.ExitOnError)
|
||
stopID := stopCmd.String("id", "", "Process ID")
|
||
|
||
restartCmd := flag.NewFlagSet("restart", flag.ExitOnError)
|
||
restartID := restartCmd.String("id", "", "Process ID")
|
||
|
||
listCmd := flag.NewFlagSet("list", flag.ExitOnError)
|
||
|
||
if len(os.Args) < 2 {
|
||
fmt.Println("Usage: pm2-win-cli <command> [options]")
|
||
fmt.Println("Commands:")
|
||
fmt.Println(" start - Start a new process")
|
||
fmt.Println(" stop - Stop a process")
|
||
fmt.Println(" restart - Restart a process")
|
||
fmt.Println(" list - List all processes")
|
||
os.Exit(1)
|
||
}
|
||
|
||
client := NewClient()
|
||
|
||
switch os.Args[1] {
|
||
case "start":
|
||
startCmd.Parse(os.Args[2:])
|
||
if *startID == "" || *startCmd == "" {
|
||
fmt.Println("ID and command are required")
|
||
os.Exit(1)
|
||
}
|
||
|
||
args := []string{}
|
||
if *startArgs != "" {
|
||
args = strings.Split(*startArgs, ",")
|
||
}
|
||
|
||
config := map[string]interface{}{
|
||
"id": *startID,
|
||
"name": *startName,
|
||
"command": *startCmd,
|
||
"args": args,
|
||
"working_dir": *startDir,
|
||
"log_file": *startLog,
|
||
"restart": true,
|
||
"max_restarts": 10,
|
||
}
|
||
|
||
result, err := client.Call("start", config)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Println(result)
|
||
|
||
case "stop":
|
||
stopCmd.Parse(os.Args[2:])
|
||
if *stopID == "" {
|
||
fmt.Println("ID is required")
|
||
os.Exit(1)
|
||
}
|
||
|
||
result, err := client.Call("stop", *stopID)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Println(result)
|
||
|
||
case "restart":
|
||
restartCmd.Parse(os.Args[2:])
|
||
if *restartID == "" {
|
||
fmt.Println("ID is required")
|
||
os.Exit(1)
|
||
}
|
||
|
||
result, err := client.Call("restart", *restartID)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Println(result)
|
||
|
||
case "list":
|
||
listCmd.Parse(os.Args[2:])
|
||
result, err := client.Call("list", nil)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
processes := result.([]interface{})
|
||
fmt.Println("ID\tName\tStatus\tPID\tRestarts\tUptime")
|
||
for _, p := range processes {
|
||
proc := p.(map[string]interface{})
|
||
fmt.Printf("%s\t%s\t%s\t%d\t%d\t%s\n",
|
||
proc["id"], proc["name"], proc["status"],
|
||
int(proc["pid"].(float64)), int(proc["restarts"].(float64)),
|
||
proc["uptime"])
|
||
}
|
||
|
||
default:
|
||
fmt.Println("Unknown command:", os.Args[1])
|
||
os.Exit(1)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 系统功能详解
|
||
|
||
### 1. 守护进程 (`daemon/main.go`)
|
||
|
||
- **服务管理**:
|
||
- `installService()`: 将守护进程安装为 Windows 服务
|
||
- `uninstallService()`: 卸载服务
|
||
- `runService()`: 作为 Windows 服务运行
|
||
- `runConsoleMode()`: 控制台模式运行(调试用)
|
||
|
||
- **核心功能**:
|
||
- `startDaemon()`: 启动守护主逻辑
|
||
- `serviceHandler`: Windows 服务处理接口实现
|
||
|
||
### 2. 进程管理器 (`daemon/process_manager.go`)
|
||
|
||
- **数据结构**:
|
||
- `ProcessConfig`: 进程配置(ID、命令、参数、日志文件等)
|
||
- `ProcessStatus`: 进程状态(PID、状态、重启次数等)
|
||
- `Process`: 被守护的进程实例
|
||
- `ProcessManager`: 管理所有进程的中央控制器
|
||
|
||
- **核心方法**:
|
||
- `StartProcess()`: 根据配置启动新进程
|
||
- `StopProcess()`: 停止指定进程
|
||
- `RestartProcess()`: 重启指定进程
|
||
- `ListProcesses()`: 列出所有进程状态
|
||
- `StartMonitor()`: 监控进程状态并自动重启
|
||
- `Run()`: 进程运行逻辑(包含自动重启机制)
|
||
- `Stop()`: 停止进程(优雅终止)
|
||
|
||
### 3. RPC 服务器 (`daemon/rpc_server.go`)
|
||
|
||
- **通信协议**:
|
||
- 基于 HTTP/JSON 的简单 RPC 协议
|
||
- 支持的方法: start, stop, restart, list, status
|
||
|
||
- **请求处理**:
|
||
- 解析客户端请求
|
||
- 调用对应的进程管理方法
|
||
- 返回结果或错误信息
|
||
|
||
### 4. Web 管理界面 (`daemon/web_ui.go`)
|
||
|
||
- **功能组件**:
|
||
- 静态文件服务: 提供 CSS/JS 资源
|
||
- 主页模板: 显示进程列表
|
||
- API 端点: 提供进程数据接口
|
||
|
||
- **用户界面**:
|
||
- 进程列表视图
|
||
- 启动/停止/重启按钮
|
||
- 实时状态更新
|
||
|
||
### 5. 命令行客户端 (`client/pm2-win-cli.go`)
|
||
|
||
- **命令支持**:
|
||
- `start`: 启动新进程
|
||
- `stop`: 停止进程
|
||
- `restart`: 重启进程
|
||
- `list`: 列出所有进程
|
||
|
||
- **参数解析**:
|
||
- 使用 `flag` 包解析命令行参数
|
||
- 验证必要参数
|
||
- 构造 RPC 请求
|
||
|
||
## 使用示例
|
||
|
||
### 1. 安装守护进程服务
|
||
|
||
```powershell
|
||
# 编译守护进程
|
||
go build -o pm2-win-daemon.exe daemon/main.go
|
||
|
||
# 安装服务
|
||
pm2-win-daemon.exe -install
|
||
```
|
||
|
||
### 2. 启动服务
|
||
|
||
```powershell
|
||
Start-Service -Name PM2WinDaemon
|
||
```
|
||
|
||
### 3. 使用命令行客户端
|
||
|
||
```powershell
|
||
# 编译客户端
|
||
go build -o pm2-win-cli.exe client/pm2-win-cli.go
|
||
|
||
# 启动 Node.js 应用
|
||
pm2-win-cli.exe start -id myapp -name "My Node App" -cmd "node" -args "app.js" -dir "C:\myapp" -log "C:\logs\myapp.log"
|
||
|
||
# 列出所有进程
|
||
pm2-win-cli.exe list
|
||
|
||
# 停止进程
|
||
pm2-win-cli.exe stop -id myapp
|
||
|
||
# 重启进程
|
||
pm2-win-cli.exe restart -id myapp
|
||
```
|
||
|
||
### 4. 访问 Web 界面
|
||
|
||
打开浏览器访问: `http://localhost:8080`
|
||
|
||
## 生产环境增强建议
|
||
|
||
1. **安全加固**:
|
||
- 添加 RPC 认证
|
||
- 使用 HTTPS
|
||
- 限制访问 IP
|
||
|
||
2. **监控集成**:
|
||
- 添加 Prometheus 指标端点
|
||
- 集成 Windows 事件日志
|
||
- 实现健康检查 API
|
||
|
||
3. **高级功能**:
|
||
- 进程资源限制(CPU/内存)
|
||
- 配置文件管理
|
||
- 日志轮转
|
||
- 集群模式支持
|
||
|
||
4. **用户界面增强**:
|
||
- 实时日志查看器
|
||
- 性能图表
|
||
- 配置文件编辑器
|
||
|
||
5. **部署优化**:
|
||
- 制作安装程序
|
||
- 添加自动更新功能
|
||
- 提供系统托盘图标
|
||
|
||
这个简易的 PM2 式进程守护系统提供了核心的进程管理功能,包括启动、停止、重启、监控和自动重启。通过命令行客户端和 Web 界面,用户可以方便地管理后台进程。系统设计为 Windows 服务运行,确保长期稳定工作,适合在 Windows 服务器上部署各种后台应用和服务。 |