decred.org/dcrdex@v1.0.5/client/cmd/bisonw/main.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package main
     5  
     6  import (
     7  	"bufio"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"os/signal"
    13  	"runtime"
    14  	"runtime/debug"
    15  	"runtime/pprof"
    16  	"strings"
    17  	"sync"
    18  	"time"
    19  
    20  	"decred.org/dcrdex/client/app"
    21  	"decred.org/dcrdex/client/asset"
    22  	_ "decred.org/dcrdex/client/asset/importall"
    23  	"decred.org/dcrdex/client/core"
    24  	"decred.org/dcrdex/client/mm"
    25  	"decred.org/dcrdex/client/rpcserver"
    26  	"decred.org/dcrdex/client/webserver"
    27  	"decred.org/dcrdex/dex"
    28  )
    29  
    30  // appName defines the application name.
    31  const appName = "bisonw"
    32  
    33  var (
    34  	appCtx, cancel = context.WithCancel(context.Background())
    35  	webserverReady = make(chan string, 1)
    36  	log            dex.Logger
    37  )
    38  
    39  func runCore(cfg *app.Config) error {
    40  	defer cancel() // for the earliest returns
    41  
    42  	asset.SetNetwork(cfg.Net)
    43  
    44  	// If explicitly running without web server then you must run the rpc
    45  	// server.
    46  	if cfg.NoWeb && !cfg.RPCOn {
    47  		return fmt.Errorf("cannot run without web server unless --rpc is specified")
    48  	}
    49  
    50  	if cfg.CPUProfile != "" {
    51  		var f *os.File
    52  		f, err := os.Create(cfg.CPUProfile)
    53  		if err != nil {
    54  			return fmt.Errorf("error starting CPU profiler: %w", err)
    55  		}
    56  		err = pprof.StartCPUProfile(f)
    57  		if err != nil {
    58  			return fmt.Errorf("error starting CPU profiler: %w", err)
    59  		}
    60  		defer pprof.StopCPUProfile()
    61  	}
    62  
    63  	// Initialize logging.
    64  	utc := !cfg.LocalLogs
    65  	logMaker, closeLogger := app.InitLogging(cfg.LogPath, cfg.DebugLevel, true, utc)
    66  	defer closeLogger()
    67  	log = logMaker.Logger("BW")
    68  	log.Infof("%s version %v (Go version %s)", appName, app.Version, runtime.Version())
    69  	if utc {
    70  		log.Infof("Logging with UTC time stamps. Current local time is %v",
    71  			time.Now().Local().Format("15:04:05 MST"))
    72  	}
    73  	log.Infof("bisonw starting for network: %s", cfg.Net)
    74  	log.Infof("Swap locktimes config: maker %s, taker %s",
    75  		dex.LockTimeMaker(cfg.Net), dex.LockTimeTaker(cfg.Net))
    76  
    77  	defer func() {
    78  		if pv := recover(); pv != nil {
    79  			log.Criticalf("Uh-oh! \n\nPanic:\n\n%v\n\nStack:\n\n%v\n\n",
    80  				pv, string(debug.Stack()))
    81  		}
    82  	}()
    83  
    84  	// Prepare the Core.
    85  	clientCore, err := core.New(cfg.Core(logMaker.Logger("CORE")))
    86  	if err != nil {
    87  		return fmt.Errorf("error creating client core: %w", err)
    88  	}
    89  
    90  	marketMaker, err := mm.NewMarketMaker(clientCore, cfg.MMConfig.EventLogDBPath, cfg.MMConfig.BotConfigPath, logMaker.Logger("MM"))
    91  	if err != nil {
    92  		return fmt.Errorf("error creating market maker: %w", err)
    93  	}
    94  
    95  	// Catch interrupt signal (e.g. ctrl+c), prompting to shutdown if the user
    96  	// is logged in, and there are active orders or matches.
    97  	killChan := make(chan os.Signal, 1)
    98  	signal.Notify(killChan, os.Interrupt)
    99  	go func() {
   100  		for range killChan {
   101  			if promptShutdown(clientCore) {
   102  				log.Infof("Shutting down...")
   103  				cancel()
   104  				return
   105  			}
   106  		}
   107  	}()
   108  
   109  	var wg sync.WaitGroup
   110  	wg.Add(1)
   111  	go func() {
   112  		defer wg.Done()
   113  		clientCore.Run(appCtx)
   114  		cancel() // in the event that Run returns prematurely prior to context cancellation
   115  	}()
   116  
   117  	<-clientCore.Ready()
   118  
   119  	var mmCM *dex.ConnectionMaster
   120  	defer func() {
   121  		log.Info("Exiting bisonw main.")
   122  		cancel()  // no-op with clean rpc/web server setup
   123  		wg.Wait() // no-op with clean setup and shutdown
   124  		if mmCM != nil {
   125  			mmCM.Wait()
   126  		}
   127  	}()
   128  
   129  	if marketMaker != nil {
   130  		mmCM = dex.NewConnectionMaster(marketMaker)
   131  		if err := mmCM.ConnectOnce(appCtx); err != nil {
   132  			return fmt.Errorf("Error connecting market maker")
   133  		}
   134  	}
   135  
   136  	if cfg.RPCOn {
   137  		rpcSrv, err := rpcserver.New(cfg.RPC(clientCore, marketMaker, logMaker.Logger("RPC")))
   138  		if err != nil {
   139  			return fmt.Errorf("failed to create rpc server: %w", err)
   140  		}
   141  
   142  		wg.Add(1)
   143  		go func() {
   144  			defer wg.Done()
   145  			cm := dex.NewConnectionMaster(rpcSrv)
   146  			err := cm.Connect(appCtx)
   147  			if err != nil {
   148  				log.Errorf("Error starting rpc server: %v", err)
   149  				cancel()
   150  				return
   151  			}
   152  			cm.Wait()
   153  		}()
   154  	}
   155  
   156  	if !cfg.NoWeb {
   157  		webSrv, err := webserver.New(cfg.Web(clientCore, marketMaker, logMaker.Logger("WEB"), utc))
   158  		if err != nil {
   159  			return fmt.Errorf("failed creating web server: %w", err)
   160  		}
   161  
   162  		wg.Add(1)
   163  		go func() {
   164  			defer wg.Done()
   165  			cm := dex.NewConnectionMaster(webSrv)
   166  			err := cm.Connect(appCtx)
   167  			if err != nil {
   168  				log.Errorf("Error starting web server: %v", err)
   169  				cancel()
   170  				return
   171  			}
   172  			webserverReady <- webSrv.Addr()
   173  			cm.Wait()
   174  		}()
   175  	} else {
   176  		close(webserverReady)
   177  	}
   178  
   179  	// Wait for everything to stop.
   180  	wg.Wait()
   181  
   182  	return nil
   183  }
   184  
   185  // promptShutdown checks if there are active orders and asks confirmation to
   186  // shutdown if there are. The return value indicates if it is safe to stop Core
   187  // or if the user has confirmed they want to shutdown with active orders.
   188  func promptShutdown(clientCore *core.Core) bool {
   189  	log.Infof("Attempting to logout...")
   190  	// Do not allow Logout hanging to prevent shutdown.
   191  	res := make(chan bool, 1)
   192  	go func() {
   193  		// Only block logout if err is ActiveOrdersLogoutErr.
   194  		var ok bool
   195  		err := clientCore.Logout()
   196  		if err == nil {
   197  			ok = true
   198  		} else if !errors.Is(err, core.ActiveOrdersLogoutErr) {
   199  			log.Errorf("Unexpected logout error: %v", err)
   200  			ok = true
   201  		} // else not ok => prompt
   202  		res <- ok
   203  	}()
   204  
   205  	select {
   206  	case <-time.After(10 * time.Second):
   207  		log.Errorf("Timeout waiting for Logout. Allowing shutdown, but you likely have active orders!")
   208  		return true // cancel all the contexts, hopefully breaking whatever deadlock
   209  	case ok := <-res:
   210  		if ok {
   211  			return true
   212  		}
   213  	}
   214  
   215  	fmt.Print("You have active orders. Shutting down now may result in failed swaps and account penalization.\n" +
   216  		"Do you want to quit anyway? ('yes' to quit, or enter to abort shutdown): ")
   217  	scanner := bufio.NewScanner(os.Stdin)
   218  	scanner.Scan() // waiting for user input
   219  	if err := scanner.Err(); err != nil {
   220  		fmt.Printf("Input error: %v", err)
   221  		return false
   222  	}
   223  
   224  	switch resp := strings.ToLower(scanner.Text()); resp {
   225  	case "y", "yes":
   226  		return true
   227  	case "n", "no", "":
   228  	default: // anything else aborts, but warn about it
   229  		fmt.Printf("Unrecognized response %q. ", resp)
   230  	}
   231  	fmt.Println("Shutdown aborted.")
   232  	return false
   233  }