github.com/simpleiot/simpleiot@v0.18.3/server/server.go (about)

     1  package server
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io/fs"
     8  	"log"
     9  	"net"
    10  	"net/http"
    11  	"os"
    12  	"path"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/nats-io/nats-server/v2/server"
    17  	"github.com/nats-io/nats.go"
    18  	"github.com/oklog/run"
    19  	"github.com/simpleiot/simpleiot/api"
    20  	"github.com/simpleiot/simpleiot/client"
    21  	"github.com/simpleiot/simpleiot/frontend"
    22  	"github.com/simpleiot/simpleiot/node"
    23  	"github.com/simpleiot/simpleiot/store"
    24  )
    25  
    26  // ErrServerStopped is returned when the server is stopped
    27  var ErrServerStopped = errors.New("Server stopped")
    28  
    29  // Options used for starting Simple IoT
    30  type Options struct {
    31  	StoreFile         string
    32  	ResetStore        bool
    33  	DataDir           string
    34  	HTTPPort          string
    35  	DebugHTTP         bool
    36  	DebugLifecycle    bool
    37  	NatsServer        string
    38  	NatsDisableServer bool
    39  	NatsPort          int
    40  	NatsHTTPPort      int
    41  	NatsWSPort        int
    42  	NatsTLSCert       string
    43  	NatsTLSKey        string
    44  	NatsTLSTimeout    float64
    45  	AuthToken         string
    46  	ParticleAPIKey    string
    47  	AppVersion        string
    48  	OSVersionField    string
    49  	LogNats           bool
    50  	Dev               bool
    51  	CustomUIDir       string
    52  	CustomUIFS        fs.FS
    53  	UIAssetsDebug     bool
    54  	// optional ID (must be unique) for this instance, otherwise, a UUID will be used
    55  	ID string
    56  }
    57  
    58  // Server represents a SIOT server process
    59  type Server struct {
    60  	nc                 *nats.Conn
    61  	options            Options
    62  	natsServer         *server.Server
    63  	clients            *client.RunGroup
    64  	chNatsClientClosed chan struct{}
    65  	chStop             chan struct{}
    66  	chWaitStart        chan struct{}
    67  }
    68  
    69  // NewServer creates a new server
    70  func NewServer(o Options) (*Server, *nats.Conn, error) {
    71  	chNatsClientClosed := make(chan struct{})
    72  
    73  	// start the server side nats client
    74  	nc, err := nats.Connect(o.NatsServer,
    75  		nats.Timeout(10*time.Second),
    76  		nats.PingInterval(60*5*time.Second),
    77  		nats.MaxPingsOutstanding(5),
    78  		nats.ReconnectBufSize(5*1024*1024),
    79  		nats.SetCustomDialer(&net.Dialer{
    80  			KeepAlive: -1,
    81  		}),
    82  		nats.Token(o.AuthToken),
    83  		nats.RetryOnFailedConnect(true),
    84  		nats.MaxReconnects(60),
    85  		nats.ErrorHandler(func(_ *nats.Conn,
    86  			sub *nats.Subscription, err error) {
    87  			var subject string
    88  			if sub != nil {
    89  				subject = sub.Subject
    90  			}
    91  			log.Printf("Server NATS client error, sub: %v, err: %s\n", subject, err)
    92  		}),
    93  		nats.CustomReconnectDelay(func(attempts int) time.Duration {
    94  			log.Println("Server NATS client reconnect attempt #", attempts)
    95  			return time.Millisecond * 250
    96  		}),
    97  		nats.ReconnectHandler(func(_ *nats.Conn) {
    98  			log.Println("Server NATS client: reconnected")
    99  		}),
   100  		nats.ClosedHandler(func(_ *nats.Conn) {
   101  			log.Println("Server NATS client: closed")
   102  			close(chNatsClientClosed)
   103  		}),
   104  		nats.ConnectHandler(func(_ *nats.Conn) {
   105  			log.Println("Server NATS client: connected")
   106  		}),
   107  	)
   108  
   109  	return &Server{
   110  		nc:                 nc,
   111  		options:            o,
   112  		chNatsClientClosed: chNatsClientClosed,
   113  		chStop:             make(chan struct{}),
   114  		chWaitStart:        make(chan struct{}),
   115  		clients:            client.NewRunGroup("Server clients"),
   116  	}, nc, err
   117  }
   118  
   119  // AddClient can be used to add clients to the server.
   120  // Clients must be added before start is called. The
   121  // Server makes sure all clients are shut down before
   122  // shutting down the server. This makes for a cleaner
   123  // shutdown.
   124  func (s *Server) AddClient(client client.RunStop) {
   125  	s.clients.Add(client)
   126  }
   127  
   128  // Run the server -- only returns if there is an error
   129  func (s *Server) Run() error {
   130  	var g run.Group
   131  
   132  	logLS := func(_ ...any) {}
   133  
   134  	if s.options.DebugLifecycle {
   135  		logLS = func(m ...any) {
   136  			log.Println(m...)
   137  		}
   138  	}
   139  
   140  	o := s.options
   141  
   142  	var err error
   143  
   144  	// anything that needs to use the store or nats server should add to this wait group.
   145  	// The store will wait on this before shutting down
   146  	var storeWg sync.WaitGroup
   147  
   148  	// ====================================
   149  	// Nats server
   150  	// ====================================
   151  	natsOptions := natsServerOptions{
   152  		Port:       o.NatsPort,
   153  		HTTPPort:   o.NatsHTTPPort,
   154  		WSPort:     o.NatsWSPort,
   155  		Auth:       o.AuthToken,
   156  		TLSCert:    o.NatsTLSCert,
   157  		TLSKey:     o.NatsTLSKey,
   158  		TLSTimeout: o.NatsTLSTimeout,
   159  	}
   160  
   161  	if !o.NatsDisableServer {
   162  		s.natsServer, err = newNatsServer(natsOptions)
   163  		if err != nil {
   164  			return fmt.Errorf("Error setting up nats server: %v", err)
   165  		}
   166  
   167  		g.Add(func() error {
   168  			s.natsServer.Start()
   169  			s.natsServer.WaitForShutdown()
   170  			logLS("LS: Exited: nats server")
   171  			return fmt.Errorf("NATS server stopped")
   172  		}, func(_ error) {
   173  			go func() {
   174  				storeWg.Wait()
   175  				s.natsServer.Shutdown()
   176  				logLS("LS: Shutdown: nats server")
   177  			}()
   178  		})
   179  	}
   180  
   181  	// ====================================
   182  	// SIOT Store
   183  	// ====================================
   184  
   185  	storeParams := store.Params{
   186  		File:      o.StoreFile,
   187  		AuthToken: o.AuthToken,
   188  		Server:    o.NatsServer,
   189  		Nc:        s.nc,
   190  		ID:        s.options.ID,
   191  	}
   192  
   193  	siotStore, err := store.NewStore(storeParams)
   194  
   195  	if o.ResetStore {
   196  		if err := siotStore.Reset(); err != nil {
   197  			log.Fatal("Error resetting store:", err)
   198  		}
   199  	}
   200  
   201  	if err != nil {
   202  		log.Fatal("Error creating store: ", err)
   203  	}
   204  
   205  	siotWaitCtx, siotWaitCancel := context.WithTimeout(context.Background(), time.Second*10)
   206  
   207  	g.Add(func() error {
   208  		err := siotStore.Run()
   209  		logLS("LS: Exited: store")
   210  		return err
   211  	}, func(err error) {
   212  		// we just run in goroutine else this Stop blocking will block everything else
   213  		go func() {
   214  			storeWg.Wait()
   215  			siotWaitCancel()
   216  			siotStore.Stop(err)
   217  			logLS("LS: Shutdown: store")
   218  		}()
   219  	})
   220  
   221  	cancelTimer := make(chan struct{})
   222  	storeWg.Add(1)
   223  	g.Add(func() error {
   224  		defer storeWg.Done()
   225  		err := siotStore.WaitStart(siotWaitCtx)
   226  		if err != nil {
   227  			logLS("LS: Exited: metrics timeout waiting for store")
   228  			return err
   229  		}
   230  
   231  		// Hack -- this needs moved to a client
   232  		t := time.NewTimer(10 * time.Second)
   233  
   234  		select {
   235  		case <-t.C:
   236  		case <-cancelTimer:
   237  			logLS("LS: Exited: store metrics")
   238  			return nil
   239  		}
   240  
   241  		rootNode, err := client.GetNodes(s.nc, "root", "all", "", false)
   242  
   243  		if err != nil {
   244  			logLS("LS: Exited: store metrics")
   245  			return fmt.Errorf("Error getting root id for metrics: %v", err)
   246  		} else if len(rootNode) == 0 {
   247  			logLS("LS: Exited: store metrics")
   248  			return fmt.Errorf("Error getting root node, no data")
   249  		}
   250  
   251  		err = siotStore.StartMetrics(rootNode[0].ID)
   252  		logLS("LS: Exited: store metrics")
   253  		return err
   254  	}, func(err error) {
   255  		close(cancelTimer)
   256  		siotStore.StopMetrics(err)
   257  		logLS("LS: Shutdown: store metrics")
   258  	})
   259  
   260  	// ====================================
   261  	// Node manager
   262  	// ====================================
   263  
   264  	nodeManager := node.NewManger(s.nc, o.AppVersion, o.OSVersionField)
   265  
   266  	storeWg.Add(1)
   267  	g.Add(func() error {
   268  		defer storeWg.Done()
   269  		err := siotStore.WaitStart(siotWaitCtx)
   270  		if err != nil {
   271  			logLS("LS: Exited: node manager timeout waiting for store")
   272  			return err
   273  		}
   274  
   275  		err = nodeManager.Start()
   276  		logLS("LS: Exited: node manager")
   277  		return err
   278  	}, func(err error) {
   279  		nodeManager.Stop(err)
   280  		logLS("LS: Shutdown: node manager")
   281  	})
   282  
   283  	// ====================================
   284  	// Build in clients manager
   285  	// ====================================
   286  
   287  	storeWg.Add(1)
   288  	g.Add(func() error {
   289  		defer storeWg.Done()
   290  		err := siotStore.WaitStart(siotWaitCtx)
   291  		if err != nil {
   292  			logLS("LS: Exited: client manager timeout waiting for store")
   293  			return err
   294  		}
   295  
   296  		err = s.clients.Run()
   297  		logLS("LS: Exited: clients manager: ", err)
   298  		return err
   299  	}, func(err error) {
   300  		s.clients.Stop(err)
   301  		logLS("LS: Shutdown: clients manager")
   302  	})
   303  
   304  	// ====================================
   305  	// Embedded files
   306  	// ====================================
   307  
   308  	var feFS fs.FS
   309  
   310  	if o.CustomUIDir != "" {
   311  		log.Println("Using custom frontend directory:", o.CustomUIDir)
   312  		feFS = os.DirFS(o.CustomUIDir)
   313  	} else if o.CustomUIFS != nil {
   314  		feFS = o.CustomUIFS
   315  		if err != nil {
   316  			log.Fatal("Error getting frontend subtree: ", err)
   317  		}
   318  	} else {
   319  		if o.Dev {
   320  			log.Println("SIOT HTTP Server -- using local instead of embedded files")
   321  			feFS = os.DirFS("./frontend/public")
   322  		} else {
   323  			// remove output dir name from frontend assets filesystem
   324  			feFS, err = fs.Sub(frontend.Content, "public")
   325  			if err != nil {
   326  				log.Fatal("Error getting frontend subtree: ", err)
   327  			}
   328  		}
   329  	}
   330  
   331  	var listFiles func(fs.FS, string, int) error
   332  
   333  	listFiles = func(fsys fs.FS, dir string, level int) error {
   334  		entries, err := fs.ReadDir(feFS, dir)
   335  		if err != nil {
   336  			return err
   337  		}
   338  
   339  		prefix := "  - "
   340  		for i := 0; i < level; i++ {
   341  			prefix = "  " + prefix
   342  		}
   343  
   344  		for _, e := range entries {
   345  			log.Printf("%v%v\n", prefix, e.Name())
   346  			if e.IsDir() {
   347  				subdir := path.Join(dir, e.Name())
   348  				err := listFiles(fsys, subdir, level+1)
   349  				if err != nil {
   350  					return err
   351  				}
   352  			}
   353  		}
   354  
   355  		return nil
   356  	}
   357  
   358  	if o.UIAssetsDebug {
   359  		log.Println("List frontend assets: ")
   360  		err := listFiles(feFS, ".", 0)
   361  		if err != nil {
   362  			log.Println("Error listing frontend assets:", err)
   363  		}
   364  	}
   365  
   366  	// wrap with fs that will automatically look for and decompress gz
   367  	// versions of files.
   368  	feFSDecomp := newFsDecomp(feFS, "index.html")
   369  
   370  	// ====================================
   371  	// HTTP API
   372  	// ====================================
   373  	httpAPI := api.NewServer(api.ServerArgs{
   374  		Port:       o.HTTPPort,
   375  		NatsWSPort: o.NatsWSPort,
   376  		Filesystem: http.FS(feFSDecomp),
   377  		Debug:      o.DebugHTTP,
   378  		JwtAuth:    siotStore.GetAuthorizer(),
   379  		AuthToken:  o.AuthToken,
   380  		Nc:         s.nc,
   381  	})
   382  
   383  	g.Add(func() error {
   384  		err := httpAPI.Start()
   385  		logLS("LS: Exited: http api")
   386  		return err
   387  	}, func(err error) {
   388  		httpAPI.Stop(err)
   389  		logLS("LS: Shutdown: http api")
   390  	})
   391  
   392  	// Give us a way to stop the server
   393  	// and signal to waiters we have started
   394  	chShutdown := make(chan struct{})
   395  	g.Add(func() error {
   396  		err := siotStore.WaitStart(siotWaitCtx)
   397  		if err != nil {
   398  			logLS("LS: Exited: server stopper, timeout waiting for store")
   399  			return err
   400  		}
   401  
   402  		select {
   403  		case <-s.chStop:
   404  			logLS("LS: Exited: stop handler")
   405  			return ErrServerStopped
   406  		case <-chShutdown:
   407  			logLS("LS: Exited: stop handler")
   408  			return nil
   409  		}
   410  	}, func(_ error) {
   411  		close(chShutdown)
   412  		logLS("LS: Shutdown: stop handler")
   413  	})
   414  
   415  	chRunError := make(chan error)
   416  
   417  	go func() {
   418  		chRunError <- g.Run()
   419  	}()
   420  
   421  	var retErr error
   422  
   423  done:
   424  	for {
   425  		select {
   426  		// unblock any waits
   427  		case <-s.chWaitStart:
   428  			// No-op, reading channel is enough to unblock wait
   429  		case retErr = <-chRunError:
   430  			break done
   431  		}
   432  	}
   433  
   434  	s.nc.Close()
   435  
   436  	return retErr
   437  }
   438  
   439  // Stop server
   440  func (s *Server) Stop(_ error) {
   441  	close(s.chStop)
   442  }
   443  
   444  // WaitStart waits for server to start. Clients should wait for this
   445  // to complete before trying to fetch nodes, etc.
   446  func (s *Server) WaitStart(ctx context.Context) error {
   447  	waitDone := make(chan struct{})
   448  
   449  	go func() {
   450  		// the following will block until the main store select
   451  		// loop starts
   452  		s.chWaitStart <- struct{}{}
   453  		close(waitDone)
   454  	}()
   455  
   456  	select {
   457  	case <-ctx.Done():
   458  		return errors.New("Store wait timeout or canceled")
   459  	case <-waitDone:
   460  		// all is well
   461  		return nil
   462  	}
   463  
   464  }