diff --git a/app.go b/app.go index ab2b9e252..47dc3a509 100644 --- a/app.go +++ b/app.go @@ -1,6 +1,10 @@ package wails import ( + "os" + "syscall" + + "github.com/syossan27/tebata" "github.com/wailsapp/wails/cmd" "github.com/wailsapp/wails/lib/binding" "github.com/wailsapp/wails/lib/event" @@ -66,6 +70,7 @@ func CreateApp(optionalConfig ...*AppConfig) *App { // Run the app func (a *App) Run() error { + if BuildMode != cmd.BuildModeProd { return a.cli.Run() } @@ -97,6 +102,13 @@ func (a *App) start() error { return err } + // Start signal handler + t := tebata.New(os.Interrupt, os.Kill, syscall.SIGTERM, syscall.SIGINT, syscall.SIGKILL) + t.Reserve(func() { + a.log.Debug("SIGNAL CAUGHT! Starting Shutdown") + a.renderer.Close() + }) + // Start event manager and give it our renderer a.eventManager.Start(a.renderer) @@ -112,8 +124,33 @@ func (a *App) start() error { return err } + // Defer the shutdown + defer a.shutdown() + // Run the renderer - return a.renderer.Run() + err = a.renderer.Run() + if err != nil { + return err + } + + return nil +} + +// shutdown the app +func (a *App) shutdown() { + // Make sure this is only called once + a.log.Debug("Shutting down") + + // Shutdown Binding Manager + a.bindingManager.Shutdown() + + // Shutdown IPC Manager + a.ipc.Shutdown() + + // Shutdown Event Manager + a.eventManager.Shutdown() + + a.log.Debug("Cleanly Shutdown") } // Bind allows the user to bind the given object diff --git a/go.mod b/go.mod index 01e6b0c51..d725cb728 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.1 github.com/stretchr/testify v1.3.0 // indirect + github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 // indirect golang.org/x/net v0.0.0-20190509222800-a4d6f7feada5 // indirect golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 diff --git a/go.sum b/go.sum index f3d52753e..96626b9aa 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,8 @@ github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba h1:2DHfQOxcpWdGf5q5IzCUFPNvRX9Icf+09RvQK2VnJq0= +github.com/syossan27/tebata v0.0.0-20180602121909-b283fe4bc5ba/go.mod h1:iLnlXG2Pakcii2CU0cbY07DRCSvpWNa7nFxtevhOChk= golang.org/x/crypto v0.0.0-20190123085648-057139ce5d2b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= diff --git a/lib/binding/manager.go b/lib/binding/manager.go index 1c10b5686..43b3959dc 100644 --- a/lib/binding/manager.go +++ b/lib/binding/manager.go @@ -16,6 +16,7 @@ type Manager struct { functions map[string]*boundFunction internalMethods *internalMethods initMethods []*boundMethod + shutdownMethods []*boundMethod log *logger.CustomLogger renderer interfaces.Renderer runtime interfaces.Runtime // The runtime object to pass to bound structs @@ -127,6 +128,9 @@ func (b *Manager) bindMethod(object interface{}) error { if newMethod.isWailsInit { b.log.Debugf("Detected WailsInit function: %s", fullMethodName) b.initMethods = append(b.initMethods, newMethod) + } else if newMethod.isWailsShutdown { + b.log.Debugf("Detected WailsShutdown function: %s", fullMethodName) + b.shutdownMethods = append(b.shutdownMethods, newMethod) } else { // Save boundMethod b.log.Infof("Bound Method: %s()", fullMethodName) @@ -292,3 +296,13 @@ func (b *Manager) callWailsInitMethods() error { } return nil } + +// Shutdown the binding manager +func (b *Manager) Shutdown() { + b.log.Debug("Shutdown called") + for _, method := range b.shutdownMethods { + b.log.Debugf("Calling Shutdown for method: %s", method.fullName) + method.call("[]") + } + b.log.Debug("Shutdown complete") +} diff --git a/lib/binding/method.go b/lib/binding/method.go index 908ba94c3..4b0897331 100644 --- a/lib/binding/method.go +++ b/lib/binding/method.go @@ -18,6 +18,7 @@ type boundMethod struct { log *logger.CustomLogger hasErrorReturnType bool // Indicates if there is an error return type isWailsInit bool + isWailsShutdown bool } // Creates a new bound method based on the given method + type @@ -39,6 +40,11 @@ func newBoundMethod(name string, fullName string, method reflect.Value, objectTy err = result.processWailsInit() } + // Are we a WailsShutdown method? + if result.Name == "WailsShutdown" { + err = result.processWailsShutdown() + } + return result, err } @@ -211,3 +217,20 @@ func (b *boundMethod) processWailsInit() error { return nil } + +func (b *boundMethod) processWailsShutdown() error { + // We must have only 1 input, it must be *wails.Runtime + if len(b.inputs) != 0 { + return fmt.Errorf("Invalid WailsShutdown() definition. Expected 0 inputs, but got %d", len(b.inputs)) + } + + // We must have only 1 output, it must be error + if len(b.returnTypes) != 0 { + return fmt.Errorf("Invalid WailsShutdown() definition. Expected 0 return types, but got %d", len(b.returnTypes)) + } + + // We are indeed a wails Shutdown method + b.isWailsShutdown = true + + return nil +} diff --git a/lib/event/manager.go b/lib/event/manager.go index d8442e14a..4f73bea93 100644 --- a/lib/event/manager.go +++ b/lib/event/manager.go @@ -3,18 +3,21 @@ package event import ( "fmt" "sync" + "time" + + "github.com/wailsapp/wails/lib/interfaces" "github.com/wailsapp/wails/lib/logger" "github.com/wailsapp/wails/lib/messages" - "github.com/wailsapp/wails/lib/interfaces" ) // Manager handles and processes events type Manager struct { incomingEvents chan *messages.EventData listeners map[string][]*eventListener - exit bool + running bool log *logger.CustomLogger renderer interfaces.Renderer // Messages will be dispatched to the frontend + wg sync.WaitGroup } // NewManager creates a new event manager with a 100 event buffer @@ -22,7 +25,7 @@ func NewManager() interfaces.EventManager { return &Manager{ incomingEvents: make(chan *messages.EventData, 100), listeners: make(map[string][]*eventListener), - exit: false, + running: false, log: logger.NewCustomLogger("Events"), } } @@ -87,15 +90,14 @@ func (e *Manager) Start(renderer interfaces.Renderer) { // Store renderer e.renderer = renderer - // Set up waitgroup so we can wait for goroutine to start - var wg sync.WaitGroup - wg.Add(1) + // Set up waitgroup so we can wait for goroutine to quit + e.running = true + e.wg.Add(1) // Run main loop in separate goroutine go func() { - wg.Done() e.log.Info("Listening") - for e.exit == false { + for e.running { // TODO: Listen for application exit select { case event := <-e.incomingEvents: @@ -139,14 +141,18 @@ func (e *Manager) Start(renderer interfaces.Renderer) { } } } + default: + time.Sleep(1 * time.Millisecond) } } + e.wg.Done() }() - - // Wait for goroutine to start - wg.Wait() } -func (e *Manager) stop() { - e.exit = true +// Shutdown is called when exiting the Application +func (e *Manager) Shutdown() { + e.log.Debug("Shutting Down") + e.running = false + e.log.Debug("Waiting for main loop to exit") + e.wg.Wait() } diff --git a/lib/interfaces/bindingmanager.go b/lib/interfaces/bindingmanager.go index 72d156d32..e145d3b95 100644 --- a/lib/interfaces/bindingmanager.go +++ b/lib/interfaces/bindingmanager.go @@ -7,4 +7,5 @@ type BindingManager interface { Bind(object interface{}) Start(renderer Renderer, runtime Runtime) error ProcessCall(callData *messages.CallData) (result interface{}, err error) + Shutdown() } diff --git a/lib/interfaces/eventmanager.go b/lib/interfaces/eventmanager.go index 270d5b207..c4f4e51fc 100644 --- a/lib/interfaces/eventmanager.go +++ b/lib/interfaces/eventmanager.go @@ -8,4 +8,5 @@ type EventManager interface { Emit(eventName string, optionalData ...interface{}) On(eventName string, callback func(...interface{})) Start(Renderer) + Shutdown() } diff --git a/lib/interfaces/ipcmanager.go b/lib/interfaces/ipcmanager.go index dd6a61d77..9d077203b 100644 --- a/lib/interfaces/ipcmanager.go +++ b/lib/interfaces/ipcmanager.go @@ -5,4 +5,5 @@ type IPCManager interface { BindRenderer(Renderer) Dispatch(message string) Start(eventManager EventManager, bindingManager BindingManager) + Shutdown() } diff --git a/lib/ipc/manager.go b/lib/ipc/manager.go index 76d30f6d1..fb1a26adc 100644 --- a/lib/ipc/manager.go +++ b/lib/ipc/manager.go @@ -2,6 +2,8 @@ package ipc import ( "fmt" + "sync" + "time" "github.com/wailsapp/wails/lib/interfaces" "github.com/wailsapp/wails/lib/logger" @@ -12,18 +14,20 @@ import ( type Manager struct { renderer interfaces.Renderer // The renderer messageQueue chan *ipcMessage - // quitChannel chan struct{} + quitChannel chan struct{} // signals chan os.Signal log *logger.CustomLogger eventManager interfaces.EventManager bindingManager interfaces.BindingManager + running bool + wg sync.WaitGroup } // NewManager creates a new IPC Manager func NewManager() interfaces.IPCManager { result := &Manager{ messageQueue: make(chan *ipcMessage, 100), - // quitChannel: make(chan struct{}), + quitChannel: make(chan struct{}), // signals: make(chan os.Signal, 1), log: logger.NewCustomLogger("IPC"), } @@ -44,9 +48,12 @@ func (i *Manager) Start(eventManager interfaces.EventManager, bindingManager int i.log.Info("Starting") // signal.Notify(manager.signals, os.Interrupt) + i.running = true + + // Keep track of this goroutine + i.wg.Add(1) go func() { - running := true - for running { + for i.running { select { case incomingMessage := <-i.messageQueue: i.log.DebugFields("Processing message", logger.Fields{ @@ -117,15 +124,12 @@ func (i *Manager) Start(eventManager interfaces.EventManager, bindingManager int i.log.DebugFields("Finished processing message", logger.Fields{ "1D": &incomingMessage, }) - // case <-manager.quitChannel: - // Debug("[MessageQueue] Quit caught") - // running = false - // case <-manager.signals: - // Debug("[MessageQueue] Signal caught") - // running = false + default: + time.Sleep(1 * time.Millisecond) } } i.log.Debug("Stopping") + i.wg.Done() }() } @@ -167,3 +171,11 @@ func (i *Manager) SendResponse(response *ipcResponse) error { // Call back to the front end return i.renderer.Callback(data) } + +// Shutdown is called when exiting the Application +func (i *Manager) Shutdown() { + i.log.Debug("Shutdown called") + i.running = false + i.log.Debug("Waiting of main loop shutdown") + i.wg.Wait() +} diff --git a/lib/renderer/bridge.go b/lib/renderer/bridge.go index e445c92fb..c4455fead 100644 --- a/lib/renderer/bridge.go +++ b/lib/renderer/bridge.go @@ -156,7 +156,7 @@ func (h *Bridge) Run() error { h.log.Info("The frontend will connect automatically.") err := h.server.ListenAndServe() - if err != nil { + if err != nil && err != http.ErrServerClosed { h.log.Fatal(err.Error()) } return err @@ -250,5 +250,9 @@ func (h *Bridge) SetTitle(title string) { // Close is unsupported for Bridge but required // for the Renderer interface func (h *Bridge) Close() { - h.log.Warn("Close() unsupported in bridge mode") + h.log.Debug("Shutting down") + err := h.server.Close() + if err != nil { + h.log.Errorf(err.Error()) + } }