github.com/status-im/status-go@v1.1.0/cmd/statusd/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "database/sql" 6 "errors" 7 "flag" 8 "fmt" 9 stdlog "log" 10 "os" 11 "os/signal" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "time" 16 17 "github.com/google/uuid" 18 "github.com/okzk/sdnotify" 19 "golang.org/x/crypto/ssh/terminal" 20 21 "github.com/ethereum/go-ethereum/log" 22 gethmetrics "github.com/ethereum/go-ethereum/metrics" 23 24 "github.com/status-im/status-go/api" 25 "github.com/status-im/status-go/appdatabase" 26 "github.com/status-im/status-go/cmd/statusd/server" 27 "github.com/status-im/status-go/common/dbsetup" 28 gethbridge "github.com/status-im/status-go/eth-node/bridge/geth" 29 "github.com/status-im/status-go/eth-node/crypto" 30 "github.com/status-im/status-go/logutils" 31 "github.com/status-im/status-go/metrics" 32 nodemetrics "github.com/status-im/status-go/metrics/node" 33 "github.com/status-im/status-go/node" 34 "github.com/status-im/status-go/params" 35 "github.com/status-im/status-go/profiling" 36 "github.com/status-im/status-go/protocol" 37 "github.com/status-im/status-go/protocol/pushnotificationserver" 38 "github.com/status-im/status-go/protocol/requests" 39 "github.com/status-im/status-go/walletdatabase" 40 ) 41 42 const ( 43 serverClientName = "Statusd" 44 ) 45 46 var ( 47 configFiles configFlags 48 logLevel = flag.String("log", "", `Log level, one of: "ERROR", "WARN", "INFO", "DEBUG", and "TRACE"`) 49 logWithoutColors = flag.Bool("log-without-color", false, "Disables log colors") 50 ipcEnabled = flag.Bool("ipc", false, "Enable IPC RPC endpoint") 51 ipcFile = flag.String("ipcfile", "", "Set IPC file path") 52 pprofEnabled = flag.Bool("pprof", false, "Enable runtime profiling via pprof") 53 pprofPort = flag.Int("pprof-port", 52525, "Port for runtime profiling via pprof") 54 communityArchiveSupportEnabled = flag.Bool("community-archives", false, "Enable community history archive support") 55 torrentClientPort = flag.Int("torrent-client-port", 9025, "Port for BitTorrent protocol connections") 56 version = flag.Bool("version", false, "Print version and dump configuration") 57 58 dataDir = flag.String("dir", getDefaultDataDir(), "Directory used by node to store data") 59 register = flag.Bool("register", false, "Register and make the node discoverable by other nodes") 60 mailserver = flag.Bool("mailserver", false, "Enable Mail Server with default configuration") 61 networkID = flag.Int( 62 "network-id", 63 params.GoerliNetworkID, 64 fmt.Sprintf( 65 "A network ID: %d (Mainnet), %d (Goerli)", 66 params.MainNetworkID, params.GoerliNetworkID, 67 ), 68 ) 69 fleet = flag.String( 70 "fleet", 71 params.FleetProd, 72 fmt.Sprintf( 73 "Select fleet: %s (default %s)", 74 []string{ 75 params.FleetProd, 76 params.FleetStatusStaging, 77 params.FleetStatusProd, 78 params.FleetWakuSandbox, 79 params.FleetWakuTest, 80 }, 81 params.FleetProd, 82 ), 83 ) 84 listenAddr = flag.String("addr", "", "address to bind listener to") 85 serverAddr = flag.String("server", "", "Address `host:port` for HTTP API server of statusd") 86 87 // don't change the name of this flag, https://github.com/ethereum/go-ethereum/blob/master/metrics/metrics.go#L41 88 metricsEnabled = flag.Bool("metrics", false, "Expose ethereum metrics with debug_metrics jsonrpc call") 89 metricsPort = flag.Int("metrics-port", 9305, "Port for the Prometheus /metrics endpoint") 90 seedPhrase = flag.String("seed-phrase", "", "Seed phrase for account creation") 91 password = flag.String("password", "", "Password for account") 92 ) 93 94 // All general log messages in this package should be routed through this logger. 95 var logger = log.New("package", "status-go/cmd/statusd") 96 97 func init() { 98 flag.Var(&configFiles, "c", "JSON configuration file(s). Multiple configuration files can be specified, and will be merged in occurrence order") 99 } 100 101 // nolint:gocyclo 102 func main() { 103 colors := terminal.IsTerminal(int(os.Stdin.Fd())) 104 if err := logutils.OverrideRootLog(true, "ERROR", logutils.FileOptions{}, colors); err != nil { 105 stdlog.Fatalf("Error initializing logger: %v", err) 106 } 107 108 flag.Usage = printUsage 109 flag.Parse() 110 if flag.NArg() > 0 { 111 printUsage() 112 logger.Error("Extra args in command line: %v", flag.Args()) 113 os.Exit(1) 114 } 115 116 if *seedPhrase != "" && *password == "" { 117 printUsage() 118 logger.Error("password is required when seed phrase is provided") 119 os.Exit(1) 120 } 121 122 opts := []params.Option{params.WithFleet(*fleet)} 123 if *mailserver { 124 opts = append(opts, params.WithMailserver()) 125 } 126 127 config, err := params.NewNodeConfigWithDefaultsAndFiles( 128 *dataDir, 129 uint64(*networkID), 130 opts, 131 configFiles, 132 ) 133 if err != nil { 134 printUsage() 135 logger.Error(err.Error()) 136 os.Exit(1) 137 } 138 139 // Use listenAddr if and only if explicitly provided in the arguments. 140 // The default value is set in params.NewNodeConfigWithDefaultsAndFiles(). 141 if *listenAddr != "" { 142 config.ListenAddr = *listenAddr 143 } 144 145 if *register && *mailserver { 146 config.RegisterTopics = append(config.RegisterTopics, params.MailServerDiscv5Topic) 147 } else if *register { 148 config.RegisterTopics = append(config.RegisterTopics, params.WhisperDiscv5Topic) 149 } 150 151 // enable IPC RPC 152 if *ipcEnabled { 153 config.IPCEnabled = true 154 config.IPCFile = *ipcFile 155 } 156 157 if *communityArchiveSupportEnabled { 158 config.TorrentConfig.Enabled = true 159 config.TorrentConfig.Port = *torrentClientPort 160 } 161 162 // set up logging options 163 setupLogging(config) 164 165 // We want statusd to be distinct from StatusIM client. 166 config.Name = serverClientName 167 168 if *version { 169 printVersion(config) 170 return 171 } 172 173 if serverAddr != nil && *serverAddr != "" { 174 srv := server.NewServer() 175 srv.Setup() 176 err = srv.Listen(*serverAddr) 177 if err != nil { 178 logger.Error("failed to start server", "error", err) 179 return 180 } 181 log.Info("server started", "address", srv.Address()) 182 defer func() { 183 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 184 defer cancel() 185 srv.Stop(ctx) 186 }() 187 } 188 189 backend := api.NewGethStatusBackend() 190 if config.NodeKey == "" { 191 logger.Error("node key needs to be set if running a push notification server") 192 return 193 } 194 195 identity, err := crypto.HexToECDSA(config.NodeKey) 196 if err != nil { 197 logger.Error("node key is invalid", "error", err) 198 return 199 } 200 201 // Generate installationID from public key, so it's always the same 202 installationID, err := uuid.FromBytes(crypto.CompressPubkey(&identity.PublicKey)[:16]) 203 if err != nil { 204 logger.Error("cannot create installation id", "error", err) 205 return 206 } 207 208 if *seedPhrase != "" { 209 // Remove data inside dir to avoid conflicts with existing data or account restoration fails 210 if err := os.RemoveAll(config.DataDir); err != nil { 211 logger.Error("failed to remove data dir", "error", err) 212 return 213 } 214 215 if err := createDirsFromConfig(config); err != nil { 216 logger.Error("failed to create directories", "error", err) 217 return 218 } 219 220 request := requests.RestoreAccount{ 221 Mnemonic: *seedPhrase, 222 FetchBackup: false, 223 CreateAccount: requests.CreateAccount{ 224 DisplayName: "Account1", 225 DeviceName: "StatusIM", 226 Password: *password, 227 CustomizationColor: "0x000000", 228 RootDataDir: config.DataDir, 229 APIConfig: &requests.APIConfig{ 230 ConnectorEnabled: config.ClusterConfig.Enabled, 231 HTTPEnabled: config.HTTPEnabled, 232 HTTPHost: config.HTTPHost, 233 HTTPPort: config.HTTPPort, 234 HTTPVirtualHosts: config.HTTPVirtualHosts, 235 WSEnabled: config.WSEnabled, 236 WSHost: config.WSHost, 237 WSPort: config.WSPort, 238 APIModules: config.APIModules, 239 }, 240 NetworkID: &config.NetworkID, 241 TestOverrideNetworks: config.Networks, 242 }, 243 } 244 245 api.OverrideApiConfigTest() 246 247 _, err := backend.RestoreAccountAndLogin(&request) 248 if err != nil { 249 logger.Error("failed to import account", "error", err) 250 return 251 } 252 253 appDB, walletDB, err := openDatabases(config.DataDir + "/" + installationID.String()) 254 if err != nil { 255 log.Error("failed to open databases") 256 return 257 } 258 259 options := []protocol.Option{ 260 protocol.WithDatabase(appDB), 261 protocol.WithWalletDatabase(walletDB), 262 protocol.WithTorrentConfig(&config.TorrentConfig), 263 protocol.WithWalletConfig(&config.WalletConfig), 264 protocol.WithAccountManager(backend.AccountManager()), 265 } 266 267 messenger, err := protocol.NewMessenger( 268 config.Name, 269 identity, 270 gethbridge.NewNodeBridge(backend.StatusNode().GethNode(), backend.StatusNode().WakuService(), backend.StatusNode().WakuV2Service()), 271 installationID.String(), 272 nil, 273 config.Version, 274 options..., 275 ) 276 277 if err != nil { 278 logger.Error("failed to create messenger", "error", err) 279 return 280 } 281 282 _, err = messenger.Start() 283 if err != nil { 284 logger.Error("failed to start messenger", "error", err) 285 return 286 } 287 288 interruptCh := haltOnInterruptSignal(backend.StatusNode()) 289 go retrieveMessagesLoop(messenger, 300*time.Millisecond, interruptCh) 290 291 } else { 292 appDB, walletDB, err := startNode(config, backend, installationID) 293 if err != nil { 294 logger.Error("failed to start node", "error", err) 295 return 296 } 297 298 err = sdnotify.Ready() 299 if err == sdnotify.ErrSdNotifyNoSocket { 300 logger.Debug("sd_notify socket not available") 301 } else if err != nil { 302 logger.Warn("sd_notify READY call failed", "error", err) 303 } else { 304 // systemd aliveness notifications, affects only Linux 305 go startSystemDWatchdog() 306 } 307 308 // handle interrupt signals 309 interruptCh := haltOnInterruptSignal(backend.StatusNode()) 310 311 // Start collecting metrics. Metrics can be enabled by providing `-metrics` flag 312 // or setting `gethmetrics.Enabled` to true during compilation time: 313 // https://github.com/status-im/go-ethereum/pull/76. 314 if *metricsEnabled || gethmetrics.Enabled { 315 go startCollectingNodeMetrics(interruptCh, backend.StatusNode()) 316 go gethmetrics.CollectProcessMetrics(3 * time.Second) 317 go metrics.NewMetricsServer(*metricsPort, gethmetrics.DefaultRegistry).Listen() 318 } 319 320 // Check if profiling shall be enabled. 321 if *pprofEnabled { 322 profiling.NewProfiler(*pprofPort).Go() 323 } 324 325 if config.PushNotificationServerConfig.Enabled { 326 options := []protocol.Option{ 327 protocol.WithPushNotifications(), 328 protocol.WithPushNotificationServerConfig(&pushnotificationserver.Config{ 329 Enabled: config.PushNotificationServerConfig.Enabled, 330 Identity: config.PushNotificationServerConfig.Identity, 331 GorushURL: config.PushNotificationServerConfig.GorushURL, 332 }), 333 protocol.WithDatabase(appDB), 334 protocol.WithWalletDatabase(walletDB), 335 protocol.WithTorrentConfig(&config.TorrentConfig), 336 protocol.WithWalletConfig(&config.WalletConfig), 337 protocol.WithAccountManager(backend.AccountManager()), 338 } 339 340 messenger, err := protocol.NewMessenger( 341 config.Name, 342 identity, 343 gethbridge.NewNodeBridge(backend.StatusNode().GethNode(), backend.StatusNode().WakuService(), backend.StatusNode().WakuV2Service()), 344 installationID.String(), 345 nil, 346 config.Version, 347 options..., 348 ) 349 if err != nil { 350 logger.Error("failed to create messenger", "error", err) 351 return 352 } 353 354 err = messenger.InitInstallations() 355 if err != nil { 356 logger.Error("failed to init messenger installations", "error", err) 357 return 358 } 359 360 err = messenger.InitFilters() 361 if err != nil { 362 logger.Error("failed to init messenger filters", "error", err) 363 return 364 } 365 366 // This will start the push notification server as well as 367 // the config is set to Enabled 368 _, err = messenger.Start() 369 if err != nil { 370 logger.Error("failed to start messenger", "error", err) 371 return 372 } 373 go retrieveMessagesLoop(messenger, 300*time.Millisecond, interruptCh) 374 } 375 } 376 377 gethNode := backend.StatusNode().GethNode() 378 if gethNode != nil { 379 // wait till node has been stopped 380 gethNode.Wait() 381 if err := sdnotify.Stopping(); err != nil { 382 logger.Warn("sd_notify STOPPING call failed", "error", err) 383 } 384 } 385 } 386 387 func getDefaultDataDir() string { 388 if home := os.Getenv("HOME"); home != "" { 389 return filepath.Join(home, ".statusd") 390 } 391 return "./statusd-data" 392 } 393 394 func setupLogging(config *params.NodeConfig) { 395 if *logLevel != "" { 396 config.LogLevel = *logLevel 397 } 398 399 logSettings := logutils.LogSettings{ 400 Enabled: config.LogEnabled, 401 MobileSystem: config.LogMobileSystem, 402 Level: config.LogLevel, 403 File: config.LogFile, 404 MaxSize: config.LogMaxSize, 405 MaxBackups: config.LogMaxBackups, 406 CompressRotated: config.LogCompressRotated, 407 } 408 colors := !(*logWithoutColors) && terminal.IsTerminal(int(os.Stdin.Fd())) 409 if err := logutils.OverrideRootLogWithConfig(logSettings, colors); err != nil { 410 stdlog.Fatalf("Error initializing logger: %v", err) 411 } 412 } 413 414 // loop for notifying systemd about process being alive 415 func startSystemDWatchdog() { 416 for range time.Tick(30 * time.Second) { 417 if err := sdnotify.Watchdog(); err != nil { 418 logger.Warn("sd_notify WATCHDOG call failed", "error", err) 419 } 420 } 421 } 422 423 // startCollectingStats collects various stats about the node and other protocols like Whisper. 424 func startCollectingNodeMetrics(interruptCh <-chan struct{}, statusNode *node.StatusNode) { 425 logger.Info("Starting collecting node metrics") 426 427 gethNode := statusNode.GethNode() 428 if gethNode == nil { 429 logger.Error("Failed to run metrics because it could not get the node") 430 return 431 } 432 433 ctx, cancel := context.WithCancel(context.Background()) 434 defer cancel() 435 go func() { 436 // Try to subscribe and collect metrics. In case of an error, retry. 437 for { 438 if err := nodemetrics.SubscribeServerEvents(ctx, gethNode); err != nil { 439 logger.Error("Failed to subscribe server events", "error", err) 440 } else { 441 // no error means that the subscription was terminated by purpose 442 return 443 } 444 445 time.Sleep(time.Second) 446 } 447 }() 448 449 <-interruptCh 450 } 451 452 var ( 453 errStatusServiceRequiresIPC = errors.New("to enable the StatusService on IPC, -ipc flag must be set") 454 errStatusServiceRequiresHTTP = errors.New("to enable the StatusService on HTTP, -http flag must be set") 455 errStatusServiceInvalidFlag = errors.New("-status flag valid values are: ipc, http") 456 ) 457 458 func configureStatusService(flagValue string, nodeConfig *params.NodeConfig) (*params.NodeConfig, error) { 459 switch flagValue { 460 case "ipc": 461 if !nodeConfig.IPCEnabled { 462 return nil, errStatusServiceRequiresIPC 463 } 464 nodeConfig.EnableStatusService = true 465 case "http": 466 if !nodeConfig.HTTPEnabled { 467 return nil, errStatusServiceRequiresHTTP 468 } 469 nodeConfig.EnableStatusService = true 470 nodeConfig.AddAPIModule("status") 471 case "": 472 nodeConfig.EnableStatusService = false 473 default: 474 return nil, errStatusServiceInvalidFlag 475 } 476 477 return nodeConfig, nil 478 } 479 480 // printVersion prints verbose output about version and config. 481 func printVersion(config *params.NodeConfig) { 482 fmt.Println(strings.Title(config.Name)) 483 fmt.Println("Version:", config.Version) 484 fmt.Println("Network ID:", config.NetworkID) 485 fmt.Println("Go Version:", runtime.Version()) 486 fmt.Println("OS:", runtime.GOOS) 487 fmt.Printf("GOPATH=%s\n", os.Getenv("GOPATH")) 488 fmt.Printf("GOROOT=%s\n", runtime.GOROOT()) 489 490 fmt.Println("Loaded Config: ", config) 491 } 492 493 func printUsage() { 494 usage := ` 495 Usage: statusd [options] 496 Examples: 497 statusd # run regular Whisper node that joins Status network 498 statusd -c ./default.json # run node with configuration specified in ./default.json file 499 statusd -c ./default.json -c ./standalone.json # run node with configuration specified in ./default.json file, after merging ./standalone.json file 500 statusd -c ./default.json -metrics # run node with configuration specified in ./default.json file, and expose ethereum metrics with debug_metrics jsonrpc call 501 statusd -c ./default.json -log DEBUG --seed-phrase="test test test test test test test test test test test junk" --password=password # run node with configuration specified in ./default.json file, and import account with seed phrase and password 502 503 Options: 504 ` 505 fmt.Fprint(os.Stderr, usage) 506 flag.PrintDefaults() 507 } 508 509 // haltOnInterruptSignal catches interrupt signal (SIGINT) and 510 // stops the node. It times out after 5 seconds 511 // if the node can not be stopped. 512 func haltOnInterruptSignal(statusNode *node.StatusNode) <-chan struct{} { 513 interruptCh := make(chan struct{}) 514 go func() { 515 signalCh := make(chan os.Signal, 1) 516 signal.Notify(signalCh, os.Interrupt) 517 defer signal.Stop(signalCh) 518 <-signalCh 519 close(interruptCh) 520 logger.Info("Got interrupt, shutting down...") 521 if err := statusNode.Stop(); err != nil { 522 logger.Error("Failed to stop node", "error", err) 523 os.Exit(1) 524 } 525 }() 526 return interruptCh 527 } 528 529 // retrieveMessagesLoop fetches messages from a messenger so that they are processed 530 func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration, cancel <-chan struct{}) { 531 ticker := time.NewTicker(tick) 532 defer ticker.Stop() 533 534 for { 535 select { 536 case <-ticker.C: 537 _, err := messenger.RetrieveAll() 538 if err != nil { 539 logger.Error("failed to retrieve raw messages", "err", err) 540 continue 541 } 542 case <-cancel: 543 return 544 } 545 } 546 } 547 548 func openDatabases(path string) (*sql.DB, *sql.DB, error) { 549 walletDB, err := walletdatabase.InitializeDB(path+"-wallet.db", "", dbsetup.ReducedKDFIterationsNumber) 550 if err != nil { 551 logger.Error("failed to initialize wallet db", "error", err) 552 return nil, nil, err 553 } 554 555 appDB, err := appdatabase.InitializeDB(path+".db", "", dbsetup.ReducedKDFIterationsNumber) 556 if err != nil { 557 logger.Error("failed to initialize app db", "error", err) 558 return nil, nil, err 559 } 560 561 return appDB, walletDB, nil 562 } 563 564 func createDirsFromConfig(config *params.NodeConfig) error { 565 // If DataDir is empty, it means we want to create an ephemeral node 566 // keeping data only in memory. 567 if config.DataDir != "" { 568 // make sure data directory exists 569 if err := os.MkdirAll(filepath.Clean(config.DataDir), os.ModePerm); err != nil { 570 return fmt.Errorf("make node: make data directory: %v", err) 571 } 572 } 573 574 if config.KeyStoreDir != "" { 575 // make sure keys directory exists 576 if err := os.MkdirAll(filepath.Clean(config.KeyStoreDir), os.ModePerm); err != nil { 577 return fmt.Errorf("make node: make keys directory: %v", err) 578 } 579 } 580 581 return nil 582 } 583 584 func startNode(config *params.NodeConfig, backend *api.GethStatusBackend, installationID uuid.UUID) (*sql.DB, *sql.DB, error) { 585 err := backend.AccountManager().InitKeystore(config.KeyStoreDir) 586 if err != nil { 587 logger.Error("Failed to init keystore", "error", err) 588 return nil, nil, err 589 } 590 591 err = createDirsFromConfig(config) 592 if err != nil { 593 logger.Error("failed to create directories", "error", err) 594 return nil, nil, err 595 } 596 597 appDB, walletDB, err := openDatabases(config.DataDir + "/" + installationID.String()) 598 if err != nil { 599 log.Error("failed to open databases") 600 return nil, nil, err 601 } 602 603 backend.StatusNode().SetAppDB(appDB) 604 backend.StatusNode().SetWalletDB(walletDB) 605 606 err = backend.StartNode(config) 607 if err != nil { 608 logger.Error("Node start failed", "error", err) 609 return nil, nil, err 610 } 611 612 return appDB, walletDB, nil 613 }