mirror of
https://github.com/bolucat/Archive.git
synced 2025-10-05 08:08:03 +08:00
253 lines
7.1 KiB
Go
253 lines
7.1 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/debug"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/MerlinKodo/clash-rev/config"
|
|
"github.com/MerlinKodo/clash-rev/constant"
|
|
C "github.com/MerlinKodo/clash-rev/constant"
|
|
"github.com/MerlinKodo/clash-rev/hub"
|
|
"github.com/MerlinKodo/clash-rev/hub/executor"
|
|
"github.com/MerlinKodo/clash-rev/log"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
updateGeoMux sync.Mutex
|
|
updatingGeo = false
|
|
)
|
|
|
|
func newAppConfig() *AppConfig {
|
|
return &AppConfig{
|
|
homeDir: os.Getenv("CLASH_HOME_DIR"),
|
|
configFile: os.Getenv("CLASH_CONFIG_FILE"),
|
|
configUrl: os.Getenv("CLASH_CONFIG_URL"),
|
|
configUrlHeader: os.Getenv("CLASH_CONFIG_URL_HEADER"),
|
|
externalUI: os.Getenv("CLASH_OVERRIDE_EXTERNAL_UI_DIR"),
|
|
externalController: os.Getenv("CLASH_OVERRIDE_EXTERNAL_CONTROLLER"),
|
|
secret: os.Getenv("CLASH_OVERRIDE_SECRET"),
|
|
}
|
|
}
|
|
|
|
func NewApp() *App {
|
|
app := &App{
|
|
Config: newAppConfig(),
|
|
}
|
|
app.setupRootCmd()
|
|
return app
|
|
}
|
|
|
|
func (a *App) Run() error {
|
|
return a.RootCmd.Execute()
|
|
}
|
|
|
|
func (a *App) setupRootCmd() {
|
|
a.RootCmd = &cobra.Command{
|
|
Use: "clash",
|
|
Short: "A rule-based tunnel in Go.",
|
|
Long: `Clash Rev is a rule-based tunnel in Go. Check out the project home page for more information: https://merlinkodo.github.io/Clash-Rev-Doc/`,
|
|
Run: a.execute,
|
|
}
|
|
a.RootCmd.PersistentFlags().StringVarP(&a.Config.homeDir, "dir", "d", a.Config.homeDir, "Specify configuration directory, env: CLASH_HOME_DIR")
|
|
a.RootCmd.PersistentFlags().StringVarP(&a.Config.configFile, "config", "f", a.Config.configFile, "Specify configuration file, env: CLASH_CONFIG_FILE")
|
|
a.RootCmd.PersistentFlags().StringVar(&a.Config.configUrl, "cfg-url", a.Config.configUrl, "Specify configuration file url, env: CLASH_CONFIG_URL")
|
|
a.RootCmd.PersistentFlags().StringVar(&a.Config.configUrlHeader, "cfg-header", a.Config.configUrlHeader, "Specify configuration file url header, env: CLASH_CONFIG_URL_HEADER")
|
|
a.RootCmd.PersistentFlags().StringVar(&a.Config.externalUI, "ext-ui", a.Config.externalUI, "Override external ui directory, env: CLASH_OVERRIDE_EXTERNAL_UI_DIR")
|
|
a.RootCmd.PersistentFlags().StringVar(&a.Config.externalController, "ext-ctl", a.Config.externalController, "Override external controller address, env: CLASH_OVERRIDE_EXTERNAL_CONTROLLER")
|
|
a.RootCmd.PersistentFlags().StringVar(&a.Config.secret, "secret", a.Config.secret, "override secret, env: CLASH_OVERRIDE_SECRET")
|
|
a.RootCmd.PersistentFlags().BoolVarP(&a.Config.geodataMode, "geodata", "m", false, "Set geodata mode")
|
|
a.RootCmd.PersistentFlags().BoolVarP(&a.Config.version, "version", "v", false, "Print current version of clash")
|
|
a.RootCmd.PersistentFlags().BoolVarP(&a.Config.testConfig, "test", "t", false, "Test configuration and exit")
|
|
}
|
|
|
|
func (a *App) execute(cmd *cobra.Command, args []string) {
|
|
setupMaxProcs()
|
|
|
|
if a.Config.version {
|
|
a.printVersion()
|
|
return
|
|
}
|
|
if a.Config.homeDir != "" {
|
|
a.Config.homeDir = resolvePath(a.Config.homeDir)
|
|
C.SetHomeDir(a.Config.homeDir)
|
|
}
|
|
|
|
a.Config.configFile = a.resolveConfigFile()
|
|
C.SetConfig(a.Config.configFile)
|
|
|
|
if a.Config.geodataMode {
|
|
C.GeodataMode = true
|
|
}
|
|
|
|
if err := config.Init(C.Path.HomeDir()); err != nil {
|
|
log.Fatalln("Initial configuration directory error: %s", err.Error())
|
|
}
|
|
|
|
if a.Config.testConfig {
|
|
a.testConfiguration()
|
|
return
|
|
}
|
|
|
|
options := a.parseOptions()
|
|
if err := hub.Parse(options...); err != nil {
|
|
log.Fatalln("Parse config error: %s", err.Error())
|
|
}
|
|
|
|
if C.GeoAutoUpdate {
|
|
ticker := time.NewTicker(time.Duration(C.GeoUpdateInterval) * time.Hour)
|
|
|
|
log.Infoln("Update GEO database every %d hours", C.GeoUpdateInterval)
|
|
go func() {
|
|
for range ticker.C {
|
|
updateGeoDatabases()
|
|
}
|
|
}()
|
|
}
|
|
|
|
defer executor.Shutdown()
|
|
|
|
a.handleSignals()
|
|
fmt.Println("Clash Rev is running now, press Ctrl+C to exit.")
|
|
select {}
|
|
}
|
|
|
|
func (a *App) printVersion() {
|
|
versionString := "Clash Rev Version: " + C.Version + "\n\n"
|
|
versionString += "OS: " + runtime.GOOS + "\n" + "Architecture: " + runtime.GOARCH + "\n" + "Go Version: " + runtime.Version() + "\n" + "Build Time: " + C.BuildTime + "\n"
|
|
|
|
var tags string
|
|
var revision string
|
|
|
|
debugInfo, loaded := debug.ReadBuildInfo()
|
|
if loaded {
|
|
for _, setting := range debugInfo.Settings {
|
|
switch setting.Key {
|
|
case "-tags":
|
|
tags = setting.Value
|
|
case "vcs.revision":
|
|
revision = setting.Value
|
|
}
|
|
}
|
|
}
|
|
if tags != "" {
|
|
versionString += "Tags: " + tags + "\n"
|
|
}
|
|
if revision != "" {
|
|
versionString += "Revision: " + revision + "\n"
|
|
}
|
|
|
|
if C.CGO_ENABLED {
|
|
versionString += "CGO Enabled: Yes\n"
|
|
} else {
|
|
versionString += "CGO Enabled: No\n"
|
|
}
|
|
|
|
fmt.Println(versionString)
|
|
}
|
|
|
|
func (a *App) resolveConfigFile() string {
|
|
if a.Config.configFile != "" {
|
|
return resolvePath(a.Config.configFile)
|
|
} else if a.Config.configUrl != "" {
|
|
log.Infoln("Downloading configuration file from %s", a.Config.configUrl)
|
|
header := parseHeader(a.Config.configUrlHeader)
|
|
configPath := filepath.Join(a.Config.homeDir, "config.yaml")
|
|
if err := downloadFile(a.Config.configUrl, configPath, header); err != nil {
|
|
log.Fatalln("Download configuration file error: %s", err.Error())
|
|
}
|
|
return configPath
|
|
} else {
|
|
return filepath.Join(C.Path.HomeDir(), C.Path.Config())
|
|
}
|
|
}
|
|
|
|
func (a *App) testConfiguration() {
|
|
if _, err := executor.Parse(); err != nil {
|
|
log.Errorln(err.Error())
|
|
fmt.Printf("configuration file %s test failed\n", C.Path.Config())
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf("configuration file %s test is successful\n", C.Path.Config())
|
|
}
|
|
|
|
func (a *App) parseOptions() []hub.Option {
|
|
var options []hub.Option
|
|
if a.Config.externalUI != "" {
|
|
options = append(options, hub.WithExternalUI(a.Config.externalUI))
|
|
}
|
|
if a.Config.externalController != "" {
|
|
options = append(options, hub.WithExternalController(a.Config.externalController))
|
|
}
|
|
if a.Config.secret != "" {
|
|
options = append(options, hub.WithSecret(a.Config.secret))
|
|
}
|
|
return options
|
|
}
|
|
|
|
func (a *App) handleSignals() {
|
|
sigs := make(chan os.Signal, 1)
|
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
|
|
|
go func() {
|
|
for sig := range sigs {
|
|
switch sig {
|
|
case syscall.SIGINT, syscall.SIGTERM:
|
|
log.Infoln("Received SIGINT or SIGTERM. Exiting gracefully...")
|
|
os.Exit(0)
|
|
case syscall.SIGHUP:
|
|
log.Infoln("Received SIGHUP. Reloading...")
|
|
if cfg, err := executor.ParseWithPath(C.Path.Config()); err == nil {
|
|
executor.ApplyConfig(cfg, true)
|
|
} else {
|
|
log.Errorln("Parse config error: %s", err.Error())
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func updateGeoDatabases() {
|
|
log.Infoln("Start updating GEO database")
|
|
updateGeoMux.Lock()
|
|
|
|
if updatingGeo {
|
|
updateGeoMux.Unlock()
|
|
log.Infoln("GEO database is updating, skip")
|
|
return
|
|
}
|
|
|
|
updatingGeo = true
|
|
updateGeoMux.Unlock()
|
|
|
|
go func() {
|
|
defer func() {
|
|
updatingGeo = false
|
|
}()
|
|
|
|
log.Warnln("Updating GEO database")
|
|
|
|
if err := config.UpdateGeoDatabases(); err != nil {
|
|
log.Errorln("update GEO database error: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
cfg, err := executor.ParseWithPath(constant.Path.Config())
|
|
if err != nil {
|
|
log.Errorln("update GEO database failed: %s", err.Error())
|
|
return
|
|
}
|
|
|
|
log.Warnln("Update GEO database success, apply new config")
|
|
executor.ApplyConfig(cfg, false)
|
|
}()
|
|
}
|