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 }