package main import ( "context" "io" "os" "os/signal" "path/filepath" runtimeDebug "runtime/debug" "sort" "strings" "syscall" "time" "github.com/sagernet/sing-box" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json/badjson" "github.com/spf13/cobra" ) var commandRun = &cobra.Command{ Use: "run", Short: "Run service", Run: func(cmd *cobra.Command, args []string) { err := run() if err != nil { log.Fatal(err) } }, } func init() { mainCommand.AddCommand(commandRun) } type OptionsEntry struct { content []byte path string options option.Options } func readConfigAt(path string) (*OptionsEntry, error) { var ( configContent []byte err error ) if path == "stdin" { configContent, err = io.ReadAll(os.Stdin) } else { configContent, err = os.ReadFile(path) } if err != nil { return nil, E.Cause(err, "read config at ", path) } var options option.Options err = options.UnmarshalJSON(configContent) if err != nil { return nil, E.Cause(err, "decode config at ", path) } return &OptionsEntry{ content: configContent, path: path, options: options, }, nil } func readConfig() ([]*OptionsEntry, error) { var optionsList []*OptionsEntry for _, path := range configPaths { optionsEntry, err := readConfigAt(path) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } for _, directory := range configDirectories { entries, err := os.ReadDir(directory) if err != nil { return nil, E.Cause(err, "read config directory at ", directory) } for _, entry := range entries { if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { continue } optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name())) if err != nil { return nil, err } optionsList = append(optionsList, optionsEntry) } } sort.Slice(optionsList, func(i, j int) bool { return optionsList[i].path < optionsList[j].path }) return optionsList, nil } func readConfigAndMerge() (option.Options, error) { optionsList, err := readConfig() if err != nil { return option.Options{}, err } if len(optionsList) == 1 { return optionsList[0].options, nil } var mergedOptions option.Options for _, options := range optionsList { mergedOptions, err = badjson.Merge(options.options, mergedOptions) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } } return mergedOptions, nil } func create() (*box.Box, context.CancelFunc, error) { options, err := readConfigAndMerge() if err != nil { return nil, nil, err } if disableColor { if options.Log == nil { options.Log = &option.LogOptions{} } options.Log.DisableColor = true } ctx, cancel := context.WithCancel(context.Background()) instance, err := box.New(box.Options{ Context: ctx, Options: options, }) if err != nil { cancel() return nil, nil, E.Cause(err, "create service") } osSignals := make(chan os.Signal, 1) signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) defer func() { signal.Stop(osSignals) close(osSignals) }() startCtx, finishStart := context.WithCancel(context.Background()) go func() { _, loaded := <-osSignals if loaded { cancel() closeMonitor(startCtx) } }() err = instance.Start() finishStart() if err != nil { cancel() return nil, nil, E.Cause(err, "start service") } return instance, cancel, nil } func run() error { osSignals := make(chan os.Signal, 1) signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) defer signal.Stop(osSignals) for { instance, cancel, err := create() if err != nil { return err } runtimeDebug.FreeOSMemory() for { osSignal := <-osSignals if osSignal == syscall.SIGHUP { err = check() if err != nil { log.Error(E.Cause(err, "reload service")) continue } } cancel() closeCtx, closed := context.WithCancel(context.Background()) go closeMonitor(closeCtx) instance.Close() closed() if osSignal != syscall.SIGHUP { return nil } break } } } func closeMonitor(ctx context.Context) { time.Sleep(C.DefaultStopFatalTimeout) select { case <-ctx.Done(): return default: } log.Fatal("sing-box did not close!") }