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 }