换成单集群模式
This commit is contained in:
172
EdgeHttpDNS/internal/accesslogs/httpdns_file_writer.go
Normal file
172
EdgeHttpDNS/internal/accesslogs/httpdns_file_writer.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package accesslogs
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPDNSLogDir = "/var/log/edge/edge-httpdns"
|
||||
envHTTPDNSLogDir = "EDGE_HTTPDNS_LOG_DIR"
|
||||
)
|
||||
|
||||
var (
|
||||
sharedHTTPDNSFileWriter *HTTPDNSFileWriter
|
||||
sharedHTTPDNSFileWriterOnce sync.Once
|
||||
)
|
||||
|
||||
// SharedHTTPDNSFileWriter 返回 HTTPDNS 本地日志写入器(单例).
|
||||
func SharedHTTPDNSFileWriter() *HTTPDNSFileWriter {
|
||||
sharedHTTPDNSFileWriterOnce.Do(func() {
|
||||
sharedHTTPDNSFileWriter = NewHTTPDNSFileWriter()
|
||||
})
|
||||
return sharedHTTPDNSFileWriter
|
||||
}
|
||||
|
||||
// HTTPDNSFileWriter 将 HTTPDNS 访问日志以 JSON Lines 写入本地文件,供 Fluent Bit 采集。
|
||||
type HTTPDNSFileWriter struct {
|
||||
dir string
|
||||
mu sync.Mutex
|
||||
file *lumberjack.Logger
|
||||
rotateConfig *serverconfigs.AccessLogRotateConfig
|
||||
inited bool
|
||||
}
|
||||
|
||||
// NewHTTPDNSFileWriter 创建 HTTPDNS 本地日志写入器.
|
||||
func NewHTTPDNSFileWriter() *HTTPDNSFileWriter {
|
||||
logDir := resolveDefaultHTTPDNSLogDir()
|
||||
return &HTTPDNSFileWriter{
|
||||
dir: logDir,
|
||||
rotateConfig: serverconfigs.NewDefaultAccessLogRotateConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
func resolveDefaultHTTPDNSLogDir() string {
|
||||
logDir := strings.TrimSpace(os.Getenv(envHTTPDNSLogDir))
|
||||
if logDir == "" {
|
||||
return defaultHTTPDNSLogDir
|
||||
}
|
||||
return logDir
|
||||
}
|
||||
|
||||
// SetDir 更新日志目录并重置文件句柄。
|
||||
func (w *HTTPDNSFileWriter) SetDir(dir string) {
|
||||
if strings.TrimSpace(dir) == "" {
|
||||
dir = resolveDefaultHTTPDNSLogDir()
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if dir == w.dir {
|
||||
return
|
||||
}
|
||||
|
||||
if w.file != nil {
|
||||
_ = w.file.Close()
|
||||
w.file = nil
|
||||
}
|
||||
w.inited = false
|
||||
w.dir = dir
|
||||
}
|
||||
|
||||
// Dir 返回当前日志目录.
|
||||
func (w *HTTPDNSFileWriter) Dir() string {
|
||||
return w.dir
|
||||
}
|
||||
|
||||
// EnsureInit 在启动时预创建目录与 access.log.
|
||||
func (w *HTTPDNSFileWriter) EnsureInit() error {
|
||||
if w.dir == "" {
|
||||
return nil
|
||||
}
|
||||
return w.init()
|
||||
}
|
||||
|
||||
func (w *HTTPDNSFileWriter) init() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.inited && w.file != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if w.dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(w.dir, 0755); err != nil {
|
||||
log.Println("[HTTPDNS_ACCESS_LOG]mkdir log dir failed:", err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
rotateConfig := w.rotateConfig.Normalize()
|
||||
w.file = &lumberjack.Logger{
|
||||
Filename: filepath.Join(w.dir, "access.log"),
|
||||
MaxSize: rotateConfig.MaxSizeMB,
|
||||
MaxBackups: rotateConfig.MaxBackups,
|
||||
MaxAge: rotateConfig.MaxAgeDays,
|
||||
Compress: *rotateConfig.Compress,
|
||||
LocalTime: *rotateConfig.LocalTime,
|
||||
}
|
||||
|
||||
w.inited = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteBatch 批量写入 HTTPDNS 访问日志.
|
||||
func (w *HTTPDNSFileWriter) WriteBatch(logs []*pb.HTTPDNSAccessLog) {
|
||||
if len(logs) == 0 || w.dir == "" {
|
||||
return
|
||||
}
|
||||
if err := w.init(); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.mu.Lock()
|
||||
file := w.file
|
||||
w.mu.Unlock()
|
||||
if file == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, logItem := range logs {
|
||||
ingestLog := FromPBAccessLog(logItem)
|
||||
if ingestLog == nil {
|
||||
continue
|
||||
}
|
||||
line, err := json.Marshal(ingestLog)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
_, _ = file.Write(append(line, '\n'))
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭日志文件.
|
||||
func (w *HTTPDNSFileWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := w.file.Close()
|
||||
w.file = nil
|
||||
w.inited = false
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("[HTTPDNS_ACCESS_LOG]close access.log failed: %v", err))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
57
EdgeHttpDNS/internal/accesslogs/httpdns_ingest_log.go
Normal file
57
EdgeHttpDNS/internal/accesslogs/httpdns_ingest_log.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package accesslogs
|
||||
|
||||
import "github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
|
||||
// HTTPDNSIngestLog HTTPDNS 访问日志单行结构(JSONEachRow),字段与
|
||||
// ClickHouse httpdns_access_logs_ingest 表一一对应。
|
||||
type HTTPDNSIngestLog struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
RequestId string `json:"request_id"`
|
||||
ClusterId int64 `json:"cluster_id"`
|
||||
NodeId int64 `json:"node_id"`
|
||||
AppId string `json:"app_id"`
|
||||
AppName string `json:"app_name"`
|
||||
Domain string `json:"domain"`
|
||||
QType string `json:"qtype"`
|
||||
ClientIP string `json:"client_ip"`
|
||||
ClientRegion string `json:"client_region"`
|
||||
Carrier string `json:"carrier"`
|
||||
SDKVersion string `json:"sdk_version"`
|
||||
OS string `json:"os"`
|
||||
ResultIPs string `json:"result_ips"`
|
||||
Status string `json:"status"`
|
||||
ErrorCode string `json:"error_code"`
|
||||
CostMs int32 `json:"cost_ms"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Day string `json:"day"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// FromPBAccessLog 将 pb.HTTPDNSAccessLog 转为本地 ingest 结构。
|
||||
func FromPBAccessLog(log *pb.HTTPDNSAccessLog) *HTTPDNSIngestLog {
|
||||
if log == nil {
|
||||
return nil
|
||||
}
|
||||
return &HTTPDNSIngestLog{
|
||||
Timestamp: log.GetCreatedAt(),
|
||||
RequestId: log.GetRequestId(),
|
||||
ClusterId: log.GetClusterId(),
|
||||
NodeId: log.GetNodeId(),
|
||||
AppId: log.GetAppId(),
|
||||
AppName: log.GetAppName(),
|
||||
Domain: log.GetDomain(),
|
||||
QType: log.GetQtype(),
|
||||
ClientIP: log.GetClientIP(),
|
||||
ClientRegion: log.GetClientRegion(),
|
||||
Carrier: log.GetCarrier(),
|
||||
SDKVersion: log.GetSdkVersion(),
|
||||
OS: log.GetOs(),
|
||||
ResultIPs: log.GetResultIPs(),
|
||||
Status: log.GetStatus(),
|
||||
ErrorCode: log.GetErrorCode(),
|
||||
CostMs: log.GetCostMs(),
|
||||
CreatedAt: log.GetCreatedAt(),
|
||||
Day: log.GetDay(),
|
||||
Summary: log.GetSummary(),
|
||||
}
|
||||
}
|
||||
@@ -13,14 +13,18 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/iplibrary"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/rpc/pb"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs"
|
||||
"github.com/TeaOSLab/EdgeCommon/pkg/serverconfigs/sslconfigs"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/accesslogs"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/configs"
|
||||
"github.com/TeaOSLab/EdgeHttpDNS/internal/rpc"
|
||||
"github.com/iwind/TeaGo/rands"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
@@ -83,61 +87,79 @@ type clientRouteProfile struct {
|
||||
RegionText string
|
||||
}
|
||||
|
||||
type ResolveServer struct {
|
||||
quitCh <-chan struct{}
|
||||
type tlsListener struct {
|
||||
addr string // e.g. ":443"
|
||||
listener net.Listener
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
type ResolveServer struct {
|
||||
quitCh <-chan struct{}
|
||||
snapshotManager *SnapshotManager
|
||||
|
||||
listenAddr string
|
||||
certFile string
|
||||
keyFile string
|
||||
server *http.Server
|
||||
// Local config fallback
|
||||
fallbackAddr string
|
||||
certFile string
|
||||
keyFile string
|
||||
|
||||
logQueue chan *pb.HTTPDNSAccessLog
|
||||
handler http.Handler // shared mux
|
||||
tlsConfig *tls.Config // shared TLS config (with GetCertificate)
|
||||
|
||||
logWriter *accesslogs.HTTPDNSFileWriter
|
||||
logQueue chan *pb.HTTPDNSAccessLog
|
||||
|
||||
// TLS certificate hot-reload
|
||||
certMu sync.RWMutex
|
||||
currentCert *tls.Certificate
|
||||
certSnapshotAt int64
|
||||
|
||||
// Listener hot-reload
|
||||
listenerMu sync.Mutex
|
||||
listeners map[string]*tlsListener // key: addr (e.g. ":443")
|
||||
}
|
||||
|
||||
func NewResolveServer(quitCh <-chan struct{}, snapshotManager *SnapshotManager) *ResolveServer {
|
||||
listenAddr := ":443"
|
||||
fallbackAddr := ":443"
|
||||
certFile := ""
|
||||
keyFile := ""
|
||||
if apiConfig, err := configs.SharedAPIConfig(); err == nil && apiConfig != nil {
|
||||
if len(apiConfig.HTTPSListenAddr) > 0 {
|
||||
listenAddr = apiConfig.HTTPSListenAddr
|
||||
fallbackAddr = apiConfig.HTTPSListenAddr
|
||||
}
|
||||
certFile = apiConfig.HTTPSCert
|
||||
keyFile = apiConfig.HTTPSKey
|
||||
}
|
||||
|
||||
logWriter := accesslogs.SharedHTTPDNSFileWriter()
|
||||
if apiConfig, err := configs.SharedAPIConfig(); err == nil && apiConfig != nil {
|
||||
if len(strings.TrimSpace(apiConfig.LogDir)) > 0 {
|
||||
logWriter.SetDir(strings.TrimSpace(apiConfig.LogDir))
|
||||
}
|
||||
}
|
||||
if err := logWriter.EnsureInit(); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]init access log file writer failed:", err.Error())
|
||||
}
|
||||
|
||||
instance := &ResolveServer{
|
||||
quitCh: quitCh,
|
||||
snapshotManager: snapshotManager,
|
||||
listenAddr: listenAddr,
|
||||
fallbackAddr: fallbackAddr,
|
||||
certFile: certFile,
|
||||
keyFile: keyFile,
|
||||
logWriter: logWriter,
|
||||
logQueue: make(chan *pb.HTTPDNSAccessLog, 8192),
|
||||
listeners: make(map[string]*tlsListener),
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/resolve", instance.handleResolve)
|
||||
mux.HandleFunc("/healthz", instance.handleHealth)
|
||||
instance.handler = mux
|
||||
|
||||
instance.server = &http.Server{
|
||||
Addr: instance.listenAddr,
|
||||
Handler: mux,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 75 * time.Second,
|
||||
MaxHeaderBytes: 8 * 1024,
|
||||
TLSConfig: &tls.Config{
|
||||
MinVersion: tls.VersionTLS11,
|
||||
// /resolve is a small JSON API; pin to HTTP/1.1 to avoid ALPN/h2 handshake variance
|
||||
// across some clients and middleboxes.
|
||||
NextProtos: []string{"http/1.1"},
|
||||
},
|
||||
// Disable automatic HTTP/2 upgrade on TLS listeners. This keeps handshake behavior
|
||||
// deterministic for SDK resolve calls.
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||
instance.tlsConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS11,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
GetCertificate: instance.getCertificate,
|
||||
}
|
||||
|
||||
return instance
|
||||
@@ -145,19 +167,240 @@ func NewResolveServer(quitCh <-chan struct{}, snapshotManager *SnapshotManager)
|
||||
|
||||
func (s *ResolveServer) Start() {
|
||||
go s.startAccessLogFlusher()
|
||||
go s.waitForShutdown()
|
||||
|
||||
log.Println("[HTTPDNS_NODE][resolve]listening HTTPS on", s.listenAddr)
|
||||
if err := s.server.ListenAndServeTLS(s.certFile, s.keyFile); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Println("[HTTPDNS_NODE][resolve]listen failed:", err.Error())
|
||||
// 1. Load initial certificate from file (fallback)
|
||||
if len(s.certFile) > 0 && len(s.keyFile) > 0 {
|
||||
cert, err := tls.LoadX509KeyPair(s.certFile, s.keyFile)
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]load cert file failed:", err.Error())
|
||||
} else {
|
||||
s.currentCert = &cert
|
||||
log.Println("[HTTPDNS_NODE][resolve]loaded initial TLS cert from file")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Try loading certificate from cluster snapshot (takes priority over file)
|
||||
if snapshot := s.snapshotManager.Current(); snapshot != nil {
|
||||
s.reloadCertFromSnapshot(snapshot)
|
||||
}
|
||||
|
||||
if s.currentCert == nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]WARNING: no TLS certificate available, HTTPS will fail")
|
||||
}
|
||||
|
||||
// 3. Parse initial listen addresses and start listeners
|
||||
if snapshot := s.snapshotManager.Current(); snapshot != nil {
|
||||
addrs := s.desiredAddrs(snapshot)
|
||||
s.syncListeners(addrs)
|
||||
} else {
|
||||
s.syncListeners([]string{s.fallbackAddr})
|
||||
}
|
||||
|
||||
// 4. Watch for changes (blocks until quit)
|
||||
s.watchLoop()
|
||||
}
|
||||
|
||||
func (s *ResolveServer) getCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
s.certMu.RLock()
|
||||
cert := s.currentCert
|
||||
s.certMu.RUnlock()
|
||||
if cert != nil {
|
||||
return cert, nil
|
||||
}
|
||||
return nil, errors.New("no TLS certificate available")
|
||||
}
|
||||
|
||||
type snapshotTLSConfig struct {
|
||||
Listen []*serverconfigs.NetworkAddressConfig `json:"listen"`
|
||||
SSLPolicy *sslconfigs.SSLPolicy `json:"sslPolicy"`
|
||||
}
|
||||
|
||||
func (s *ResolveServer) parseTLSConfig(snapshot *LoadedSnapshot) *snapshotTLSConfig {
|
||||
if snapshot.ClusterID <= 0 {
|
||||
return nil
|
||||
}
|
||||
cluster := snapshot.Clusters[snapshot.ClusterID]
|
||||
if cluster == nil {
|
||||
return nil
|
||||
}
|
||||
raw := cluster.GetTlsPolicyJSON()
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var cfg snapshotTLSConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]parse tlsPolicyJSON failed:", err.Error())
|
||||
return nil
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (s *ResolveServer) desiredAddrs(snapshot *LoadedSnapshot) []string {
|
||||
cfg := s.parseTLSConfig(snapshot)
|
||||
if cfg == nil || len(cfg.Listen) == 0 {
|
||||
return []string{s.fallbackAddr}
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
var addrs []string
|
||||
for _, listenCfg := range cfg.Listen {
|
||||
if listenCfg == nil {
|
||||
continue
|
||||
}
|
||||
if err := listenCfg.Init(); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]init listen config failed:", err.Error())
|
||||
continue
|
||||
}
|
||||
for _, addr := range listenCfg.Addresses() {
|
||||
if _, ok := seen[addr]; !ok {
|
||||
seen[addr] = struct{}{}
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(addrs) == 0 {
|
||||
return []string{s.fallbackAddr}
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (s *ResolveServer) reloadCertFromSnapshot(snapshot *LoadedSnapshot) {
|
||||
cfg := s.parseTLSConfig(snapshot)
|
||||
if cfg == nil || cfg.SSLPolicy == nil || len(cfg.SSLPolicy.Certs) == 0 {
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
return
|
||||
}
|
||||
if err := cfg.SSLPolicy.Init(context.Background()); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]init SSLPolicy failed:", err.Error())
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
return
|
||||
}
|
||||
cert := cfg.SSLPolicy.FirstCert()
|
||||
if cert == nil {
|
||||
s.certMu.Lock()
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
s.certMu.Lock()
|
||||
s.currentCert = cert
|
||||
s.certSnapshotAt = snapshot.LoadedAt
|
||||
s.certMu.Unlock()
|
||||
log.Println("[HTTPDNS_NODE][resolve]TLS certificate reloaded from snapshot")
|
||||
}
|
||||
|
||||
func (s *ResolveServer) startListener(addr string) error {
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listen on %s: %w", addr, err)
|
||||
}
|
||||
tlsLn := tls.NewListener(ln, s.tlsConfig)
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: s.handler,
|
||||
ReadTimeout: 5 * time.Second,
|
||||
ReadHeaderTimeout: 3 * time.Second,
|
||||
WriteTimeout: 5 * time.Second,
|
||||
IdleTimeout: 75 * time.Second,
|
||||
MaxHeaderBytes: 8 * 1024,
|
||||
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||
}
|
||||
|
||||
s.listeners[addr] = &tlsListener{
|
||||
addr: addr,
|
||||
listener: tlsLn,
|
||||
server: srv,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := srv.Serve(tlsLn); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Println("[HTTPDNS_NODE][resolve]serve failed on", addr, ":", err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("[HTTPDNS_NODE][resolve]listening HTTPS on", addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ResolveServer) stopListener(addr string) {
|
||||
tl, ok := s.listeners[addr]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = tl.server.Shutdown(ctx)
|
||||
delete(s.listeners, addr)
|
||||
log.Println("[HTTPDNS_NODE][resolve]stopped listener on", addr)
|
||||
}
|
||||
|
||||
func (s *ResolveServer) syncListeners(desired []string) {
|
||||
s.listenerMu.Lock()
|
||||
defer s.listenerMu.Unlock()
|
||||
|
||||
desiredSet := make(map[string]struct{}, len(desired))
|
||||
for _, addr := range desired {
|
||||
desiredSet[addr] = struct{}{}
|
||||
}
|
||||
|
||||
// Stop listeners that are no longer desired
|
||||
for addr := range s.listeners {
|
||||
if _, ok := desiredSet[addr]; !ok {
|
||||
s.stopListener(addr)
|
||||
}
|
||||
}
|
||||
|
||||
// Start new listeners
|
||||
for _, addr := range desired {
|
||||
if _, ok := s.listeners[addr]; !ok {
|
||||
if err := s.startListener(addr); err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]start listener failed:", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ResolveServer) waitForShutdown() {
|
||||
<-s.quitCh
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
_ = s.server.Shutdown(ctx)
|
||||
func (s *ResolveServer) watchLoop() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
snapshot := s.snapshotManager.Current()
|
||||
if snapshot == nil {
|
||||
continue
|
||||
}
|
||||
s.certMu.RLock()
|
||||
lastAt := s.certSnapshotAt
|
||||
s.certMu.RUnlock()
|
||||
if snapshot.LoadedAt == lastAt {
|
||||
continue
|
||||
}
|
||||
// Snapshot changed — sync listeners and reload cert
|
||||
addrs := s.desiredAddrs(snapshot)
|
||||
s.syncListeners(addrs)
|
||||
s.reloadCertFromSnapshot(snapshot)
|
||||
case <-s.quitCh:
|
||||
s.shutdownAll()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ResolveServer) shutdownAll() {
|
||||
s.listenerMu.Lock()
|
||||
defer s.listenerMu.Unlock()
|
||||
for addr := range s.listeners {
|
||||
s.stopListener(addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ResolveServer) handleHealth(writer http.ResponseWriter, _ *http.Request) {
|
||||
@@ -219,11 +462,22 @@ func (s *ResolveServer) handleResolve(writer http.ResponseWriter, request *http.
|
||||
return
|
||||
}
|
||||
|
||||
if snapshot.ClusterID > 0 &&
|
||||
loadedApp.App.GetPrimaryClusterId() != snapshot.ClusterID &&
|
||||
loadedApp.App.GetBackupClusterId() != snapshot.ClusterID {
|
||||
s.writeFailedResolve(writer, requestID, snapshot, loadedApp.App, domain, qtype, httpdnsCodeAppInvalid, "当前应用未绑定到该解析集群", startAt, request, query)
|
||||
return
|
||||
if snapshot.ClusterID > 0 {
|
||||
var appClusterIds []int64
|
||||
if len(loadedApp.App.GetClusterIdsJSON()) > 0 {
|
||||
_ = json.Unmarshal(loadedApp.App.GetClusterIdsJSON(), &appClusterIds)
|
||||
}
|
||||
var clusterBound bool
|
||||
for _, cid := range appClusterIds {
|
||||
if cid == snapshot.ClusterID {
|
||||
clusterBound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !clusterBound {
|
||||
s.writeFailedResolve(writer, requestID, snapshot, loadedApp.App, domain, qtype, httpdnsCodeAppInvalid, "当前应用未绑定到该解析集群", startAt, request, query)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
loadedDomain := loadedApp.Domains[domain]
|
||||
@@ -341,11 +595,14 @@ func pickDefaultTTL(snapshot *LoadedSnapshot, app *pb.HTTPDNSApp) int32 {
|
||||
}
|
||||
}
|
||||
if app != nil {
|
||||
if cluster := snapshot.Clusters[app.GetPrimaryClusterId()]; cluster != nil && cluster.GetDefaultTTL() > 0 {
|
||||
return cluster.GetDefaultTTL()
|
||||
var appClusterIds []int64
|
||||
if len(app.GetClusterIdsJSON()) > 0 {
|
||||
_ = json.Unmarshal(app.GetClusterIdsJSON(), &appClusterIds)
|
||||
}
|
||||
if cluster := snapshot.Clusters[app.GetBackupClusterId()]; cluster != nil && cluster.GetDefaultTTL() > 0 {
|
||||
return cluster.GetDefaultTTL()
|
||||
for _, cid := range appClusterIds {
|
||||
if cluster := snapshot.Clusters[cid]; cluster != nil && cluster.GetDefaultTTL() > 0 {
|
||||
return cluster.GetDefaultTTL()
|
||||
}
|
||||
}
|
||||
}
|
||||
return 30
|
||||
@@ -591,15 +848,19 @@ func normalizeChinaRegion(province string) string {
|
||||
|
||||
func normalizeContinent(country string) string {
|
||||
switch normalizeCountryName(country) {
|
||||
case "中国", "中国香港", "中国澳门", "中国台湾", "日本", "韩国", "新加坡", "印度", "泰国", "越南":
|
||||
case "中国", "中国香港", "中国澳门", "中国台湾", "日本", "韩国", "新加坡", "印度", "泰国", "越南",
|
||||
"印度尼西亚", "马来西亚", "菲律宾", "柬埔寨", "缅甸", "老挝", "斯里兰卡", "孟加拉国", "巴基斯坦", "尼泊尔",
|
||||
"阿联酋", "沙特阿拉伯", "土耳其", "以色列", "伊朗", "伊拉克", "卡塔尔", "科威特", "蒙古":
|
||||
return "亚洲"
|
||||
case "美国", "加拿大", "墨西哥":
|
||||
case "美国", "加拿大", "墨西哥", "巴拿马", "哥斯达黎加", "古巴":
|
||||
return "北美洲"
|
||||
case "巴西", "阿根廷", "智利", "哥伦比亚":
|
||||
case "巴西", "阿根廷", "智利", "哥伦比亚", "秘鲁", "委内瑞拉", "厄瓜多尔":
|
||||
return "南美洲"
|
||||
case "德国", "英国", "法国", "荷兰", "西班牙", "意大利", "俄罗斯":
|
||||
case "德国", "英国", "法国", "荷兰", "西班牙", "意大利", "俄罗斯",
|
||||
"波兰", "瑞典", "瑞士", "挪威", "芬兰", "丹麦", "葡萄牙", "爱尔兰", "比利时", "奥地利",
|
||||
"乌克兰", "捷克", "罗马尼亚", "匈牙利", "希腊":
|
||||
return "欧洲"
|
||||
case "南非", "埃及", "尼日利亚", "肯尼亚", "摩洛哥":
|
||||
case "南非", "埃及", "尼日利亚", "肯尼亚", "摩洛哥", "阿尔及利亚", "坦桑尼亚", "埃塞俄比亚", "加纳", "突尼斯":
|
||||
return "非洲"
|
||||
case "澳大利亚", "新西兰":
|
||||
return "大洋洲"
|
||||
@@ -639,8 +900,8 @@ func pickRuleRecords(rules []*pb.HTTPDNSCustomRule, qtype string, profile *clien
|
||||
Type: qtype,
|
||||
IP: value,
|
||||
Weight: item.GetWeight(),
|
||||
Line: ruleLineSummary(rule),
|
||||
Region: ruleRegionSummary(rule),
|
||||
Line: lookupIPLineLabel(value),
|
||||
Region: lookupIPRegionSummary(value),
|
||||
})
|
||||
}
|
||||
if len(records) == 0 {
|
||||
@@ -969,6 +1230,91 @@ func normalizeCountryName(country string) string {
|
||||
return "智利"
|
||||
case "哥伦比亚", "colombia":
|
||||
return "哥伦比亚"
|
||||
// --- 亚洲(新增)---
|
||||
case "印度尼西亚", "indonesia":
|
||||
return "印度尼西亚"
|
||||
case "马来西亚", "malaysia":
|
||||
return "马来西亚"
|
||||
case "菲律宾", "philippines":
|
||||
return "菲律宾"
|
||||
case "柬埔寨", "cambodia":
|
||||
return "柬埔寨"
|
||||
case "缅甸", "myanmar", "burma":
|
||||
return "缅甸"
|
||||
case "老挝", "laos":
|
||||
return "老挝"
|
||||
case "斯里兰卡", "srilanka":
|
||||
return "斯里兰卡"
|
||||
case "孟加拉国", "孟加拉", "bangladesh":
|
||||
return "孟加拉国"
|
||||
case "巴基斯坦", "pakistan":
|
||||
return "巴基斯坦"
|
||||
case "尼泊尔", "nepal":
|
||||
return "尼泊尔"
|
||||
case "阿联酋", "阿拉伯联合酋长国", "uae", "unitedarabemirates":
|
||||
return "阿联酋"
|
||||
case "沙特阿拉伯", "沙特", "saudiarabia", "saudi":
|
||||
return "沙特阿拉伯"
|
||||
case "土耳其", "turkey", "türkiye", "turkiye":
|
||||
return "土耳其"
|
||||
case "以色列", "israel":
|
||||
return "以色列"
|
||||
case "伊朗", "iran":
|
||||
return "伊朗"
|
||||
case "伊拉克", "iraq":
|
||||
return "伊拉克"
|
||||
case "卡塔尔", "qatar":
|
||||
return "卡塔尔"
|
||||
case "科威特", "kuwait":
|
||||
return "科威特"
|
||||
case "蒙古", "mongolia":
|
||||
return "蒙古"
|
||||
// --- 欧洲(新增)---
|
||||
case "波兰", "poland":
|
||||
return "波兰"
|
||||
case "瑞典", "sweden":
|
||||
return "瑞典"
|
||||
case "瑞士", "switzerland":
|
||||
return "瑞士"
|
||||
case "挪威", "norway":
|
||||
return "挪威"
|
||||
case "芬兰", "finland":
|
||||
return "芬兰"
|
||||
case "丹麦", "denmark":
|
||||
return "丹麦"
|
||||
case "葡萄牙", "portugal":
|
||||
return "葡萄牙"
|
||||
case "爱尔兰", "ireland":
|
||||
return "爱尔兰"
|
||||
case "比利时", "belgium":
|
||||
return "比利时"
|
||||
case "奥地利", "austria":
|
||||
return "奥地利"
|
||||
case "乌克兰", "ukraine":
|
||||
return "乌克兰"
|
||||
case "捷克", "czech", "czechrepublic", "czechia":
|
||||
return "捷克"
|
||||
case "罗马尼亚", "romania":
|
||||
return "罗马尼亚"
|
||||
case "匈牙利", "hungary":
|
||||
return "匈牙利"
|
||||
case "希腊", "greece":
|
||||
return "希腊"
|
||||
// --- 北美洲(新增)---
|
||||
case "巴拿马", "panama":
|
||||
return "巴拿马"
|
||||
case "哥斯达黎加", "costarica":
|
||||
return "哥斯达黎加"
|
||||
case "古巴", "cuba":
|
||||
return "古巴"
|
||||
// --- 南美洲(新增)---
|
||||
case "秘鲁", "peru":
|
||||
return "秘鲁"
|
||||
case "委内瑞拉", "venezuela":
|
||||
return "委内瑞拉"
|
||||
case "厄瓜多尔", "ecuador":
|
||||
return "厄瓜多尔"
|
||||
// --- 非洲 ---
|
||||
case "南非", "southafrica":
|
||||
return "南非"
|
||||
case "埃及", "egypt":
|
||||
@@ -979,6 +1325,17 @@ func normalizeCountryName(country string) string {
|
||||
return "肯尼亚"
|
||||
case "摩洛哥", "morocco":
|
||||
return "摩洛哥"
|
||||
case "阿尔及利亚", "algeria":
|
||||
return "阿尔及利亚"
|
||||
case "坦桑尼亚", "tanzania":
|
||||
return "坦桑尼亚"
|
||||
case "埃塞俄比亚", "ethiopia":
|
||||
return "埃塞俄比亚"
|
||||
case "加纳", "ghana":
|
||||
return "加纳"
|
||||
case "突尼斯", "tunisia":
|
||||
return "突尼斯"
|
||||
// --- 大洋洲 ---
|
||||
case "澳大利亚", "australia":
|
||||
return "澳大利亚"
|
||||
case "新西兰", "newzealand":
|
||||
@@ -1087,18 +1444,7 @@ func (s *ResolveServer) startAccessLogFlusher() {
|
||||
if len(batch) == 0 {
|
||||
return
|
||||
}
|
||||
rpcClient, err := rpc.SharedRPC()
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]access-log rpc unavailable:", err.Error())
|
||||
return
|
||||
}
|
||||
_, err = rpcClient.HTTPDNSAccessLogRPC.CreateHTTPDNSAccessLogs(rpcClient.Context(), &pb.CreateHTTPDNSAccessLogsRequest{
|
||||
Logs: batch,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println("[HTTPDNS_NODE][resolve]flush access logs failed:", err.Error())
|
||||
return
|
||||
}
|
||||
s.logWriter.WriteBatch(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
@@ -1119,6 +1465,7 @@ func (s *ResolveServer) startAccessLogFlusher() {
|
||||
flush()
|
||||
case <-s.quitCh:
|
||||
flush()
|
||||
_ = s.logWriter.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
160
EdgeHttpDNS/internal/utils/exec/cmd.go
Normal file
160
EdgeHttpDNS/internal/utils/exec/cmd.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package executils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
args []string
|
||||
env []string
|
||||
dir string
|
||||
|
||||
ctx context.Context
|
||||
timeout time.Duration
|
||||
cancelFunc func()
|
||||
|
||||
captureStdout bool
|
||||
captureStderr bool
|
||||
|
||||
stdout *bytes.Buffer
|
||||
stderr *bytes.Buffer
|
||||
|
||||
rawCmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewCmd(name string, args ...string) *Cmd {
|
||||
return &Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||
return (&Cmd{
|
||||
name: name,
|
||||
args: args,
|
||||
}).WithTimeout(timeout)
|
||||
}
|
||||
|
||||
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||
this.timeout = timeout
|
||||
|
||||
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||
this.ctx = ctx
|
||||
this.cancelFunc = cancelFunc
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStdout() *Cmd {
|
||||
this.captureStdout = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithStderr() *Cmd {
|
||||
this.captureStderr = true
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||
this.env = env
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||
this.dir = dir
|
||||
return this
|
||||
}
|
||||
|
||||
func (this *Cmd) Start() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Start()
|
||||
}
|
||||
|
||||
func (this *Cmd) Wait() error {
|
||||
var cmd = this.compose()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
func (this *Cmd) Run() error {
|
||||
if this.cancelFunc != nil {
|
||||
defer this.cancelFunc()
|
||||
}
|
||||
|
||||
var cmd = this.compose()
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStdout() string {
|
||||
if this.stdout != nil {
|
||||
return this.stdout.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stdout() string {
|
||||
return strings.TrimSpace(this.RawStdout())
|
||||
}
|
||||
|
||||
func (this *Cmd) RawStderr() string {
|
||||
if this.stderr != nil {
|
||||
return this.stderr.String()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (this *Cmd) Stderr() string {
|
||||
return strings.TrimSpace(this.RawStderr())
|
||||
}
|
||||
|
||||
func (this *Cmd) String() string {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.String()
|
||||
}
|
||||
var newCmd = exec.Command(this.name, this.args...)
|
||||
return newCmd.String()
|
||||
}
|
||||
|
||||
func (this *Cmd) Process() *os.Process {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd.Process
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (this *Cmd) compose() *exec.Cmd {
|
||||
if this.rawCmd != nil {
|
||||
return this.rawCmd
|
||||
}
|
||||
|
||||
if this.ctx != nil {
|
||||
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||
} else {
|
||||
this.rawCmd = exec.Command(this.name, this.args...)
|
||||
}
|
||||
|
||||
if this.env != nil {
|
||||
this.rawCmd.Env = this.env
|
||||
}
|
||||
|
||||
if len(this.dir) > 0 {
|
||||
this.rawCmd.Dir = this.dir
|
||||
}
|
||||
|
||||
if this.captureStdout {
|
||||
this.stdout = &bytes.Buffer{}
|
||||
this.rawCmd.Stdout = this.stdout
|
||||
}
|
||||
if this.captureStderr {
|
||||
this.stderr = &bytes.Buffer{}
|
||||
this.rawCmd.Stderr = this.stderr
|
||||
}
|
||||
|
||||
return this.rawCmd
|
||||
}
|
||||
57
EdgeHttpDNS/internal/utils/exec/look_linux.go
Normal file
57
EdgeHttpDNS/internal/utils/exec/look_linux.go
Normal file
@@ -0,0 +1,57 @@
|
||||
//go:build linux
|
||||
|
||||
package executils
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// LookPath customize our LookPath() function, to work in broken $PATH environment variable
|
||||
func LookPath(file string) (string, error) {
|
||||
result, err := exec.LookPath(file)
|
||||
if err == nil && len(result) > 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// add common dirs contains executable files these may be excluded in $PATH environment variable
|
||||
var binPaths = []string{
|
||||
"/usr/sbin",
|
||||
"/usr/bin",
|
||||
"/usr/local/sbin",
|
||||
"/usr/local/bin",
|
||||
}
|
||||
|
||||
for _, binPath := range binPaths {
|
||||
var fullPath = binPath + string(os.PathSeparator) + file
|
||||
|
||||
stat, err := os.Stat(fullPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if stat.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
|
||||
var mode = stat.Mode()
|
||||
if mode.IsDir() {
|
||||
return "", syscall.EISDIR
|
||||
}
|
||||
err = syscall.Faccessat(unix.AT_FDCWD, fullPath, unix.X_OK, unix.AT_EACCESS)
|
||||
if err == nil || (err != syscall.ENOSYS && err != syscall.EPERM) {
|
||||
return fullPath, err
|
||||
}
|
||||
if mode&0111 != 0 {
|
||||
return fullPath, nil
|
||||
}
|
||||
return "", fs.ErrPermission
|
||||
}
|
||||
|
||||
return "", &exec.Error{
|
||||
Name: file,
|
||||
Err: exec.ErrNotFound,
|
||||
}
|
||||
}
|
||||
9
EdgeHttpDNS/internal/utils/exec/look_others.go
Normal file
9
EdgeHttpDNS/internal/utils/exec/look_others.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !linux
|
||||
|
||||
package executils
|
||||
|
||||
import "os/exec"
|
||||
|
||||
func LookPath(file string) (string, error) {
|
||||
return exec.LookPath(file)
|
||||
}
|
||||
@@ -6,25 +6,107 @@ package utils
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
|
||||
teaconst "github.com/TeaOSLab/EdgeHttpDNS/internal/const"
|
||||
executils "github.com/TeaOSLab/EdgeHttpDNS/internal/utils/exec"
|
||||
"github.com/iwind/TeaGo/Tea"
|
||||
)
|
||||
|
||||
var systemdServiceFile = "/etc/systemd/system/" + teaconst.SystemdServiceName + ".service"
|
||||
var initServiceFile = "/etc/init.d/" + teaconst.SystemdServiceName
|
||||
|
||||
// Install 安装系统服务
|
||||
func (m *ServiceManager) Install(exePath string, args []string) error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can install the service")
|
||||
}
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
// systemd 不可用,降级到 init.d
|
||||
return m.installInitService(exePath, args)
|
||||
}
|
||||
|
||||
desc := `[Unit]
|
||||
Description=GoEdge HTTPDNS Node Service
|
||||
return m.installSystemdService(systemd, exePath, args)
|
||||
}
|
||||
|
||||
// Start 启动服务
|
||||
func (m *ServiceManager) Start() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can start the service")
|
||||
}
|
||||
|
||||
// 优先检查 systemd
|
||||
if fileExists(systemdServiceFile) {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return executils.NewTimeoutCmd(10*time.Second, systemd, "start", teaconst.SystemdServiceName+".service").Start()
|
||||
}
|
||||
|
||||
// 降级到 init.d
|
||||
if fileExists(initServiceFile) {
|
||||
return executils.NewTimeoutCmd(10*time.Second, "service", teaconst.ProcessName, "start").Start()
|
||||
}
|
||||
|
||||
return errors.New("no service file found, please install the service first")
|
||||
}
|
||||
|
||||
// Uninstall 删除服务
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
|
||||
// systemd
|
||||
if fileExists(systemdServiceFile) {
|
||||
systemd, err := executils.LookPath("systemctl")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "stop", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "disable", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "daemon-reload").Run()
|
||||
}
|
||||
return os.Remove(systemdServiceFile)
|
||||
}
|
||||
|
||||
// init.d
|
||||
if fileExists(initServiceFile) {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, "service", teaconst.ProcessName, "stop").Run()
|
||||
chkCmd, err := executils.LookPath("chkconfig")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, chkCmd, "--del", teaconst.ProcessName).Run()
|
||||
}
|
||||
return os.Remove(initServiceFile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// installSystemdService 安装 systemd 服务
|
||||
func (m *ServiceManager) installSystemdService(systemd, exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
longName := "GoEdge HTTPDNS"
|
||||
|
||||
// 用 bash 包装启动命令,兼容路径含空格的场景
|
||||
var startCmd = exePath + " daemon"
|
||||
bashPath, _ := executils.LookPath("bash")
|
||||
if len(bashPath) > 0 {
|
||||
startCmd = bashPath + " -c \"" + startCmd + "\""
|
||||
}
|
||||
|
||||
desc := `### BEGIN INIT INFO
|
||||
# Provides: ` + shortName + `
|
||||
# Required-Start: $all
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop:
|
||||
# Short-Description: ` + longName + ` Service
|
||||
### END INIT INFO
|
||||
|
||||
[Unit]
|
||||
Description=` + longName + ` Node Service
|
||||
Before=shutdown.target
|
||||
After=network-online.target
|
||||
|
||||
@@ -32,34 +114,102 @@ After=network-online.target
|
||||
Type=simple
|
||||
Restart=always
|
||||
RestartSec=1s
|
||||
ExecStart=` + exePath + ` daemon
|
||||
ExecStart=` + startCmd + `
|
||||
ExecStop=` + exePath + ` stop
|
||||
ExecReload=` + exePath + ` restart
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target`
|
||||
|
||||
err = os.WriteFile(systemdServiceFile, []byte(desc), 0777)
|
||||
// 权限 0644,systemd 单元文件不需要执行权限
|
||||
err := os.WriteFile(systemdServiceFile, []byte(desc), 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "stop", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return exec.Command(systemd, "enable", teaconst.SystemdServiceName+".service").Run()
|
||||
// 停止已有服务
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "stop", shortName+".service").Run()
|
||||
|
||||
// 重新加载
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, systemd, "daemon-reload").Run()
|
||||
|
||||
// 启用开机自启
|
||||
return executils.NewTimeoutCmd(10*time.Second, systemd, "enable", shortName+".service").Run()
|
||||
}
|
||||
|
||||
func (m *ServiceManager) Uninstall() error {
|
||||
if os.Getgid() != 0 {
|
||||
return errors.New("only root users can uninstall the service")
|
||||
}
|
||||
// installInitService 安装 init.d 服务(降级方案,适用于无 systemd 的旧系统)
|
||||
func (m *ServiceManager) installInitService(exePath string, args []string) error {
|
||||
shortName := teaconst.SystemdServiceName
|
||||
longName := "GoEdge HTTPDNS"
|
||||
|
||||
systemd, err := exec.LookPath("systemctl")
|
||||
// 生成 init.d 脚本
|
||||
script := `#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: ` + shortName + `
|
||||
# Required-Start: $all
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: ` + longName + ` Service
|
||||
### END INIT INFO
|
||||
|
||||
INSTALL_DIR=` + Tea.Root + `
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` daemon &
|
||||
echo "` + longName + ` started"
|
||||
;;
|
||||
stop)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` stop
|
||||
echo "` + longName + ` stopped"
|
||||
;;
|
||||
restart)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` stop
|
||||
sleep 1
|
||||
` + exePath + ` daemon &
|
||||
echo "` + longName + ` restarted"
|
||||
;;
|
||||
status)
|
||||
cd "${INSTALL_DIR}"
|
||||
` + exePath + ` status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
`
|
||||
|
||||
// init.d 脚本需要执行权限
|
||||
err := os.WriteFile(initServiceFile, []byte(script), 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = exec.Command(systemd, "disable", teaconst.SystemdServiceName+".service").Run()
|
||||
_ = exec.Command(systemd, "daemon-reload").Run()
|
||||
return os.Remove(systemdServiceFile)
|
||||
// 尝试用 chkconfig 注册(CentOS/RHEL)
|
||||
chkCmd, err := executils.LookPath("chkconfig")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, chkCmd, "--add", teaconst.ProcessName).Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试用 update-rc.d 注册(Debian/Ubuntu)
|
||||
updateRcCmd, err := executils.LookPath("update-rc.d")
|
||||
if err == nil {
|
||||
_ = executils.NewTimeoutCmd(10*time.Second, updateRcCmd, teaconst.ProcessName, "defaults").Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileExists 检查文件是否存在
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user