Подготовить HY2XS к production-сборке
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"hy2xs-admin/model/constant"
|
||||
"hy2xs-admin/util"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Hysteria2Process struct {
|
||||
process
|
||||
binPath string
|
||||
configPath string
|
||||
}
|
||||
|
||||
var mutexHysteria2 sync.Mutex
|
||||
var cmdHysteria2 exec.Cmd
|
||||
var hysteria2Instance *Hysteria2Process
|
||||
|
||||
func init() {
|
||||
hysteria2Instance = &Hysteria2Process{process{mutex: &mutexHysteria2, cmd: &cmdHysteria2}, util.GetHysteria2BinPath(), constant.Hysteria2ConfigPath}
|
||||
}
|
||||
|
||||
func NewHysteria2Instance() *Hysteria2Process {
|
||||
return hysteria2Instance
|
||||
}
|
||||
|
||||
func (h *Hysteria2Process) IsRunning() bool {
|
||||
return h.isRunning()
|
||||
}
|
||||
|
||||
func (h *Hysteria2Process) StartHysteria2() error {
|
||||
if err := h.start(h.binPath, "-c", h.configPath, "server"); err != nil {
|
||||
_ = util.RemoveFile(h.configPath)
|
||||
logrus.Errorf("start hysteria2 err: %v", err)
|
||||
return errors.New("start hysteria2 err")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hysteria2Process) StopHysteria2() error {
|
||||
if err := h.stop(); err != nil {
|
||||
logrus.Errorf("stop hysteria2 err: %v", err)
|
||||
return errors.New("stop hysteria2 err")
|
||||
}
|
||||
_ = util.RemoveFile(h.configPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Hysteria2Process) Release() error {
|
||||
if err := h.release(); err != nil {
|
||||
logrus.Errorf("release hysteria2 err: %v", err)
|
||||
return errors.New("release hysteria2 err")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"hy2xs-admin/model/bo"
|
||||
"hy2xs-admin/model/constant"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Hysteria2Api struct {
|
||||
apiPort int64
|
||||
}
|
||||
|
||||
func NewHysteria2Api(apiPort int64) *Hysteria2Api {
|
||||
return &Hysteria2Api{
|
||||
apiPort: apiPort,
|
||||
}
|
||||
}
|
||||
|
||||
// ListUsers 每个用户的流量信息
|
||||
func (h *Hysteria2Api) ListUsers(clear bool, secret string) (map[string]bo.Hysteria2UserTraffic, error) {
|
||||
var users map[string]bo.Hysteria2UserTraffic
|
||||
if !NewHysteria2Instance().IsRunning() {
|
||||
return users, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/traffic", h.apiPort)
|
||||
if clear {
|
||||
url = fmt.Sprintf("%s?clear=1", url)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 ListUsers NewRequest err: %v", err)
|
||||
return nil, errors.New(constant.SysError)
|
||||
}
|
||||
req.Header.Set("Authorization", secret)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
logrus.Errorf("Hysteria2 ListUsers err: %v", err)
|
||||
return nil, errors.New("http connection error")
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 io read err: %v", err)
|
||||
return nil, errors.New("http connection error")
|
||||
}
|
||||
if err = json.Unmarshal(body, &users); err != nil {
|
||||
logrus.Errorf("Hysteria2 ListUsers Unmarshal err: %v", err)
|
||||
return nil, errors.New(constant.SysError)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// KickUsers 踢下线
|
||||
func (h *Hysteria2Api) KickUsers(keys []string, secret string) error {
|
||||
if !NewHysteria2Instance().IsRunning() {
|
||||
return nil
|
||||
}
|
||||
usernamesByte, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 KickUsers Marshal err: %v", err)
|
||||
return errors.New(constant.SysError)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/kick", h.apiPort)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url,
|
||||
bytes.NewBuffer(usernamesByte))
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 KickUsers NewRequest err: %v", err)
|
||||
return errors.New(constant.SysError)
|
||||
}
|
||||
req.Header.Set("Authorization", secret)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
logrus.Errorf("Hysteria2 KickUsers err: %v", err)
|
||||
return errors.New("http connection error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OnlineUsers 在线用户
|
||||
func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) {
|
||||
var onlineUsers map[string]int64
|
||||
if !NewHysteria2Instance().IsRunning() {
|
||||
return onlineUsers, nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
url := fmt.Sprintf("http://127.0.0.1:%d/online", h.apiPort)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 OnlineUsers NewRequest err: %v", err)
|
||||
return nil, errors.New(constant.SysError)
|
||||
}
|
||||
req.Header.Set("Authorization", secret)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
defer func() {
|
||||
if resp != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil || resp.StatusCode != http.StatusOK {
|
||||
logrus.Errorf("Hysteria2 OnlineUsers err: %v", err)
|
||||
return nil, errors.New("http connection error")
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
logrus.Errorf("Hysteria2 io read err: %v", err)
|
||||
return nil, errors.New("http connection error")
|
||||
}
|
||||
if err = json.Unmarshal(body, &onlineUsers); err != nil {
|
||||
logrus.Errorf("Hysteria2 OnlineUsers Unmarshal err: %v", err)
|
||||
return nil, errors.New(constant.SysError)
|
||||
}
|
||||
return onlineUsers, nil
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
"hy2xs-admin/model/constant"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var logger logrus.Logger
|
||||
|
||||
func initLogger() {
|
||||
logger.SetOutput(&lumberjack.Logger{
|
||||
Filename: constant.Hysteria2LogPath,
|
||||
MaxSize: 1,
|
||||
MaxBackups: 2,
|
||||
MaxAge: 30,
|
||||
Compress: true,
|
||||
LocalTime: true,
|
||||
})
|
||||
logger.SetFormatter(&logrus.JSONFormatter{TimestampFormat: "2006-01-02 15:04:05"})
|
||||
logger.SetLevel(logrus.InfoLevel)
|
||||
}
|
||||
|
||||
func init() {
|
||||
initLogger()
|
||||
}
|
||||
|
||||
type process struct {
|
||||
mutex *sync.Mutex
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func (p *process) isRunning() bool {
|
||||
return p.cmd != nil && p.cmd.Process != nil && p.cmd.ProcessState == nil
|
||||
}
|
||||
|
||||
func (p *process) start(name string, arg ...string) error {
|
||||
if !p.mutex.TryLock() {
|
||||
logrus.Errorf("start cmd err: lock not acquired")
|
||||
return errors.New("start cmd err")
|
||||
}
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if p.isRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(name, arg...)
|
||||
if cmd.Err != nil {
|
||||
logrus.Errorf("cmd err: %v", cmd.Err)
|
||||
return errors.New("cmd err")
|
||||
}
|
||||
|
||||
// 获取命令的 stdout 和 stderr
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error obtaining stdout: %v", err)
|
||||
return err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
logrus.Errorf("Error obtaining stderr: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logrus.Errorf("cmd start err: %v", err)
|
||||
return errors.New("cmd start err")
|
||||
}
|
||||
|
||||
p.cmd = cmd
|
||||
|
||||
go p.handleLogs(stdout, stderr)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *process) stop() error {
|
||||
if !p.mutex.TryLock() {
|
||||
return errors.New("cmd stop err: lock not acquired")
|
||||
}
|
||||
|
||||
if !p.isRunning() {
|
||||
p.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := p.cmd
|
||||
p.mutex.Unlock()
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
}()
|
||||
|
||||
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
|
||||
logrus.Warnf("send SIGTERM failed: %v", err)
|
||||
}
|
||||
|
||||
timer := time.NewTimer(3 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if normalizeExitErr(err, syscall.SIGTERM) != nil {
|
||||
return fmt.Errorf("process exit failed: %w", err)
|
||||
}
|
||||
|
||||
case <-timer.C:
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
return fmt.Errorf("SIGKILL failed: %w", err)
|
||||
}
|
||||
|
||||
err := <-done
|
||||
if normalizeExitErr(err, syscall.SIGKILL) != nil {
|
||||
return fmt.Errorf("process killed but exit abnormal: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
p.mutex.Lock()
|
||||
p.cmd = nil
|
||||
p.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeExitErr(err error, allowedSignals ...syscall.Signal) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
exitErr, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
status, ok := exitErr.Sys().(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
if status.Signaled() {
|
||||
sig := status.Signal()
|
||||
for _, s := range allowedSignals {
|
||||
if sig == s {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *process) release() error {
|
||||
if !p.mutex.TryLock() {
|
||||
logrus.Errorf("cmd release err: lock not acquired")
|
||||
return errors.New("cmd release err")
|
||||
}
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
if !p.isRunning() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := p.cmd.Process.Release(); err != nil {
|
||||
logrus.Errorf("cmd release err: %v", err)
|
||||
return errors.New("cmd release err")
|
||||
}
|
||||
p.cmd = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *process) handleLogs(stdout, stderr io.ReadCloser) {
|
||||
// 日志
|
||||
stdoutChan := make(chan string)
|
||||
stderrChan := make(chan string)
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
stdoutChan <- scanner.Text()
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logrus.Errorf("Error reading stdout: %v", err)
|
||||
}
|
||||
close(stdoutChan)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
stderrChan <- scanner.Text()
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
logrus.Errorf("Error reading stderr: %v", err)
|
||||
}
|
||||
close(stderrChan)
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case line, ok := <-stdoutChan:
|
||||
if !ok {
|
||||
stdoutChan = nil
|
||||
} else {
|
||||
logger.Infof(line)
|
||||
}
|
||||
case line, ok := <-stderrChan:
|
||||
if !ok {
|
||||
stderrChan = nil
|
||||
} else {
|
||||
logger.Errorf(line)
|
||||
}
|
||||
}
|
||||
|
||||
// 当两个 channel 都关闭时,退出循环
|
||||
if stdoutChan == nil && stderrChan == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user