Files
Prism/cmd/prism/main.go
T
renjue 9dfa7aa7c5
CI / docker (push) Successful in 4m28s
feat: Prism HTTP/SOCKS5 代理网关及管理台
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 15:37:02 +08:00

184 lines
4.7 KiB
Go

package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"github.com/robfig/cron/v3"
"github.com/prism/proxy/internal/api"
"github.com/prism/proxy/internal/cache"
"github.com/prism/proxy/internal/configbuilder"
"github.com/prism/proxy/internal/core"
"github.com/prism/proxy/internal/geodata"
"github.com/prism/proxy/internal/health"
applog "github.com/prism/proxy/internal/log"
"github.com/prism/proxy/internal/metrics"
"github.com/prism/proxy/internal/mihomoapi"
"github.com/prism/proxy/internal/service"
"github.com/prism/proxy/internal/store"
"github.com/prism/proxy/internal/subscription"
"github.com/prism/proxy/internal/trafficlog"
)
func main() {
dataDir := flag.String("data", "data", "data directory")
flag.Parse()
log.Printf("Prism starting, data dir: %s", *dataDir)
if err := os.MkdirAll(*dataDir, 0o755); err != nil {
log.Fatal(err)
}
st, err := store.Open(*dataDir)
if err != nil {
log.Fatal(err)
}
defer st.Close()
log.Printf("database ready")
if n, err := st.BackfillNodeCountries(context.Background()); err != nil {
log.Printf("country backfill: %v", err)
} else if n > 0 {
log.Printf("country tags backfilled for %d nodes", n)
}
core.InitDataDir(*dataDir)
c := cache.New()
engine := core.NewEngine()
builder := configbuilder.New(st, *dataDir)
logCollector := applog.NewCollector(st, 5000, true)
subSvc := subscription.NewService(st)
app := &service.App{
Store: st,
Cache: c,
Engine: engine,
Builder: builder,
Subscription: subSvc,
Logs: logCollector,
DataDir: *dataDir,
}
ctx := context.Background()
apiBase, secret, _ := app.MihomoAPIBase(ctx)
hc := health.NewChecker(st, c, apiBase, secret)
mc := metrics.NewCollector(st, c, apiBase, secret)
trafficCollector := trafficlog.NewCollector(st, 5000)
trafficCollector.Start(ctx, mihomoapi.New(apiBase, secret))
settings, _ := st.GetSettings(ctx)
applyGeoSettingsOnStart(*dataDir, settings)
if settings["geo_auto_download"] != "false" {
go func() {
log.Printf("checking geo data in background...")
cfg := geodata.ConfigFromSettings(settings)
if updated, err := geodata.Ensure(*dataDir, cfg, false); err != nil {
log.Printf("geo data: %v", err)
logCollector.Add("warn", "prism", "geo data: "+err.Error())
} else if len(updated) > 0 {
log.Printf("geo data downloaded: %v", updated)
}
}()
}
logCollector.Add("info", "prism", "Prism started")
log.Printf("loading proxy engine...")
if err := app.ReloadEngine(ctx); err != nil {
log.Printf("initial engine reload: %v (will retry after config)", err)
} else {
log.Printf("proxy engine ready")
}
go func() {
warmCtx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
_, _ = mc.Refresh(warmCtx)
app.RefreshEngineStatusAsync()
}()
srv := api.NewServer(app, hc, mc, subSvc, logCollector, trafficCollector)
router := srv.Router()
apiPort := settings["api_port"]
if apiPort == "" {
apiPort = "8080"
}
httpServer := &http.Server{
Addr: ":" + apiPort,
Handler: router,
}
cr := cron.New()
_, _ = cr.AddFunc("@every 1m", func() {
_, _ = mc.Refresh(context.Background())
})
_, _ = cr.AddFunc("@every 5m", func() {
settings, _ := st.GetSettings(context.Background())
sec, _ := strconv.Atoi(settings["health_interval_sec"])
if sec <= 0 {
sec = 300
}
// health checker runs on its own schedule approximation
_ = hc.TestAll(context.Background(), nil)
})
_, _ = cr.AddFunc("@every 10m", func() {
syncSubscriptions(app, subSvc)
})
cr.Start()
go func() {
log.Printf("Prism API listening on :%s", apiPort)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
cr.Stop()
engine.Shutdown()
}
func syncSubscriptions(app *service.App, subSvc *subscription.Service) {
ctx := context.Background()
settings, err := app.Store.GetSettings(ctx)
if err != nil || settings["sub_sync_enabled"] != "true" {
return
}
subs, err := app.Store.ListSubscriptions(ctx)
if err != nil {
return
}
now := time.Now()
for _, sub := range subs {
if !sub.Enabled {
continue
}
if sub.LastFetchAt != nil && now.Sub(*sub.LastFetchAt) < time.Duration(sub.IntervalSec)*time.Second {
continue
}
if err := subSvc.FetchAndStore(ctx, sub.ID); err != nil {
app.Logs.Add("error", "subscription", fmt.Sprintf("sync %s failed: %v", sub.Name, err))
continue
}
}
_ = app.ReloadEngine(ctx)
}