diff --git a/v2/internal/app/desktop.go b/v2/internal/app/desktop.go index 405d07a9..82c85ae7 100644 --- a/v2/internal/app/desktop.go +++ b/v2/internal/app/desktop.go @@ -3,6 +3,9 @@ package app import ( + "context" + "sync" + "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/ffenestri" "github.com/wailsapp/wails/v2/internal/logger" @@ -26,10 +29,10 @@ type App struct { options *options.App // Subsystems - log *subsystem.Log - runtime *subsystem.Runtime - event *subsystem.Event - binding *subsystem.Binding + log *subsystem.Log + runtime *subsystem.Runtime + event *subsystem.Event + //binding *subsystem.Binding call *subsystem.Call menu *subsystem.Menu dispatcher *messagedispatcher.Dispatcher @@ -109,8 +112,13 @@ func (a *App) Run() error { var err error + // Setup a context + var subsystemWaitGroup sync.WaitGroup + parentContext := context.WithValue(context.Background(), "waitgroup", &subsystemWaitGroup) + ctx, cancel := context.WithCancel(parentContext) + // Setup signal handler - signalsubsystem, err := signal.NewManager(a.servicebus, a.logger) + signalsubsystem, err := signal.NewManager(ctx, cancel, a.servicebus, a.logger, a.shutdownCallback) if err != nil { return err } @@ -124,7 +132,7 @@ func (a *App) Run() error { return err } - runtimesubsystem, err := subsystem.NewRuntime(a.servicebus, a.logger, a.startupCallback, a.shutdownCallback) + runtimesubsystem, err := subsystem.NewRuntime(ctx, a.servicebus, a.logger, a.startupCallback, a.shutdownCallback) if err != nil { return err } @@ -138,17 +146,6 @@ func (a *App) Run() error { a.loglevelStore = a.runtime.GoRuntime().Store.New("wails:loglevel", a.options.LogLevel) a.appconfigStore = a.runtime.GoRuntime().Store.New("wails:appconfig", a.options) - // Start the binding subsystem - bindingsubsystem, err := subsystem.NewBinding(a.servicebus, a.logger, a.bindings, a.runtime.GoRuntime()) - if err != nil { - return err - } - a.binding = bindingsubsystem - err = a.binding.Start() - if err != nil { - return err - } - // Start the logging subsystem log, err := subsystem.NewLog(a.servicebus, a.logger, a.loglevelStore) if err != nil { @@ -172,18 +169,18 @@ func (a *App) Run() error { } // Start the eventing subsystem - event, err := subsystem.NewEvent(a.servicebus, a.logger) + eventsubsystem, err := subsystem.NewEvent(ctx, a.servicebus, a.logger) if err != nil { return err } - a.event = event + a.event = eventsubsystem err = a.event.Start() if err != nil { return err } // Start the menu subsystem - menusubsystem, err := subsystem.NewMenu(a.servicebus, a.logger, a.menuManager) + menusubsystem, err := subsystem.NewMenu(ctx, a.servicebus, a.logger, a.menuManager) if err != nil { return err } @@ -194,11 +191,11 @@ func (a *App) Run() error { } // Start the call subsystem - call, err := subsystem.NewCall(a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime()) + callSubsystem, err := subsystem.NewCall(ctx, a.servicebus, a.logger, a.bindings.DB(), a.runtime.GoRuntime()) if err != nil { return err } - a.call = call + a.call = callSubsystem err = a.call.Start() if err != nil { return err @@ -210,12 +207,31 @@ func (a *App) Run() error { return err } - result := a.window.Run(dispatcher, bindingDump, a.debug) + err = a.window.Run(dispatcher, bindingDump, a.debug) a.logger.Trace("Ffenestri.Run() exited") + if err != nil { + return err + } + + // Close down all the subsystems + a.logger.Trace("Cancelling subsystems") + cancel() + subsystemWaitGroup.Wait() + + a.logger.Trace("Cancelling dispatcher") + dispatcher.Close() + + // Close log + a.logger.Trace("Stopping log") + log.Close() + + a.logger.Trace("Stopping Service bus") err = a.servicebus.Stop() if err != nil { return err } - return result + println("Desktop.Run() finished") + + return nil } diff --git a/v2/internal/ffenestri/ffenestri.go b/v2/internal/ffenestri/ffenestri.go index 28c0c4bd..aac5efb1 100644 --- a/v2/internal/ffenestri/ffenestri.go +++ b/v2/internal/ffenestri/ffenestri.go @@ -1,11 +1,12 @@ package ffenestri import ( - "github.com/wailsapp/wails/v2/internal/menumanager" "runtime" "strings" "unsafe" + "github.com/wailsapp/wails/v2/internal/menumanager" + "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/messagedispatcher" "github.com/wailsapp/wails/v2/pkg/options" @@ -118,7 +119,8 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug fullscreen := a.bool2Cint(a.config.Fullscreen) startHidden := a.bool2Cint(a.config.StartHidden) logLevel := C.int(a.config.LogLevel) - app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel) + hideWindowOnClose := a.bool2Cint(a.config.HideWindowOnClose) + app := C.NewApplication(title, width, height, resizable, devtools, fullscreen, startHidden, logLevel, hideWindowOnClose) // Save app reference a.app = (*C.struct_Application)(app) @@ -167,6 +169,7 @@ func (a *Application) Run(incomingDispatcher Dispatcher, bindings string, debug // Yes - Save memory reference and run app, cleaning up afterwards a.saveMemoryReference(unsafe.Pointer(app)) C.Run(app, 0, nil) + println("Back in ffenestri.go") } else { // Oh no! We couldn't initialise the application a.logger.Fatal("Cannot initialise Application.") diff --git a/v2/internal/ffenestri/ffenestri.h b/v2/internal/ffenestri/ffenestri.h index 5f630a7d..c9fbb36a 100644 --- a/v2/internal/ffenestri/ffenestri.h +++ b/v2/internal/ffenestri/ffenestri.h @@ -4,7 +4,7 @@ #include struct Application; -extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel); +extern struct Application *NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose); extern void SetMinWindowSize(struct Application*, int minWidth, int minHeight); extern void SetMaxWindowSize(struct Application*, int maxWidth, int maxHeight); extern void Run(struct Application*, int argc, char **argv); diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index 164d9e8d..368b13c2 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -34,6 +34,12 @@ BOOL yes(id self, SEL cmd) return YES; } +// no command simply returns NO! +BOOL no(id self, SEL cmd) +{ + return NO; +} + // Prints a hashmap entry int hashmap_log(void *const context, struct hashmap_element_s *const e) { printf("%s: %p ", (char*)e->key, e->data); @@ -65,6 +71,7 @@ struct Application { // Cocoa data id application; id delegate; + id windowDelegate; id mainWindow; id wkwebview; id manager; @@ -92,6 +99,7 @@ struct Application { const char *appearance; int decorations; int logLevel; + int hideWindowOnClose; // Features int frame; @@ -120,6 +128,9 @@ struct Application { // Bindings const char *bindings; + // shutting down flag + bool shuttingDown; + }; // Debug works like sprintf but mutes if the global debug flag is true @@ -222,6 +233,9 @@ void applyWindowColour(struct Application *app) { } void SetColour(struct Application *app, int red, int green, int blue, int alpha) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + app->red = red; app->green = green; app->blue = blue; @@ -235,12 +249,18 @@ void FullSizeContent(struct Application *app) { } void Hide(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( msg(app->application, s("hide:")) ); } void Show(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( msg(app->mainWindow, s("makeKeyAndOrderFront:"), NULL); msg(app->application, s("activateIgnoringOtherApps:"), YES); @@ -422,6 +442,7 @@ void freeDialogIconCache(struct Application *app) { } void DestroyApplication(struct Application *app) { + app->shuttingDown = true; Debug(app, "Destroying Application"); // Free the bindings @@ -466,10 +487,16 @@ void DestroyApplication(struct Application *app) { msg(app->manager, s("removeScriptMessageHandlerForName:"), str("error")); // Close main window - msg(app->mainWindow, s("close")); + if( app->windowDelegate != NULL ) { + printf("\n\n\n\nReleasing window delegate\n\n\n\n\n"); + msg(app->windowDelegate, s("release")); + printf("\n\n\n\n\nRemoving window delegate\n\n\n\n\n\n\n"); + msg(app->mainWindow, s("setDelegate:"), NULL); + } + +// msg(app->mainWindow, s("close")); + - // Terminate app - msg(c("NSApp"), s("terminate:"), NULL); Debug(app, "Finished Destroying Application"); } @@ -477,11 +504,32 @@ void DestroyApplication(struct Application *app) { // used by the application void Quit(struct Application *app) { Debug(app, "Quit Called"); - DestroyApplication(app); + ON_MAIN_THREAD ( + // Terminate app + msg(app->application, s("stop:"), NULL); + id fakeevent = msg(c("NSEvent"), + s("otherEventWithType:location:modifierFlags:timestamp:windowNumber:context:subtype:data1:data2:"), + 15, // Type + msg(c("CGPoint"), s("init:x:y:"), 0, 0), // location + 0, // flags + 0, // timestamp + 0, // window + NULL, // context + 0, // subtype + 0, // data1 + 0 // data2 + ); + msg(c("NSApp"), s("postEvent:atStart:"), fakeevent, true); +// msg(c(app->mainWindow), s("performClose:")) + + ); } // SetTitle sets the main window title to the given string void SetTitle(struct Application *app, const char *title) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "SetTitle Called"); ON_MAIN_THREAD( msg(app->mainWindow, s("setTitle:"), str(title)); @@ -503,6 +551,9 @@ bool isFullScreen(struct Application *app) { // Fullscreen sets the main window to be fullscreen void Fullscreen(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "Fullscreen Called"); if( ! isFullScreen(app) ) { ToggleFullscreen(app); @@ -511,6 +562,9 @@ void Fullscreen(struct Application *app) { // UnFullscreen resets the main window after a fullscreen void UnFullscreen(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "UnFullscreen Called"); if( isFullScreen(app) ) { ToggleFullscreen(app); @@ -518,6 +572,9 @@ void UnFullscreen(struct Application *app) { } void Center(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "Center Called"); ON_MAIN_THREAD( MAIN_WINDOW_CALL("center"); @@ -532,23 +589,35 @@ void ToggleMaximise(struct Application *app) { } void Maximise(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + if( app->maximised == 0) { ToggleMaximise(app); } } void Unmaximise(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + if( app->maximised == 1) { ToggleMaximise(app); } } void Minimise(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( MAIN_WINDOW_CALL("miniaturize:"); ); } void Unminimise(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( MAIN_WINDOW_CALL("deminiaturize:"); ); @@ -572,6 +641,9 @@ void dumpFrame(struct Application *app, const char *message, CGRect frame) { } void SetSize(struct Application *app, int width, int height) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( id screen = getCurrentScreen(app); @@ -588,6 +660,9 @@ void SetSize(struct Application *app, int width, int height) { } void SetPosition(struct Application *app, int x, int y) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( id screen = getCurrentScreen(app); CGRect screenFrame = GET_FRAME(screen); @@ -613,6 +688,9 @@ void processDialogButton(id alert, char *buttonTitle, char *cancelButton, char * } extern void MessageDialog(struct Application *app, char *callbackID, char *type, char *title, char *message, char *icon, char *button1, char *button2, char *button3, char *button4, char *defaultButton, char *cancelButton) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( id alert = ALLOC_INIT("NSAlert"); char *dialogType = type; @@ -726,6 +804,9 @@ extern void MessageDialog(struct Application *app, char *callbackID, char *type, // OpenDialog opens a dialog to select files/directories void OpenDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int allowFiles, int allowDirs, int allowMultiple, int showHiddenFiles, int canCreateDirectories, int resolvesAliases, int treatPackagesAsDirectories) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "OpenDialog Called with callback id: %s", callbackID); // Create an open panel @@ -814,6 +895,9 @@ void OpenDialog(struct Application *app, char *callbackID, char *title, char *fi // SaveDialog opens a dialog to select files/directories void SaveDialog(struct Application *app, char *callbackID, char *title, char *filters, char *defaultFilename, char *defaultDir, int showHiddenFiles, int canCreateDirectories, int treatPackagesAsDirectories) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + Debug(app, "SaveDialog Called with callback id: %s", callbackID); // Create an open panel @@ -891,6 +975,9 @@ void DisableFrame(struct Application *app) void setMinMaxSize(struct Application *app) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + if (app->maxHeight > 0 && app->maxWidth > 0) { msg(app->mainWindow, s("setMaxSize:"), CGSizeMake(app->maxWidth, app->maxHeight)); @@ -917,6 +1004,9 @@ void setMinMaxSize(struct Application *app) void SetMinWindowSize(struct Application *app, int minWidth, int minHeight) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + app->minWidth = minWidth; app->minHeight = minHeight; @@ -930,6 +1020,9 @@ void SetMinWindowSize(struct Application *app, int minWidth, int minHeight) void SetMaxWindowSize(struct Application *app, int maxWidth, int maxHeight) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + app->maxWidth = maxWidth; app->maxHeight = maxHeight; @@ -950,24 +1043,40 @@ void SetDebug(void *applicationPointer, int flag) { // AddContextMenu sets the context menu map for this application void AddContextMenu(struct Application *app, const char *contextMenuJSON) { - AddContextMenuToStore(app->contextMenuStore, contextMenuJSON); + // Guard against calling during shutdown + if( app->shuttingDown ) return; + + AddContextMenuToStore(app->contextMenuStore, contextMenuJSON); } void UpdateContextMenu(struct Application *app, const char* contextMenuJSON) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + UpdateContextMenuInStore(app->contextMenuStore, contextMenuJSON); } void AddTrayMenu(struct Application *app, const char *trayMenuJSON) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + AddTrayMenuToStore(app->trayMenuStore, trayMenuJSON); } void SetTrayMenu(struct Application *app, const char* trayMenuJSON) { + + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( UpdateTrayMenuInStore(app->trayMenuStore, trayMenuJSON); ); } void UpdateTrayMenuLabel(struct Application* app, const char* JSON) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( UpdateTrayMenuLabelInStore(app->trayMenuStore, JSON); ); @@ -1034,6 +1143,9 @@ void createApplication(struct Application *app) { } void DarkModeEnabled(struct Application *app, const char *callbackID) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + ON_MAIN_THREAD( const char *result = isDarkMode(app) ? "T" : "F"; @@ -1054,9 +1166,9 @@ void DarkModeEnabled(struct Application *app, const char *callbackID) { void createDelegate(struct Application *app) { // Define delegate Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "AppDelegate", 0); - bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate")); - class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) yes, "c@:@"); - class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); + bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSApplicationDelegate")); + class_addMethod(delegateClass, s("applicationShouldTerminateAfterLastWindowClosed:"), (IMP) no, "c@:@"); +// class_addMethod(delegateClass, s("applicationWillTerminate:"), (IMP) closeWindow, "v@:@"); class_addMethod(delegateClass, s("applicationWillFinishLaunching:"), (IMP) willFinishLaunching, "v@:@"); // All Menu Items use a common callback @@ -1082,6 +1194,11 @@ void createDelegate(struct Application *app) { msg(app->application, s("setDelegate:"), delegate); } +bool windowShouldClose(id self, SEL cmd, id sender) { + msg(sender, s("orderBack:")); + return false; + } + void createMainWindow(struct Application *app) { // Create main window id mainWindow = ALLOC("NSWindow"); @@ -1100,6 +1217,15 @@ void createMainWindow(struct Application *app) { msg(mainWindow, s("setTitlebarAppearsTransparent:"), app->titlebarAppearsTransparent ? YES : NO); msg(mainWindow, s("setTitleVisibility:"), app->hideTitle); + if( app->hideWindowOnClose ) { + // Create window delegate to override windowShouldClose + Class delegateClass = objc_allocateClassPair((Class) c("NSObject"), "WindowDelegate", 0); + bool resultAddProtoc = class_addProtocol(delegateClass, objc_getProtocol("NSWindowDelegate")); + class_replaceMethod(delegateClass, s("windowShouldClose:"), (IMP) windowShouldClose, "v@:@"); + app->windowDelegate = msg((id)delegateClass, s("new")); + msg(mainWindow, s("setDelegate:"), app->windowDelegate); + } + app->mainWindow = mainWindow; } @@ -1186,7 +1312,7 @@ void parseMenuRole(struct Application *app, id parentMenu, JsonNode *item) { return; } if ( STREQ(roleName, "quit")) { - addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE); + addMenuItem(parentMenu, "Quit", "terminate:", "q", FALSE); return; } if ( STREQ(roleName, "togglefullscreen")) { @@ -1464,6 +1590,9 @@ void updateMenu(struct Application *app, const char *menuAsJSON) { // SetApplicationMenu sets the initial menu for the application void SetApplicationMenu(struct Application *app, const char *menuAsJSON) { + // Guard against calling during shutdown + if( app->shuttingDown ) return; + if ( app->applicationMenu == NULL ) { app->applicationMenu = NewApplicationMenu(menuAsJSON); return; @@ -1705,11 +1834,13 @@ void Run(struct Application *app, int argc, char **argv) { Debug(app, "Run called"); msg(app->application, s("run")); + DestroyApplication(app); + MEMFREE(internalCode); } -void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel) { +void* NewApplication(const char *title, int width, int height, int resizable, int devtools, int fullscreen, int startHidden, int logLevel, int hideWindowOnClose) { // Load the tray icons LoadTrayIcons(); @@ -1730,6 +1861,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in result->startHidden = startHidden; result->decorations = 0; result->logLevel = logLevel; + result->hideWindowOnClose = hideWindowOnClose; result->mainWindow = NULL; result->mouseEvent = NULL; @@ -1758,12 +1890,17 @@ void* NewApplication(const char *title, int width, int height, int resizable, in // Context Menus result->contextMenuStore = NewContextMenuStore(); + // Window delegate + result->windowDelegate = NULL; + // Window Appearance result->titlebarAppearsTransparent = 0; result->webviewIsTranparent = 0; result->sendMessageToBackend = (ffenestriCallback) messageFromWindowCallback; + result->shuttingDown = false; + return (void*) result; } diff --git a/v2/internal/messagedispatcher/messagedispatcher.go b/v2/internal/messagedispatcher/messagedispatcher.go index e96b25c9..382b2f38 100644 --- a/v2/internal/messagedispatcher/messagedispatcher.go +++ b/v2/internal/messagedispatcher/messagedispatcher.go @@ -1,6 +1,7 @@ package messagedispatcher import ( + "context" "encoding/json" "strconv" "strings" @@ -24,7 +25,6 @@ type Dispatcher struct { dialogChannel <-chan *servicebus.Message systemChannel <-chan *servicebus.Message menuChannel <-chan *servicebus.Message - running bool servicebus *servicebus.ServiceBus logger logger.CustomLogger @@ -32,6 +32,13 @@ type Dispatcher struct { // Clients clients map[string]*DispatchClient lock sync.RWMutex + + // Context for cancellation + ctx context.Context + cancel context.CancelFunc + + // internal wait group + wg sync.WaitGroup } // New dispatcher. Needs a service bus to send to. @@ -76,6 +83,9 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, return nil, err } + // Create context + ctx, cancel := context.WithCancel(context.Background()) + result := &Dispatcher{ servicebus: servicebus, eventChannel: eventChannel, @@ -87,6 +97,8 @@ func New(servicebus *servicebus.ServiceBus, logger *logger.Logger) (*Dispatcher, dialogChannel: dialogChannel, systemChannel: systemChannel, menuChannel: menuChannel, + ctx: ctx, + cancel: cancel, } return result, nil @@ -97,15 +109,18 @@ func (d *Dispatcher) Start() error { d.logger.Trace("Starting") - d.running = true + d.wg.Add(1) // Spin off a go routine go func() { - for d.running { + defer d.logger.Trace("Shutdown") + for { select { + case <-d.ctx.Done(): + d.wg.Done() + return case <-d.quitChannel: d.processQuit() - d.running = false case resultMessage := <-d.resultChannel: d.processCallResult(resultMessage) case eventMessage := <-d.eventChannel: @@ -120,9 +135,6 @@ func (d *Dispatcher) Start() error { d.processMenuMessage(menuMessage) } } - - // Call shutdown - d.shutdown() }() return nil @@ -136,10 +148,6 @@ func (d *Dispatcher) processQuit() { } } -func (d *Dispatcher) shutdown() { - d.logger.Trace("Shutdown") -} - // RegisterClient will register the given callback with the dispatcher // and return a DispatchClient that the caller can use to send messages func (d *Dispatcher) RegisterClient(client Client) *DispatchClient { @@ -524,3 +532,8 @@ func (d *Dispatcher) processMenuMessage(result *servicebus.Message) { d.logger.Error("Unknown menufrontend command: %s", command) } } + +func (d *Dispatcher) Close() { + d.cancel() + d.wg.Wait() +} diff --git a/v2/internal/runtime/runtime.go b/v2/internal/runtime/runtime.go index 2a6416cd..9a4b46fa 100644 --- a/v2/internal/runtime/runtime.go +++ b/v2/internal/runtime/runtime.go @@ -6,19 +6,20 @@ import ( // Runtime is a means for the user to interact with the application at runtime type Runtime struct { - Browser Browser - Events Events - Window Window - Dialog Dialog - System System - Menu Menu - Store *StoreProvider - Log Log - bus *servicebus.ServiceBus + Browser Browser + Events Events + Window Window + Dialog Dialog + System System + Menu Menu + Store *StoreProvider + Log Log + bus *servicebus.ServiceBus + shutdownCallback func() } // New creates a new runtime -func New(serviceBus *servicebus.ServiceBus) *Runtime { +func New(serviceBus *servicebus.ServiceBus, shutdownCallback func()) *Runtime { result := &Runtime{ Browser: newBrowser(), Events: newEvents(serviceBus), @@ -35,5 +36,11 @@ func New(serviceBus *servicebus.ServiceBus) *Runtime { // Quit the application func (r *Runtime) Quit() { + // Call back to user's shutdown method if defined + if r.shutdownCallback != nil { + r.shutdownCallback() + } + + // Start shutdown of Wails r.bus.Publish("quit", "runtime.Quit()") } diff --git a/v2/internal/servicebus/servicebus.go b/v2/internal/servicebus/servicebus.go index d118d484..86faa9ae 100644 --- a/v2/internal/servicebus/servicebus.go +++ b/v2/internal/servicebus/servicebus.go @@ -1,6 +1,7 @@ package servicebus import ( + "context" "fmt" "strings" "sync" @@ -12,23 +13,26 @@ import ( type ServiceBus struct { listeners map[string][]chan *Message messageQueue chan *Message - quitChannel chan struct{} - wg sync.WaitGroup lock sync.RWMutex closed bool debug bool logger logger.CustomLogger + ctx context.Context + cancel context.CancelFunc } // New creates a new ServiceBus // The internal message queue is set to 100 messages // Listener queues are set to 10 func New(logger *logger.Logger) *ServiceBus { + + ctx, cancel := context.WithCancel(context.Background()) return &ServiceBus{ listeners: make(map[string][]chan *Message), messageQueue: make(chan *Message, 100), - quitChannel: make(chan struct{}, 1), logger: logger.CustomLogger("Service Bus"), + ctx: ctx, + cancel: cancel, } } @@ -63,24 +67,22 @@ func (s *ServiceBus) Debug() { // Start the service bus func (s *ServiceBus) Start() error { - s.logger.Trace("Starting") - // Prevent starting when closed if s.closed { return fmt.Errorf("cannot call start on closed servicebus") } - // We run in a different thread - s.wg.Add(1) + s.logger.Trace("Starting") go func() { - - quit := false + defer s.logger.Trace("Stopped") // Loop until we get a quit message - for !quit { + for { select { + case <-s.ctx.Done(): + return // Listen for messages case message := <-s.messageQueue: @@ -91,16 +93,9 @@ func (s *ServiceBus) Start() error { } // Dispatch message s.dispatchMessage(message) - - // Listen for quit messages - case <-s.quitChannel: - quit = true } } - // Indicate we have shut down - s.wg.Done() - }() return nil @@ -117,10 +112,7 @@ func (s *ServiceBus) Stop() error { s.closed = true // Send quit message - s.quitChannel <- struct{}{} - - // Wait for dispatcher to stop - s.wg.Wait() + s.cancel() // Close down subscriber channels s.lock.Lock() @@ -135,7 +127,6 @@ func (s *ServiceBus) Stop() error { // Close message queue close(s.messageQueue) - s.logger.Trace("Stopped") return nil } @@ -172,7 +163,6 @@ func (s *ServiceBus) Subscribe(topic string) (<-chan *Message, error) { func (s *ServiceBus) Publish(topic string, data interface{}) { // Prevent publish when closed if s.closed { - s.logger.Fatal("cannot call publish on closed servicebus") return } @@ -184,7 +174,6 @@ func (s *ServiceBus) Publish(topic string, data interface{}) { func (s *ServiceBus) PublishForTarget(topic string, data interface{}, target string) { // Prevent publish when closed if s.closed { - s.logger.Fatal("cannot call publish on closed servicebus") return } message := NewMessageForTarget(topic, data, target) diff --git a/v2/internal/signal/signal.go b/v2/internal/signal/signal.go index b394991c..aa13598a 100644 --- a/v2/internal/signal/signal.go +++ b/v2/internal/signal/signal.go @@ -1,8 +1,10 @@ package signal import ( + "context" "os" gosignal "os/signal" + "sync" "syscall" "github.com/wailsapp/wails/v2/internal/logger" @@ -20,24 +22,29 @@ type Manager struct { // signalChannel signalchannel chan os.Signal - // Quit channel - quitChannel <-chan *servicebus.Message + // ctx + ctx context.Context + cancel context.CancelFunc + + // The shutdown callback to notify the user's app that a shutdown + // has started + shutdownCallback func() + + // Parent waitgroup + wg *sync.WaitGroup } // NewManager creates a new signal manager -func NewManager(bus *servicebus.ServiceBus, logger *logger.Logger) (*Manager, error) { - - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } +func NewManager(ctx context.Context, cancel context.CancelFunc, bus *servicebus.ServiceBus, logger *logger.Logger, shutdownCallback func()) (*Manager, error) { result := &Manager{ - bus: bus, - logger: logger.CustomLogger("Event Manager"), - signalchannel: make(chan os.Signal, 2), - quitChannel: quitChannel, + bus: bus, + logger: logger.CustomLogger("Event Manager"), + signalchannel: make(chan os.Signal, 2), + ctx: ctx, + cancel: cancel, + shutdownCallback: shutdownCallback, + wg: ctx.Value("waitgroup").(*sync.WaitGroup), } return result, nil @@ -49,20 +56,28 @@ func (m *Manager) Start() { // Hook into interrupts gosignal.Notify(m.signalchannel, os.Interrupt, syscall.SIGTERM) - // Spin off signal listener + m.wg.Add(1) + + // Spin off signal listener and wait for either a cancellation + // or signal go func() { - running := true - for running { - select { - case <-m.signalchannel: - println() - m.logger.Trace("Ctrl+C detected. Shutting down...") - m.bus.Publish("quit", "ctrl-c pressed") - case <-m.quitChannel: - running = false - break + select { + case <-m.signalchannel: + println() + m.logger.Trace("Ctrl+C detected. Shutting down...") + m.bus.Publish("quit", "ctrl-c pressed") + + // Shutdown app first + if m.shutdownCallback != nil { + m.shutdownCallback() } + + // Start shutdown of Wails + m.cancel() + + case <-m.ctx.Done(): } m.logger.Trace("Shutdown") + m.wg.Done() }() } diff --git a/v2/internal/subsystem/binding.go b/v2/internal/subsystem/binding.go index 5e5ec343..a51c95b8 100644 --- a/v2/internal/subsystem/binding.go +++ b/v2/internal/subsystem/binding.go @@ -10,9 +10,9 @@ import ( // Binding is the Binding subsystem. It manages all service bus messages // starting with "binding". type Binding struct { - quitChannel <-chan *servicebus.Message bindingChannel <-chan *servicebus.Message - running bool + + running bool // Binding db bindings *binding.Bindings @@ -27,12 +27,6 @@ type Binding struct { // NewBinding creates a new binding subsystem. Uses the given bindings db for reference. func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings, runtime *runtime.Runtime) (*Binding, error) { - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } - // Subscribe to event messages bindingChannel, err := bus.Subscribe("binding") if err != nil { @@ -40,7 +34,6 @@ func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *bin } result := &Binding{ - quitChannel: quitChannel, bindingChannel: bindingChannel, logger: logger.CustomLogger("Binding Subsystem"), bindings: bindings, @@ -61,20 +54,16 @@ func (b *Binding) Start() error { go func() { for b.running { select { - case <-b.quitChannel: - b.running = false case bindingMessage := <-b.bindingChannel: b.logger.Trace("Got binding message: %+v", bindingMessage) } } - - // Call shutdown - b.shutdown() + b.logger.Trace("Shutdown") }() return nil } -func (b *Binding) shutdown() { - b.logger.Trace("Shutdown") +func (b *Binding) Close() { + b.running = false } diff --git a/v2/internal/subsystem/call.go b/v2/internal/subsystem/call.go index 60e82f9c..e60e1750 100644 --- a/v2/internal/subsystem/call.go +++ b/v2/internal/subsystem/call.go @@ -1,10 +1,13 @@ package subsystem import ( + "context" "encoding/json" "fmt" - "github.com/wailsapp/wails/v2/pkg/options/dialog" "strings" + "sync" + + "github.com/wailsapp/wails/v2/pkg/options/dialog" "github.com/wailsapp/wails/v2/internal/binding" "github.com/wailsapp/wails/v2/internal/logger" @@ -16,9 +19,10 @@ import ( // Call is the Call subsystem. It manages all service bus messages // starting with "call". type Call struct { - quitChannel <-chan *servicebus.Message callChannel <-chan *servicebus.Message - running bool + + // quit flag + shouldQuit bool // bindings DB DB *binding.DB @@ -31,16 +35,16 @@ type Call struct { // runtime runtime *runtime.Runtime + + // context + ctx context.Context + + // parent waitgroup + wg *sync.WaitGroup } // NewCall creates a new call subsystem -func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, runtime *runtime.Runtime) (*Call, error) { - - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } +func NewCall(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, runtime *runtime.Runtime) (*Call, error) { // Subscribe to event messages callChannel, err := bus.Subscribe("call:invoke") @@ -49,12 +53,13 @@ func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, } result := &Call{ - quitChannel: quitChannel, callChannel: callChannel, logger: logger.CustomLogger("Call Subsystem"), DB: DB, bus: bus, runtime: runtime, + ctx: ctx, + wg: ctx.Value("waitgroup").(*sync.WaitGroup), } return result, nil @@ -63,22 +68,21 @@ func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB, // Start the subsystem func (c *Call) Start() error { - c.running = true + c.wg.Add(1) // Spin off a go routine go func() { - for c.running { + defer c.logger.Trace("Shutdown") + for { select { - case <-c.quitChannel: - c.running = false + case <-c.ctx.Done(): + c.wg.Done() + return case callMessage := <-c.callChannel: - // TODO: Check if this works ok in a goroutine c.processCall(callMessage) } } - // Call shutdown - c.shutdown() }() return nil @@ -190,10 +194,6 @@ func (c *Call) sendError(err error, payload *message.CallMessage, clientID strin c.bus.PublishForTarget("call:result", string(messageData), clientID) } -func (c *Call) shutdown() { - c.logger.Trace("Shutdown") -} - // CallbackMessage defines a message that contains the result of a call type CallbackMessage struct { Result interface{} `json:"result"` diff --git a/v2/internal/subsystem/event.go b/v2/internal/subsystem/event.go index 8e3adf64..d1c31805 100644 --- a/v2/internal/subsystem/event.go +++ b/v2/internal/subsystem/event.go @@ -1,6 +1,7 @@ package subsystem import ( + "context" "strings" "sync" @@ -22,9 +23,7 @@ type eventListener struct { // Event is the Eventing subsystem. It manages all service bus messages // starting with "event". type Event struct { - quitChannel <-chan *servicebus.Message eventChannel <-chan *servicebus.Message - running bool // Event listeners listeners map[string][]*eventListener @@ -32,16 +31,16 @@ type Event struct { // logger logger logger.CustomLogger + + // ctx + ctx context.Context + + // parent waitgroup + wg *sync.WaitGroup } // NewEvent creates a new log subsystem -func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) { - - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } +func NewEvent(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) { // Subscribe to event messages eventChannel, err := bus.Subscribe("event") @@ -50,10 +49,11 @@ func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) } result := &Event{ - quitChannel: quitChannel, eventChannel: eventChannel, logger: logger.CustomLogger("Event Subsystem"), listeners: make(map[string][]*eventListener), + ctx: ctx, + wg: ctx.Value("waitgroup").(*sync.WaitGroup), } return result, nil @@ -80,15 +80,16 @@ func (e *Event) Start() error { e.logger.Trace("Starting") - e.running = true + e.wg.Add(1) // Spin off a go routine go func() { - for e.running { + defer e.logger.Trace("Shutdown") + for { select { - case <-e.quitChannel: - e.running = false - break + case <-e.ctx.Done(): + e.wg.Done() + return case eventMessage := <-e.eventChannel: splitTopic := strings.Split(eventMessage.Topic(), ":") eventType := splitTopic[1] @@ -128,8 +129,6 @@ func (e *Event) Start() error { } } - // Call shutdown - e.shutdown() }() return nil @@ -190,7 +189,3 @@ func (e *Event) notifyListeners(eventName string, message *message.EventMessage) // Unlock e.notifyLock.Unlock() } - -func (e *Event) shutdown() { - e.logger.Trace("Shutdown") -} diff --git a/v2/internal/subsystem/log.go b/v2/internal/subsystem/log.go index 64eb4893..023d2d12 100644 --- a/v2/internal/subsystem/log.go +++ b/v2/internal/subsystem/log.go @@ -1,8 +1,10 @@ package subsystem import ( + "context" "strconv" "strings" + "sync" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/runtime" @@ -12,15 +14,23 @@ import ( // Log is the Logging subsystem. It handles messages with topics starting // with "log:" type Log struct { - logChannel <-chan *servicebus.Message - quitChannel <-chan *servicebus.Message - running bool + logChannel <-chan *servicebus.Message + + // quit flag + shouldQuit bool // Logger! logger *logger.Logger // Loglevel store logLevelStore *runtime.Store + + // Context for shutdown + ctx context.Context + cancel context.CancelFunc + + // internal waitgroup + wg sync.WaitGroup } // NewLog creates a new log subsystem @@ -32,17 +42,14 @@ func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger, logLevelStore *ru return nil, err } - // Subscribe to quit messages - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } + ctx, cancel := context.WithCancel(context.Background()) result := &Log{ logChannel: logChannel, - quitChannel: quitChannel, logger: logger, logLevelStore: logLevelStore, + ctx: ctx, + cancel: cancel, } return result, nil @@ -51,15 +58,17 @@ func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger, logLevelStore *ru // Start the subsystem func (l *Log) Start() error { - l.running = true + l.wg.Add(1) // Spin off a go routine go func() { - for l.running { + defer l.logger.Trace("Logger Shutdown") + + for l.shouldQuit == false { select { - case <-l.quitChannel: - l.running = false - break + case <-l.ctx.Done(): + l.wg.Done() + return case logMessage := <-l.logChannel: logType := strings.TrimPrefix(logMessage.Topic(), "log:") switch logType { @@ -98,8 +107,12 @@ func (l *Log) Start() error { } } } - l.logger.Trace("Logger Shutdown") }() return nil } + +func (l *Log) Close() { + l.cancel() + l.wg.Wait() +} diff --git a/v2/internal/subsystem/menu.go b/v2/internal/subsystem/menu.go index 81ff1033..26d30562 100644 --- a/v2/internal/subsystem/menu.go +++ b/v2/internal/subsystem/menu.go @@ -1,9 +1,12 @@ package subsystem import ( + "context" "encoding/json" - "github.com/wailsapp/wails/v2/pkg/menu" "strings" + "sync" + + "github.com/wailsapp/wails/v2/pkg/menu" "github.com/wailsapp/wails/v2/internal/logger" "github.com/wailsapp/wails/v2/internal/menumanager" @@ -13,9 +16,10 @@ import ( // Menu is the subsystem that handles the operation of menus. It manages all service bus messages // starting with "menu". type Menu struct { - quitChannel <-chan *servicebus.Message menuChannel <-chan *servicebus.Message - running bool + + // shutdown flag + shouldQuit bool // logger logger logger.CustomLogger @@ -25,16 +29,16 @@ type Menu struct { // Menu Manager menuManager *menumanager.Manager + + // ctx + ctx context.Context + + // parent waitgroup + wg *sync.WaitGroup } // NewMenu creates a new menu subsystem -func NewMenu(bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *menumanager.Manager) (*Menu, error) { - - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } +func NewMenu(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *menumanager.Manager) (*Menu, error) { // Subscribe to menu messages menuChannel, err := bus.Subscribe("menu:") @@ -43,11 +47,12 @@ func NewMenu(bus *servicebus.ServiceBus, logger *logger.Logger, menuManager *men } result := &Menu{ - quitChannel: quitChannel, menuChannel: menuChannel, logger: logger.CustomLogger("Menu Subsystem"), bus: bus, menuManager: menuManager, + ctx: ctx, + wg: ctx.Value("waitgroup").(*sync.WaitGroup), } return result, nil @@ -58,15 +63,16 @@ func (m *Menu) Start() error { m.logger.Trace("Starting") - m.running = true + m.wg.Add(1) // Spin off a go routine go func() { - for m.running { + defer m.logger.Trace("Shutdown") + for { select { - case <-m.quitChannel: - m.running = false - break + case <-m.ctx.Done(): + m.wg.Done() + return case menuMessage := <-m.menuChannel: splitTopic := strings.Split(menuMessage.Topic(), ":") menuMessageType := splitTopic[1] @@ -147,14 +153,7 @@ func (m *Menu) Start() error { } } } - - // Call shutdown - m.shutdown() }() return nil } - -func (m *Menu) shutdown() { - m.logger.Trace("Shutdown") -} diff --git a/v2/internal/subsystem/runtime.go b/v2/internal/subsystem/runtime.go index 0e592e41..9d624b54 100644 --- a/v2/internal/subsystem/runtime.go +++ b/v2/internal/subsystem/runtime.go @@ -1,6 +1,7 @@ package subsystem import ( + "context" "fmt" "strings" @@ -12,7 +13,6 @@ import ( // Runtime is the Runtime subsystem. It handles messages with topics starting // with "runtime:" type Runtime struct { - quitChannel <-chan *servicebus.Message runtimeChannel <-chan *servicebus.Message // The hooks channel allows us to hook into frontend startup @@ -20,22 +20,20 @@ type Runtime struct { startupCallback func(*runtime.Runtime) shutdownCallback func() - running bool + // quit flag + shouldQuit bool logger logger.CustomLogger // Runtime library runtime *runtime.Runtime + + //ctx + ctx context.Context } // NewRuntime creates a new runtime subsystem -func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime), shutdownCallback func()) (*Runtime, error) { - - // Register quit channel - quitChannel, err := bus.Subscribe("quit") - if err != nil { - return nil, err - } +func NewRuntime(ctx context.Context, bus *servicebus.ServiceBus, logger *logger.Logger, startupCallback func(*runtime.Runtime), shutdownCallback func()) (*Runtime, error) { // Subscribe to log messages runtimeChannel, err := bus.Subscribe("runtime:") @@ -50,13 +48,13 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, startupCallba } result := &Runtime{ - quitChannel: quitChannel, runtimeChannel: runtimeChannel, hooksChannel: hooksChannel, logger: logger.CustomLogger("Runtime Subsystem"), - runtime: runtime.New(bus), + runtime: runtime.New(bus, shutdownCallback), startupCallback: startupCallback, shutdownCallback: shutdownCallback, + ctx: ctx, } return result, nil @@ -65,15 +63,11 @@ func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger, startupCallba // Start the subsystem func (r *Runtime) Start() error { - r.running = true - // Spin off a go routine go func() { - for r.running { + defer r.logger.Trace("Shutdown") + for { select { - case <-r.quitChannel: - r.running = false - break case hooksMessage := <-r.hooksChannel: r.logger.Trace(fmt.Sprintf("Received hooksmessage: %+v", hooksMessage)) messageSlice := strings.Split(hooksMessage.Topic(), ":") @@ -113,11 +107,10 @@ func (r *Runtime) Start() error { if err != nil { r.logger.Error(err.Error()) } + case <-r.ctx.Done(): + return } } - - // Call shutdown - r.shutdown() }() return nil @@ -128,15 +121,6 @@ func (r *Runtime) GoRuntime() *runtime.Runtime { return r.runtime } -func (r *Runtime) shutdown() { - if r.shutdownCallback != nil { - go r.shutdownCallback() - } else { - r.logger.Warning("no shutdown callback registered!") - } - r.logger.Trace("Shutdown") -} - func (r *Runtime) processBrowserMessage(method string, data interface{}) error { switch method { case "open": diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 1deefcd7..d921d253 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -14,27 +14,28 @@ import ( // App contains options for creating the App type App struct { - Title string - Width int - Height int - DisableResize bool - Fullscreen bool - MinWidth int - MinHeight int - MaxWidth int - MaxHeight int - StartHidden bool - DevTools bool - RGBA int - ContextMenus []*menu.ContextMenu - TrayMenus []*menu.TrayMenu - Menu *menu.Menu - Mac *mac.Options - Logger logger.Logger `json:"-"` - LogLevel logger.LogLevel - Startup func(*wailsruntime.Runtime) `json:"-"` - Shutdown func() `json:"-"` - Bind []interface{} + Title string + Width int + Height int + DisableResize bool + Fullscreen bool + MinWidth int + MinHeight int + MaxWidth int + MaxHeight int + StartHidden bool + HideWindowOnClose bool + DevTools bool + RGBA int + ContextMenus []*menu.ContextMenu + TrayMenus []*menu.TrayMenu + Menu *menu.Menu + Mac *mac.Options + Logger logger.Logger `json:"-"` + LogLevel logger.LogLevel + Startup func(*wailsruntime.Runtime) `json:"-"` + Shutdown func() `json:"-"` + Bind []interface{} } // MergeDefaults will set the minimum default values for an application