github.com/vmware/transport-go@v1.3.4/plank/pkg/server/server.go (about) 1 // Copyright 2019-2021 VMware, Inc. 2 // SPDX-License-Identifier: BSD-2-Clause 3 4 package server 5 6 import ( 7 "context" 8 "crypto/tls" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "io/ioutil" 13 "net" 14 "net/http" 15 "os" 16 "os/signal" 17 "path" 18 "reflect" 19 "sync" 20 "sync/atomic" 21 "syscall" 22 "time" 23 24 "github.com/spf13/cobra" 25 "github.com/spf13/pflag" 26 "github.com/vmware/transport-go/model" 27 28 "github.com/gorilla/handlers" 29 "github.com/gorilla/mux" 30 "github.com/vmware/transport-go/bus" 31 "github.com/vmware/transport-go/plank/pkg/middleware" 32 "github.com/vmware/transport-go/plank/utils" 33 "github.com/vmware/transport-go/service" 34 ) 35 36 const PLANK_SERVER_ONLINE_CHANNEL = bus.TRANSPORT_INTERNAL_CHANNEL_PREFIX + "plank-online-notify" 37 const AllMethodsWildcard = "*" // every method, open the gates! 38 39 // NewPlatformServer configures and returns a new platformServer instance 40 func NewPlatformServer(config *PlatformServerConfig) PlatformServer { 41 if !checkConfigForLogConfig(config) { 42 utils.Log.Error("unable to create new platform server, log config not found") 43 return nil 44 } 45 46 ps := new(platformServer) 47 sanitizeConfigRootPath(config) 48 ps.serverConfig = config 49 ps.ServerAvailability = &ServerAvailability{} 50 ps.routerConcurrencyProtection = new(int32) 51 ps.messageBridgeMap = make(map[string]*MessageBridge) 52 ps.eventbus = bus.GetBus() 53 ps.initialize() 54 return ps 55 } 56 57 func checkConfigForLogConfig(config *PlatformServerConfig) bool { 58 if config.LogConfig != nil { 59 return true 60 } 61 return false 62 } 63 64 // NewPlatformServerFromConfig returns a new instance of PlatformServer based on the config JSON file provided as configPath 65 func NewPlatformServerFromConfig(configPath string) (PlatformServer, error) { 66 var config PlatformServerConfig 67 68 // no config no server 69 configBytes, err := ioutil.ReadFile(configPath) 70 if err != nil { 71 return nil, err 72 } 73 74 // malformed config no server as well 75 if err = json.Unmarshal(configBytes, &config); err != nil { 76 return nil, err 77 } 78 79 ps := new(platformServer) 80 ps.eventbus = bus.GetBus() 81 sanitizeConfigRootPath(&config) 82 83 // ensure references to file system paths are relative to config.RootDir 84 config.LogConfig.OutputLog = utils.JoinBasePathIfRelativeRegularFilePath(config.LogConfig.Root, config.LogConfig.OutputLog) 85 config.LogConfig.AccessLog = utils.JoinBasePathIfRelativeRegularFilePath(config.LogConfig.Root, config.LogConfig.AccessLog) 86 config.LogConfig.ErrorLog = utils.JoinBasePathIfRelativeRegularFilePath(config.LogConfig.Root, config.LogConfig.ErrorLog) 87 88 // handle invalid duration by setting it to the default value of 5 minutes 89 if config.ShutdownTimeout <= 0 { 90 config.ShutdownTimeout = 5 91 } 92 93 // handle invalid duration by setting it to the default value of 1 minute 94 if config.RestBridgeTimeout <= 0 { 95 config.RestBridgeTimeout = 1 96 } 97 98 // the raw value from the config.json needs to be multiplied by time.Minute otherwise it's interpreted as nanosecond 99 config.ShutdownTimeout = config.ShutdownTimeout * time.Minute 100 101 // the raw value from the config.json needs to be multiplied by time.Minute otherwise it's interpreted as nanosecond 102 config.RestBridgeTimeout = config.RestBridgeTimeout * time.Minute 103 104 if config.TLSCertConfig != nil { 105 if !path.IsAbs(config.TLSCertConfig.CertFile) { 106 config.TLSCertConfig.CertFile = path.Clean(path.Join(config.RootDir, config.TLSCertConfig.CertFile)) 107 } 108 109 if !path.IsAbs(config.TLSCertConfig.KeyFile) { 110 config.TLSCertConfig.KeyFile = path.Clean(path.Join(config.RootDir, config.TLSCertConfig.KeyFile)) 111 } 112 } 113 114 ps.serverConfig = &config 115 ps.ServerAvailability = &ServerAvailability{} 116 ps.routerConcurrencyProtection = new(int32) 117 ps.initialize() 118 return ps, nil 119 } 120 121 // CreateServerConfig creates a new instance of PlatformServerConfig and returns the pointer to it. 122 func CreateServerConfig() (*PlatformServerConfig, error) { 123 factory := &serverConfigFactory{} 124 factory.configureFlags(pflag.CommandLine) 125 factory.parseFlags(os.Args) 126 return generatePlatformServerConfig(factory) 127 } 128 129 // CreateServerConfigForCobraCommand performs the same as CreateServerConfig but loads the flags to 130 // the provided cobra Command's *pflag.FlagSet instead of the global FlagSet instance. 131 func CreateServerConfigForCobraCommand(cmd *cobra.Command) (*PlatformServerConfig, error) { 132 factory := &serverConfigFactory{} 133 factory.configureFlags(cmd.Flags()) 134 factory.parseFlags(os.Args) 135 return generatePlatformServerConfig(factory) 136 } 137 138 // StartServer starts listening on the host and port as specified by ServerConfig 139 func (ps *platformServer) StartServer(syschan chan os.Signal) { 140 connClosed := make(chan struct{}) 141 142 ps.SyscallChan = syschan 143 signal.Notify(ps.SyscallChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM) 144 145 // ensure port is available 146 ps.checkPortAvailability() 147 148 // finalize handler by setting out writer 149 ps.loadGlobalHttpHandler(ps.router) 150 151 // configure SPA 152 // NOTE: the reason SPA app route is configured during server startup is that if the base uri is `/` for SPA 153 // then all other routes registered after SPA route will be masked away. 154 ps.configureSPA() 155 156 go func() { 157 ps.ServerAvailability.Http = true 158 if ps.serverConfig.TLSCertConfig != nil { 159 utils.Log.Infof("[plank] Starting HTTP server at %s:%d with TLS", ps.serverConfig.Host, ps.serverConfig.Port) 160 if err := ps.HttpServer.ListenAndServeTLS(ps.serverConfig.TLSCertConfig.CertFile, ps.serverConfig.TLSCertConfig.KeyFile); err != nil { 161 if !errors.Is(err, http.ErrServerClosed) { 162 utils.Log.Fatalln(wrapError(errServerInit, err)) 163 } 164 } 165 } else { 166 utils.Log.Infof("[plank] Starting HTTP server at %s:%d", ps.serverConfig.Host, ps.serverConfig.Port) 167 if err := ps.HttpServer.ListenAndServe(); err != nil { 168 if !errors.Is(err, http.ErrServerClosed) { 169 utils.Log.Fatalln(wrapError(errServerInit, err)) 170 } 171 } 172 } 173 }() 174 175 // if Fabric broker configuration is found, start the broker 176 if ps.serverConfig.FabricConfig != nil { 177 go func() { 178 fabricPort := ps.serverConfig.Port 179 fabricEndpoint := ps.serverConfig.FabricConfig.FabricEndpoint 180 if ps.serverConfig.FabricConfig.UseTCP { 181 // if using TCP adjust port accordingly and drop endpoint 182 fabricPort = ps.serverConfig.FabricConfig.TCPPort 183 fabricEndpoint = "" 184 } 185 brokerLocation := fmt.Sprintf("%s:%d%s", ps.serverConfig.Host, fabricPort, fabricEndpoint) 186 utils.Log.Infof("[plank] Starting Transport broker at %s", brokerLocation) 187 ps.ServerAvailability.Fabric = true 188 189 if err := ps.eventbus.StartFabricEndpoint(ps.fabricConn, *ps.serverConfig.FabricConfig.EndpointConfig); err != nil { 190 utils.Log.Fatalln(wrapError(errServerInit, err)) 191 } 192 }() 193 } 194 195 // spawn another goroutine to respond to syscall to shut down servers and terminate the main thread 196 go func() { 197 <-ps.SyscallChan 198 // notify subscribers that the server is shutting down 199 _ = ps.eventbus.SendResponseMessage(PLANK_SERVER_ONLINE_CHANNEL, false, nil) 200 ps.StopServer() 201 close(connClosed) 202 }() 203 204 // notify subscribers that the server is ready to interact with 205 httpReady := false 206 for { 207 _, err := net.Dial("tcp", fmt.Sprintf(":%d", ps.serverConfig.Port)) 208 httpReady = err == nil 209 if !httpReady { 210 time.Sleep(1 * time.Millisecond) 211 utils.Log.Debugln("waiting for http server to be ready to accept connections") 212 continue 213 } 214 _ = ps.eventbus.SendResponseMessage(PLANK_SERVER_ONLINE_CHANNEL, true, nil) 215 break 216 } 217 218 <-connClosed 219 } 220 221 // StopServer attempts to gracefully stop the HTTP and STOMP server if running 222 func (ps *platformServer) StopServer() { 223 utils.Log.Infoln("[plank] Server shutting down") 224 ps.ServerAvailability.Http = false 225 226 baseCtx := context.Background() 227 shutdownCtx, cancel := context.WithTimeout(baseCtx, ps.serverConfig.ShutdownTimeout) 228 229 go func() { 230 select { 231 case <-shutdownCtx.Done(): 232 if errors.Is(shutdownCtx.Err(), context.DeadlineExceeded) { 233 utils.Log.Fatalf( 234 "Server failed to gracefully shut down after %s", 235 ps.serverConfig.ShutdownTimeout.String()) 236 } 237 } 238 }() 239 defer cancel() 240 241 // call all registered services' OnServerShutdown() hook 242 svcRegistry := service.GetServiceRegistry() 243 lcm := service.GetServiceLifecycleManager() 244 wg := sync.WaitGroup{} 245 for _, svcChannel := range svcRegistry.GetAllServiceChannels() { 246 hooks := lcm.GetServiceHooks(svcChannel) 247 if hooks != nil { 248 utils.Log.Infof("Teardown in progress for service at '%s'", svcChannel) 249 wg.Add(1) 250 go func(cName string, h service.ServiceLifecycleHookEnabled) { 251 h.OnServerShutdown() 252 utils.Log.Infof("Teardown completed for service at '%s'", cName) 253 wg.Done() 254 255 }(svcChannel, hooks) 256 } 257 } 258 259 // start graceful shutdown 260 err := ps.HttpServer.Shutdown(shutdownCtx) 261 if err != nil { 262 utils.Log.Errorln(err) 263 } 264 265 if ps.fabricConn != nil { 266 err = ps.eventbus.StopFabricEndpoint() 267 if err != nil { 268 utils.Log.Errorln(err) 269 } 270 ps.ServerAvailability.Fabric = false 271 } 272 273 // wait for all teardown jobs to be done. if shutdown deadline arrives earlier 274 // the main thread will be terminated forcefully 275 wg.Wait() 276 } 277 278 // SetStaticRoute adds a route where static resources will be served 279 func (ps *platformServer) SetStaticRoute(prefix, fullpath string, middlewareFn ...mux.MiddlewareFunc) { 280 ps.router.Handle(prefix, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 281 http.Redirect(w, r, prefix+"/", http.StatusMovedPermanently) 282 })) 283 284 ndir := NoDirFileSystem{http.Dir(fullpath)} 285 endpointHandlerMapKey := prefix + "*" 286 compositeHandler := http.StripPrefix(prefix, middleware.BasicSecurityHeaderMiddleware()(http.FileServer(ndir))) 287 288 for _, mw := range middlewareFn { 289 compositeHandler = mw(compositeHandler) 290 } 291 292 ps.endpointHandlerMap[endpointHandlerMapKey] = compositeHandler.(http.HandlerFunc) 293 ps.router.PathPrefix(prefix + "/").Name(endpointHandlerMapKey).Handler(ps.endpointHandlerMap[endpointHandlerMapKey]) 294 } 295 296 // RegisterService registers a Fabric service with Bifrost 297 func (ps *platformServer) RegisterService(svc service.FabricService, svcChannel string) error { 298 sr := service.GetServiceRegistry() 299 err := sr.RegisterService(svc, svcChannel) 300 svcType := reflect.TypeOf(svc) 301 302 if err == nil { 303 utils.Log.Infof("[plank] Service '%s' registered at channel '%s'", svcType.String(), svcChannel) 304 svcLifecycleManager := service.GetServiceLifecycleManager() 305 var hooks service.ServiceLifecycleHookEnabled 306 if hooks = svcLifecycleManager.GetServiceHooks(svcChannel); hooks == nil { 307 // if service has no lifecycle hooks mark the channel as ready straight up 308 storeManager := ps.eventbus.GetStoreManager() 309 store := storeManager.GetStore(service.ServiceReadyStore) 310 store.Put(svcChannel, true, service.ServiceInitStateChange) 311 utils.Log.Infof("[plank] Service '%s' initialized successfully", svcType.String()) 312 } 313 } 314 return err 315 } 316 317 // SetHttpChannelBridge establishes a conduit between the transport service channel and an HTTP endpoint 318 // that allows a client to invoke the service via REST. 319 func (ps *platformServer) SetHttpChannelBridge(bridgeConfig *service.RESTBridgeConfig) { 320 ps.lock.Lock() 321 defer ps.lock.Unlock() 322 323 endpointHandlerKey := bridgeConfig.Uri + "-" + bridgeConfig.Method 324 325 if _, ok := ps.endpointHandlerMap[endpointHandlerKey]; ok { 326 utils.Log.Warnf("[plank] Endpoint '%s (%s)' is already associated with a handler. "+ 327 "Try another endpoint or remove it before assigning a new handler", bridgeConfig.Uri, bridgeConfig.Method) 328 return 329 } 330 331 // create a map for service channel - bridges mapping if it does not exist 332 if ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] == nil { 333 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] = make([]string, 0) 334 } 335 336 if _, exists := ps.messageBridgeMap[bridgeConfig.ServiceChannel]; !exists { 337 handler, _ := ps.eventbus.ListenStream(bridgeConfig.ServiceChannel) 338 handler.Handle(func(message *model.Message) { 339 ps.messageBridgeMap[bridgeConfig.ServiceChannel].payloadChannel <- message 340 }, func(err error) {}) 341 342 ps.messageBridgeMap[bridgeConfig.ServiceChannel] = &MessageBridge{ 343 ServiceListenStream: handler, 344 payloadChannel: make(chan *model.Message, 100), 345 } 346 } 347 348 // NOTE: mux.Router does not have mutex or any locking mechanism so it could sometimes lead to concurrency write 349 // panics. the following is to ensure the modification to ps.router can happen only once per thread, this atomic 350 // counter also protects against concurrent writing to ps.endpointHandlerMap 351 for !atomic.CompareAndSwapInt32(ps.routerConcurrencyProtection, 0, 1) { 352 time.Sleep(1 * time.Nanosecond) 353 } 354 355 // build endpoint handler 356 ps.endpointHandlerMap[endpointHandlerKey] = ps.buildEndpointHandler( 357 bridgeConfig.ServiceChannel, 358 bridgeConfig.FabricRequestBuilder, 359 ps.serverConfig.RestBridgeTimeout, 360 ps.messageBridgeMap[bridgeConfig.ServiceChannel].payloadChannel) 361 362 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] = append( 363 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel], endpointHandlerKey) 364 365 permittedMethods := []string{bridgeConfig.Method} 366 if bridgeConfig.AllowHead { 367 permittedMethods = append(permittedMethods, http.MethodHead) 368 } 369 if bridgeConfig.AllowOptions { 370 permittedMethods = append(permittedMethods, http.MethodOptions) 371 } 372 373 ps.router. 374 Path(bridgeConfig.Uri). 375 Methods(permittedMethods...). 376 Name(fmt.Sprintf("%s-%s", bridgeConfig.Uri, bridgeConfig.Method)). 377 Handler(ps.endpointHandlerMap[endpointHandlerKey]) 378 if !atomic.CompareAndSwapInt32(ps.routerConcurrencyProtection, 1, 0) { 379 panic("Concurrency write on router detected when running ") 380 } 381 382 utils.Log.Infof( 383 "[plank] Service channel '%s' is now bridged to a REST endpoint %s (%s)", 384 bridgeConfig.ServiceChannel, bridgeConfig.Uri, bridgeConfig.Method) 385 } 386 387 // SetHttpPathPrefixChannelBridge establishes a conduit between the transport service channel and a path prefix 388 // every request on this prefix will be sent through to the target service, all methods, all sub paths, lock, stock and barrel. 389 func (ps *platformServer) SetHttpPathPrefixChannelBridge(bridgeConfig *service.RESTBridgeConfig) { 390 ps.lock.Lock() 391 defer ps.lock.Unlock() 392 393 endpointHandlerKey := bridgeConfig.Uri + "-" + AllMethodsWildcard 394 395 if _, ok := ps.endpointHandlerMap[endpointHandlerKey]; ok { 396 utils.Log.Warnf("[plank] Path prefix '%s (%s)' is already being handled. "+ 397 "Try another prefix or remove it before assigning a new handler", bridgeConfig.Uri, bridgeConfig.Method) 398 return 399 } 400 401 // create a map for service channel - bridges mapping if it does not exist 402 if ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] == nil { 403 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] = make([]string, 0) 404 } 405 406 if _, exists := ps.messageBridgeMap[bridgeConfig.ServiceChannel]; !exists { 407 handler, _ := ps.eventbus.ListenStream(bridgeConfig.ServiceChannel) 408 handler.Handle(func(message *model.Message) { 409 ps.messageBridgeMap[bridgeConfig.ServiceChannel].payloadChannel <- message 410 }, func(err error) {}) 411 412 ps.messageBridgeMap[bridgeConfig.ServiceChannel] = &MessageBridge{ 413 ServiceListenStream: handler, 414 payloadChannel: make(chan *model.Message, 100), 415 } 416 } 417 418 // build endpoint handler 419 ps.endpointHandlerMap[endpointHandlerKey] = ps.buildEndpointHandler( 420 bridgeConfig.ServiceChannel, 421 bridgeConfig.FabricRequestBuilder, 422 ps.serverConfig.RestBridgeTimeout, 423 ps.messageBridgeMap[bridgeConfig.ServiceChannel].payloadChannel) 424 425 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel] = append( 426 ps.serviceChanToBridgeEndpoints[bridgeConfig.ServiceChannel], endpointHandlerKey) 427 428 // NOTE: mux.Router does not have mutex or any locking mechanism so it could sometimes lead to concurrency write 429 // panics. the following is to ensure the modification to ps.router can happen only once per thread 430 for !atomic.CompareAndSwapInt32(ps.routerConcurrencyProtection, 0, 1) { 431 time.Sleep(1 * time.Nanosecond) 432 } 433 ps.router. 434 PathPrefix(bridgeConfig.Uri). 435 Name(endpointHandlerKey). 436 Handler(ps.endpointHandlerMap[endpointHandlerKey]) 437 if !atomic.CompareAndSwapInt32(ps.routerConcurrencyProtection, 1, 0) { 438 panic("Concurrency write on router detected when running SetHttpPathPrefixChannelBridge()") 439 } 440 441 utils.Log.Infof( 442 "[plank] Service channel '%s' is now bridged to a REST path prefix '%s'", 443 bridgeConfig.ServiceChannel, bridgeConfig.Uri) 444 445 } 446 447 // GetMiddlewareManager returns the MiddleManager instance 448 func (ps *platformServer) GetMiddlewareManager() middleware.MiddlewareManager { 449 return ps.middlewareManager 450 } 451 452 func (ps *platformServer) GetRestBridgeSubRoute(uri, method string) (*mux.Route, error) { 453 route, err := ps.getSubRoute(fmt.Sprintf("%s-%s", uri, method)) 454 if route == nil { 455 return nil, fmt.Errorf("no route exists at %s (%s) exists", uri, method) 456 } 457 return route, err 458 } 459 460 // CustomizeTLSConfig is used to create a customized TLS configuration for use with http.Server. 461 // this function needs to be called before the server starts, otherwise it will error out. 462 func (c *platformServer) CustomizeTLSConfig(tls *tls.Config) error { 463 if c.ServerAvailability.Http || c.ServerAvailability.Fabric { 464 return fmt.Errorf("TLS configuration can be provided only if the server is not running") 465 } 466 c.HttpServer.TLSConfig = tls 467 return nil 468 } 469 470 // clearHttpChannelBridgesForService takes serviceChannel, gets all mux.Route instances associated with 471 // the service and removes them while keeping the rest of the routes intact. returns the pointer 472 // of a new instance of mux.Router. 473 func (ps *platformServer) clearHttpChannelBridgesForService(serviceChannel string) *mux.Router { 474 ps.lock.Lock() 475 defer ps.lock.Unlock() 476 477 // NOTE: gorilla mux doesn't allow us to mutate routes field of the Router struct which is critical in rerouting incoming 478 // requests to the new route. there is not a public API that allows us to do it so we're instead creating a new instance of 479 // Router and assigning the existing config and route. this means `ps.route` is treated as immutable and will be 480 // replaced with a new instance of mux.Router by the operation performed in this function 481 482 // walk over existing routes and store them temporarily EXCEPT the ones that are being overwritten which can 483 // be tracked by the service channel 484 newRouter := mux.NewRouter().Schemes("http", "https").Subrouter() 485 lookupMap := make(map[string]bool) 486 for _, key := range ps.serviceChanToBridgeEndpoints[serviceChannel] { 487 lookupMap[key] = true 488 } 489 490 ps.router.Walk(func(r *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 491 name := r.GetName() 492 path, _ := r.GetPathTemplate() 493 handler := r.GetHandler() 494 methods, _ := r.GetMethods() 495 // do not want to copy over the routes that will be overridden 496 if lookupMap[name] { 497 utils.Log.Debugf("[plank] route '%s' will be overridden so not copying over to the new router instance", name) 498 } else { 499 newRouter.Name(name).Path(path).Methods(methods...).Handler(handler) 500 } 501 return nil 502 }) 503 504 // if in override mode delete existing mappings associated with the service 505 existingMappings := ps.serviceChanToBridgeEndpoints[serviceChannel] 506 ps.serviceChanToBridgeEndpoints[serviceChannel] = make([]string, 0) 507 for _, handlerKey := range existingMappings { 508 utils.Log.Infof("[plank] Removing existing service - REST mapping '%s' for service '%s'", handlerKey, serviceChannel) 509 delete(ps.endpointHandlerMap, handlerKey) 510 } 511 return newRouter 512 } 513 514 func (ps *platformServer) getSubRoute(name string) (*mux.Route, error) { 515 route := ps.router.Get(name) 516 if route == nil { 517 return nil, fmt.Errorf("no route exists under name %s", name) 518 } 519 return route, nil 520 } 521 522 func (ps *platformServer) loadGlobalHttpHandler(h *mux.Router) { 523 ps.lock.Lock() 524 defer ps.lock.Unlock() 525 ps.router = h 526 ps.HttpServer.Handler = handlers.RecoveryHandler()( 527 handlers.CompressHandler( 528 handlers.ProxyHeaders( 529 handlers.CombinedLoggingHandler( 530 ps.serverConfig.LogConfig.GetAccessLogFilePointer(), ps.router)))) 531 } 532 533 func (ps *platformServer) checkPortAvailability() { 534 // is the port free? 535 _, err := net.Dial("tcp", fmt.Sprintf(":%d", ps.serverConfig.Port)) 536 537 // connection should fail otherwise it means there's already a listener on the host+port combination, in which case we stop here 538 if err == nil { 539 utils.Log.Fatalf("Server could not start at %s:%d because another process is using it. Please try another endpoint.", 540 ps.serverConfig.Host, ps.serverConfig.Port) 541 } 542 } 543 544 func (ps *platformServer) setEventBusRef(evtBus bus.EventBus) { 545 ps.lock.Lock() 546 ps.eventbus = evtBus 547 ps.lock.Unlock() 548 }