github.com/sagernet/sing-box@v1.2.7/cmd/sing-box/cmd_run.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"io"
     6  	"os"
     7  	"os/signal"
     8  	"path/filepath"
     9  	runtimeDebug "runtime/debug"
    10  	"sort"
    11  	"strings"
    12  	"syscall"
    13  	"time"
    14  
    15  	"github.com/sagernet/sing-box"
    16  	"github.com/sagernet/sing-box/common/badjsonmerge"
    17  	"github.com/sagernet/sing-box/log"
    18  	"github.com/sagernet/sing-box/option"
    19  	E "github.com/sagernet/sing/common/exceptions"
    20  
    21  	"github.com/spf13/cobra"
    22  )
    23  
    24  var commandRun = &cobra.Command{
    25  	Use:   "run",
    26  	Short: "Run service",
    27  	Run: func(cmd *cobra.Command, args []string) {
    28  		err := run()
    29  		if err != nil {
    30  			log.Fatal(err)
    31  		}
    32  	},
    33  }
    34  
    35  func init() {
    36  	mainCommand.AddCommand(commandRun)
    37  }
    38  
    39  type OptionsEntry struct {
    40  	content []byte
    41  	path    string
    42  	options option.Options
    43  }
    44  
    45  func readConfigAt(path string) (*OptionsEntry, error) {
    46  	var (
    47  		configContent []byte
    48  		err           error
    49  	)
    50  	if path == "stdin" {
    51  		configContent, err = io.ReadAll(os.Stdin)
    52  	} else {
    53  		configContent, err = os.ReadFile(path)
    54  	}
    55  	if err != nil {
    56  		return nil, E.Cause(err, "read config at ", path)
    57  	}
    58  	var options option.Options
    59  	err = options.UnmarshalJSON(configContent)
    60  	if err != nil {
    61  		return nil, E.Cause(err, "decode config at ", path)
    62  	}
    63  	return &OptionsEntry{
    64  		content: configContent,
    65  		path:    path,
    66  		options: options,
    67  	}, nil
    68  }
    69  
    70  func readConfig() ([]*OptionsEntry, error) {
    71  	var optionsList []*OptionsEntry
    72  	for _, path := range configPaths {
    73  		optionsEntry, err := readConfigAt(path)
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  		optionsList = append(optionsList, optionsEntry)
    78  	}
    79  	for _, directory := range configDirectories {
    80  		entries, err := os.ReadDir(directory)
    81  		if err != nil {
    82  			return nil, E.Cause(err, "read config directory at ", directory)
    83  		}
    84  		for _, entry := range entries {
    85  			if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() {
    86  				continue
    87  			}
    88  			optionsEntry, err := readConfigAt(filepath.Join(directory, entry.Name()))
    89  			if err != nil {
    90  				return nil, err
    91  			}
    92  			optionsList = append(optionsList, optionsEntry)
    93  		}
    94  	}
    95  	sort.Slice(optionsList, func(i, j int) bool {
    96  		return optionsList[i].path < optionsList[j].path
    97  	})
    98  	return optionsList, nil
    99  }
   100  
   101  func readConfigAndMerge() (option.Options, error) {
   102  	optionsList, err := readConfig()
   103  	if err != nil {
   104  		return option.Options{}, err
   105  	}
   106  	if len(optionsList) == 1 {
   107  		return optionsList[0].options, nil
   108  	}
   109  	var mergedOptions option.Options
   110  	for _, options := range optionsList {
   111  		mergedOptions, err = badjsonmerge.MergeOptions(options.options, mergedOptions)
   112  		if err != nil {
   113  			return option.Options{}, E.Cause(err, "merge config at ", options.path)
   114  		}
   115  	}
   116  	return mergedOptions, nil
   117  }
   118  
   119  func create() (*box.Box, context.CancelFunc, error) {
   120  	options, err := readConfigAndMerge()
   121  	if err != nil {
   122  		return nil, nil, err
   123  	}
   124  	if disableColor {
   125  		if options.Log == nil {
   126  			options.Log = &option.LogOptions{}
   127  		}
   128  		options.Log.DisableColor = true
   129  	}
   130  	ctx, cancel := context.WithCancel(context.Background())
   131  	instance, err := box.New(box.Options{
   132  		Context: ctx,
   133  		Options: options,
   134  	})
   135  	if err != nil {
   136  		cancel()
   137  		return nil, nil, E.Cause(err, "create service")
   138  	}
   139  
   140  	osSignals := make(chan os.Signal, 1)
   141  	signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
   142  	defer func() {
   143  		signal.Stop(osSignals)
   144  		close(osSignals)
   145  	}()
   146  
   147  	go func() {
   148  		_, loaded := <-osSignals
   149  		if loaded {
   150  			cancel()
   151  		}
   152  	}()
   153  	err = instance.Start()
   154  	if err != nil {
   155  		cancel()
   156  		return nil, nil, E.Cause(err, "start service")
   157  	}
   158  	return instance, cancel, nil
   159  }
   160  
   161  func run() error {
   162  	osSignals := make(chan os.Signal, 1)
   163  	signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
   164  	defer signal.Stop(osSignals)
   165  	for {
   166  		instance, cancel, err := create()
   167  		if err != nil {
   168  			return err
   169  		}
   170  		runtimeDebug.FreeOSMemory()
   171  		for {
   172  			osSignal := <-osSignals
   173  			if osSignal == syscall.SIGHUP {
   174  				err = check()
   175  				if err != nil {
   176  					log.Error(E.Cause(err, "reload service"))
   177  					continue
   178  				}
   179  			}
   180  			cancel()
   181  			closeCtx, closed := context.WithCancel(context.Background())
   182  			go closeMonitor(closeCtx)
   183  			instance.Close()
   184  			closed()
   185  			if osSignal != syscall.SIGHUP {
   186  				return nil
   187  			}
   188  			break
   189  		}
   190  	}
   191  }
   192  
   193  func closeMonitor(ctx context.Context) {
   194  	time.Sleep(3 * time.Second)
   195  	select {
   196  	case <-ctx.Done():
   197  		return
   198  	default:
   199  	}
   200  	log.Fatal("sing-box did not close!")
   201  }