github.com/MetalBlockchain/metalgo@v1.11.9/app/app.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package app
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"os/signal"
    10  	"sync"
    11  	"syscall"
    12  
    13  	"go.uber.org/zap"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	"github.com/MetalBlockchain/metalgo/node"
    17  	"github.com/MetalBlockchain/metalgo/utils/logging"
    18  	"github.com/MetalBlockchain/metalgo/utils/perms"
    19  	"github.com/MetalBlockchain/metalgo/utils/ulimit"
    20  )
    21  
    22  const Header = `        ███    ███ ███████ ████████  █████  ██      
    23  	████  ████ ██         ██    ██   ██ ██      
    24  	██ ████ ██ █████      ██    ███████ ██      
    25  	██  ██  ██ ██         ██    ██   ██ ██      
    26  	██      ██ ███████    ██    ██   ██ ███████`
    27  
    28  var _ App = (*app)(nil)
    29  
    30  type App interface {
    31  	// Start kicks off the application and returns immediately.
    32  	// Start should only be called once.
    33  	Start() error
    34  
    35  	// Stop notifies the application to exit and returns immediately.
    36  	// Stop should only be called after [Start].
    37  	// It is safe to call Stop multiple times.
    38  	Stop() error
    39  
    40  	// ExitCode should only be called after [Start] returns with no error. It
    41  	// should block until the application finishes
    42  	ExitCode() (int, error)
    43  }
    44  
    45  func New(config node.Config) (App, error) {
    46  	// Set the data directory permissions to be read write.
    47  	if err := perms.ChmodR(config.DatabaseConfig.Path, true, perms.ReadWriteExecute); err != nil {
    48  		return nil, fmt.Errorf("failed to restrict the permissions of the database directory with: %w", err)
    49  	}
    50  	if err := perms.ChmodR(config.LoggingConfig.Directory, true, perms.ReadWriteExecute); err != nil {
    51  		return nil, fmt.Errorf("failed to restrict the permissions of the log directory with: %w", err)
    52  	}
    53  
    54  	logFactory := logging.NewFactory(config.LoggingConfig)
    55  	log, err := logFactory.Make("main")
    56  	if err != nil {
    57  		logFactory.Close()
    58  		return nil, fmt.Errorf("failed to initialize log: %w", err)
    59  	}
    60  
    61  	// update fd limit
    62  	fdLimit := config.FdLimit
    63  	if err := ulimit.Set(fdLimit, log); err != nil {
    64  		log.Fatal("failed to set fd-limit",
    65  			zap.Error(err),
    66  		)
    67  		logFactory.Close()
    68  		return nil, err
    69  	}
    70  
    71  	n, err := node.New(&config, logFactory, log)
    72  	if err != nil {
    73  		log.Stop()
    74  		logFactory.Close()
    75  		return nil, fmt.Errorf("failed to initialize node: %w", err)
    76  	}
    77  
    78  	return &app{
    79  		node:       n,
    80  		log:        log,
    81  		logFactory: logFactory,
    82  	}, nil
    83  }
    84  
    85  func Run(app App) int {
    86  	// start running the application
    87  	if err := app.Start(); err != nil {
    88  		return 1
    89  	}
    90  
    91  	// register signals to kill the application
    92  	signals := make(chan os.Signal, 1)
    93  	signal.Notify(signals, syscall.SIGINT)
    94  	signal.Notify(signals, syscall.SIGTERM)
    95  
    96  	// start up a new go routine to handle attempts to kill the application
    97  	var eg errgroup.Group
    98  	eg.Go(func() error {
    99  		for range signals {
   100  			return app.Stop()
   101  		}
   102  		return nil
   103  	})
   104  
   105  	// wait for the app to exit and get the exit code response
   106  	exitCode, err := app.ExitCode()
   107  
   108  	// shut down the signal go routine
   109  	signal.Stop(signals)
   110  	close(signals)
   111  
   112  	// if there was an error closing or running the application, report that error
   113  	if eg.Wait() != nil || err != nil {
   114  		return 1
   115  	}
   116  
   117  	// return the exit code that the application reported
   118  	return exitCode
   119  }
   120  
   121  // app is a wrapper around a node that runs in this process
   122  type app struct {
   123  	node       *node.Node
   124  	log        logging.Logger
   125  	logFactory logging.Factory
   126  	exitWG     sync.WaitGroup
   127  }
   128  
   129  // Start the business logic of the node (as opposed to config reading, etc).
   130  // Does not block until the node is done. Errors returned from this method
   131  // are not logged.
   132  func (a *app) Start() error {
   133  	// [p.ExitCode] will block until [p.exitWG.Done] is called
   134  	a.exitWG.Add(1)
   135  	go func() {
   136  		defer func() {
   137  			if r := recover(); r != nil {
   138  				fmt.Println("caught panic", r)
   139  			}
   140  			a.log.Stop()
   141  			a.logFactory.Close()
   142  			a.exitWG.Done()
   143  		}()
   144  		defer func() {
   145  			// If [p.node.Dispatch()] panics, then we should log the panic and
   146  			// then re-raise the panic. This is why the above defer is broken
   147  			// into two parts.
   148  			a.log.StopOnPanic()
   149  		}()
   150  
   151  		err := a.node.Dispatch()
   152  		a.log.Debug("dispatch returned",
   153  			zap.Error(err),
   154  		)
   155  	}()
   156  	return nil
   157  }
   158  
   159  // Stop attempts to shutdown the currently running node. This function will
   160  // return immediately.
   161  func (a *app) Stop() error {
   162  	a.node.Shutdown(0)
   163  	return nil
   164  }
   165  
   166  // ExitCode returns the exit code that the node is reporting. This function
   167  // blocks until the node has been shut down.
   168  func (a *app) ExitCode() (int, error) {
   169  	a.exitWG.Wait()
   170  	return a.node.ExitCode(), nil
   171  }