mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-16 23:55:52 +01:00
I have successfully implemented the "Start At Login" feature for Wails v3, addressing all the major concerns raised in PR #3910. Here's what was accomplished: ### ✅ **Core Implementation** 1. **Added StartAtLogin option** to `application.Options` struct 2. **Implemented platform-specific methods** for all three platforms: - **macOS**: Uses AppleScript with proper escaping to prevent injection attacks - **Windows**: Uses Windows Registry with restrictive permissions - **Linux**: Uses XDG autostart specification with .desktop files 3. **Added public API methods**: - `SetStartAtLogin(enabled bool) error` - Enable/disable start at login - `StartsAtLogin() (bool, error)` - Check current status ### ✅ **Security Improvements** (addressing PR comments) 1. **Path validation and sanitization** across all platforms 2. **AppleScript injection protection** on macOS with proper escaping 3. **Registry permissions** restricted to necessary access on Windows 4. **Executable path validation** with symlink resolution 5. **Input sanitization** for application names and paths ### ✅ **Error Handling & Documentation** 1. **Comprehensive error handling** with descriptive error messages 2. **Complete API documentation** with platform-specific behavior notes 3. **macOS Info.plist requirement** documented (NSAppleEventsUsageDescription) 4. **Cross-platform compatibility** notes and troubleshooting ### ✅ **Example & Testing** 1. **Working example application** demonstrating usage 2. **Comprehensive README** with platform-specific requirements 3. **Runtime toggling capability** implemented 4. **Compilation verified** - the implementation builds successfully ### 🔧 **Technical Details** - **macOS**: Uses Bundle information and AppleScript with security hardening - **Windows**: Uses HKEY_CURRENT_USER registry with KEY_SET_VALUE/KEY_QUERY_VALUE permissions - **Linux**: Creates XDG-compliant .desktop files in ~/.config/autostart/ ### 📋 **Key Features** - ✅ Cross-platform support (macOS, Windows, Linux) - ✅ Runtime configuration via public API - ✅ Application startup configuration via Options - ✅ Security hardening against injection attacks - ✅ Proper error handling and validation - ✅ Complete documentation and examples The implementation is now ready for testing and can be integrated into Wails v3. All major security concerns from the original PR have been addressed, and the feature includes proper documentation for developers.
894 lines
22 KiB
Go
894 lines
22 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/assetserver/bundledassets"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/signal"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/assetserver"
|
|
"github.com/wailsapp/wails/v3/internal/assetserver/webview"
|
|
"github.com/wailsapp/wails/v3/internal/capabilities"
|
|
)
|
|
|
|
//go:embed assets/*
|
|
var alphaAssets embed.FS
|
|
|
|
var globalApplication *App
|
|
|
|
// AlphaAssets is the default assets for the alpha application
|
|
var AlphaAssets = AssetOptions{
|
|
Handler: BundledAssetFileServer(alphaAssets),
|
|
}
|
|
|
|
func init() {
|
|
runtime.LockOSThread()
|
|
}
|
|
|
|
type EventListener struct {
|
|
callback func(app *ApplicationEvent)
|
|
}
|
|
|
|
func Get() *App {
|
|
return globalApplication
|
|
}
|
|
|
|
func New(appOptions Options) *App {
|
|
if globalApplication != nil {
|
|
return globalApplication
|
|
}
|
|
|
|
mergeApplicationDefaults(&appOptions)
|
|
|
|
result := newApplication(appOptions)
|
|
globalApplication = result
|
|
fatalHandler(result.handleFatalError)
|
|
|
|
if result.Logger == nil {
|
|
if result.isDebugMode {
|
|
result.Logger = DefaultLogger(result.options.LogLevel)
|
|
} else {
|
|
result.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
}
|
|
|
|
if !appOptions.DisableDefaultSignalHandler {
|
|
result.signalHandler = signal.NewSignalHandler(result.Quit)
|
|
result.signalHandler.Logger = result.Logger
|
|
result.signalHandler.ExitMessage = func(sig os.Signal) string {
|
|
return "Quitting application..."
|
|
}
|
|
}
|
|
|
|
result.logStartup()
|
|
result.logPlatformInfo()
|
|
|
|
result.customEventProcessor = NewWailsEventProcessor(result.Event.dispatch)
|
|
|
|
messageProc := NewMessageProcessor(result.Logger)
|
|
opts := &assetserver.Options{
|
|
Handler: appOptions.Assets.Handler,
|
|
Middleware: assetserver.ChainMiddleware(
|
|
func(next http.Handler) http.Handler {
|
|
if m := appOptions.Assets.Middleware; m != nil {
|
|
return m(next)
|
|
}
|
|
return next
|
|
},
|
|
func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
path := req.URL.Path
|
|
switch path {
|
|
case "/wails/runtime.js":
|
|
err := assetserver.ServeFile(rw, path, bundledassets.RuntimeJS)
|
|
if err != nil {
|
|
result.fatal("unable to serve runtime.js: %w", err)
|
|
}
|
|
case "/wails/runtime":
|
|
messageProc.ServeHTTP(rw, req)
|
|
case "/wails/capabilities":
|
|
err := assetserver.ServeFile(rw, path, globalApplication.capabilities.AsBytes())
|
|
if err != nil {
|
|
result.fatal("unable to serve capabilities: %w", err)
|
|
}
|
|
case "/wails/flags":
|
|
updatedOptions := result.impl.GetFlags(appOptions)
|
|
flags, err := json.Marshal(updatedOptions)
|
|
if err != nil {
|
|
result.fatal("invalid flags provided to application: %w", err)
|
|
}
|
|
err = assetserver.ServeFile(rw, path, flags)
|
|
if err != nil {
|
|
result.fatal("unable to serve flags: %w", err)
|
|
}
|
|
default:
|
|
next.ServeHTTP(rw, req)
|
|
}
|
|
})
|
|
},
|
|
),
|
|
Logger: result.Logger,
|
|
}
|
|
|
|
if appOptions.Assets.DisableLogging {
|
|
opts.Logger = slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
|
|
srv, err := assetserver.NewAssetServer(opts)
|
|
if err != nil {
|
|
result.fatal("application initialisation failed: %w", err)
|
|
}
|
|
|
|
result.assets = srv
|
|
result.assets.LogDetails()
|
|
|
|
result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
|
|
result.options.Services = slices.Clone(appOptions.Services)
|
|
|
|
// Process keybindings
|
|
if result.options.KeyBindings != nil {
|
|
result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
|
|
}
|
|
|
|
if appOptions.OnShutdown != nil {
|
|
result.OnShutdown(appOptions.OnShutdown)
|
|
}
|
|
|
|
// Initialize single instance manager if enabled
|
|
if appOptions.SingleInstance != nil {
|
|
manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
|
|
if err != nil {
|
|
if errors.Is(err, alreadyRunningError) && manager != nil {
|
|
err = manager.notifyFirstInstance()
|
|
if err != nil {
|
|
globalApplication.error("failed to notify first instance: %w", err)
|
|
}
|
|
os.Exit(appOptions.SingleInstance.ExitCode)
|
|
}
|
|
result.fatal("failed to initialize single instance manager: %w", err)
|
|
} else {
|
|
result.singleInstanceManager = manager
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func mergeApplicationDefaults(o *Options) {
|
|
if o.Name == "" {
|
|
o.Name = "My Wails Application"
|
|
}
|
|
if o.Description == "" {
|
|
o.Description = "An application written using Wails"
|
|
}
|
|
if o.Windows.WndClass == "" {
|
|
o.Windows.WndClass = "WailsWebviewWindow"
|
|
}
|
|
}
|
|
|
|
type (
|
|
platformApp interface {
|
|
run() error
|
|
destroy()
|
|
setApplicationMenu(menu *Menu)
|
|
name() string
|
|
getCurrentWindowID() uint
|
|
showAboutDialog(name string, description string, icon []byte)
|
|
setIcon(icon []byte)
|
|
on(id uint)
|
|
dispatchOnMainThread(id uint)
|
|
hide()
|
|
show()
|
|
getPrimaryScreen() (*Screen, error)
|
|
getScreens() ([]*Screen, error)
|
|
GetFlags(options Options) map[string]any
|
|
isOnMainThread() bool
|
|
isDarkMode() bool
|
|
getAccentColor() string
|
|
setStartAtLogin(enabled bool) error
|
|
startsAtLogin() (bool, error)
|
|
}
|
|
|
|
runnable interface {
|
|
Run()
|
|
}
|
|
)
|
|
|
|
// Messages sent from javascript get routed here
|
|
type windowMessage struct {
|
|
windowId uint
|
|
message string
|
|
}
|
|
|
|
var windowMessageBuffer = make(chan *windowMessage, 5)
|
|
|
|
type dragAndDropMessage struct {
|
|
windowId uint
|
|
filenames []string
|
|
}
|
|
|
|
var windowDragAndDropBuffer = make(chan *dragAndDropMessage, 5)
|
|
|
|
func addDragAndDropMessage(windowId uint, filenames []string) {
|
|
windowDragAndDropBuffer <- &dragAndDropMessage{
|
|
windowId: windowId,
|
|
filenames: filenames,
|
|
}
|
|
}
|
|
|
|
var _ webview.Request = &webViewAssetRequest{}
|
|
|
|
const webViewRequestHeaderWindowId = "x-wails-window-id"
|
|
const webViewRequestHeaderWindowName = "x-wails-window-name"
|
|
|
|
type webViewAssetRequest struct {
|
|
webview.Request
|
|
windowId uint
|
|
windowName string
|
|
}
|
|
|
|
var windowKeyEvents = make(chan *windowKeyEvent, 5)
|
|
|
|
type windowKeyEvent struct {
|
|
windowId uint
|
|
acceleratorString string
|
|
}
|
|
|
|
func (r *webViewAssetRequest) Header() (http.Header, error) {
|
|
h, err := r.Request.Header()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hh := h.Clone()
|
|
hh.Set(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(r.windowId), 10))
|
|
return hh, nil
|
|
}
|
|
|
|
var webviewRequests = make(chan *webViewAssetRequest, 5)
|
|
|
|
type eventHook struct {
|
|
callback func(event *ApplicationEvent)
|
|
}
|
|
|
|
type App struct {
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
options Options
|
|
applicationEventListeners map[uint][]*EventListener
|
|
applicationEventListenersLock sync.RWMutex
|
|
applicationEventHooks map[uint][]*eventHook
|
|
applicationEventHooksLock sync.RWMutex
|
|
|
|
// Manager pattern for organized API
|
|
Window *WindowManager
|
|
ContextMenu *ContextMenuManager
|
|
KeyBinding *KeyBindingManager
|
|
Browser *BrowserManager
|
|
Env *EnvironmentManager
|
|
Dialog *DialogManager
|
|
Event *EventManager
|
|
Menu *MenuManager
|
|
Screen *ScreenManager
|
|
Clipboard *ClipboardManager
|
|
SystemTray *SystemTrayManager
|
|
|
|
// Windows
|
|
windows map[uint]Window
|
|
windowsLock sync.RWMutex
|
|
|
|
// System Trays
|
|
systemTrays map[uint]*SystemTray
|
|
systemTraysLock sync.Mutex
|
|
systemTrayID uint
|
|
systemTrayIDLock sync.RWMutex
|
|
|
|
// MenuItems
|
|
menuItems map[uint]*MenuItem
|
|
menuItemsLock sync.Mutex
|
|
|
|
// Starting and running
|
|
starting bool
|
|
running bool
|
|
runLock sync.Mutex
|
|
pendingRun []runnable
|
|
|
|
bindings *Bindings
|
|
|
|
// platform app
|
|
impl platformApp
|
|
|
|
// The main application menu (private - use app.Menu.GetApplicationMenu/SetApplicationMenu)
|
|
applicationMenu *Menu
|
|
|
|
clipboard *Clipboard
|
|
customEventProcessor *EventProcessor
|
|
Logger *slog.Logger
|
|
|
|
contextMenus map[string]*ContextMenu
|
|
contextMenusLock sync.RWMutex
|
|
|
|
assets *assetserver.AssetServer
|
|
startURL string
|
|
|
|
// Hooks
|
|
windowCreatedCallbacks []func(window Window)
|
|
pid int
|
|
|
|
// Capabilities
|
|
capabilities capabilities.Capabilities
|
|
isDebugMode bool
|
|
|
|
// Keybindings
|
|
keyBindings map[string]func(window *WebviewWindow)
|
|
keyBindingsLock sync.RWMutex
|
|
|
|
// Shutdown
|
|
performingShutdown bool
|
|
shutdownLock sync.Mutex
|
|
serviceShutdownLock sync.Mutex
|
|
|
|
// Shutdown tasks are run when the application is shutting down.
|
|
// They are run in the order they are added and run on the main thread.
|
|
// The application option `OnShutdown` is run first.
|
|
shutdownTasks []func()
|
|
|
|
// signalHandler is used to handle signals
|
|
signalHandler *signal.SignalHandler
|
|
|
|
// Wails ApplicationEvent Listener related
|
|
wailsEventListenerLock sync.Mutex
|
|
wailsEventListeners []WailsEventListener
|
|
|
|
// singleInstanceManager handles single instance functionality
|
|
singleInstanceManager *singleInstanceManager
|
|
}
|
|
|
|
func (a *App) Config() Options {
|
|
return a.options
|
|
}
|
|
|
|
// Context returns the application context that is canceled when the application shuts down.
|
|
// This context should be used for graceful shutdown of goroutines and long-running operations.
|
|
func (a *App) Context() context.Context {
|
|
return a.ctx
|
|
}
|
|
|
|
func (a *App) handleWarning(msg string) {
|
|
if a.options.WarningHandler != nil {
|
|
a.options.WarningHandler(msg)
|
|
} else {
|
|
a.Logger.Warn(msg)
|
|
}
|
|
}
|
|
|
|
func (a *App) handleError(err error) {
|
|
if a.options.ErrorHandler != nil {
|
|
a.options.ErrorHandler(err)
|
|
} else {
|
|
a.Logger.Error(err.Error())
|
|
}
|
|
}
|
|
|
|
// RegisterService appends the given service to the list of bound services.
|
|
// Registered services will be bound and initialised
|
|
// in registration order upon calling [App.Run].
|
|
//
|
|
// RegisterService will log an error message
|
|
// and discard the given service
|
|
// if called after [App.Run].
|
|
func (a *App) RegisterService(service Service) {
|
|
a.runLock.Lock()
|
|
defer a.runLock.Unlock()
|
|
|
|
if a.starting || a.running {
|
|
a.error("services must be registered before running the application. Service '%s' will not be registered.", getServiceName(service))
|
|
return
|
|
}
|
|
|
|
a.options.Services = append(a.options.Services, service)
|
|
}
|
|
|
|
// EmitEvent will emit an event
|
|
|
|
func (a *App) handleFatalError(err error) {
|
|
a.handleError(&FatalError{err: err})
|
|
os.Exit(1)
|
|
}
|
|
|
|
func (a *App) init() {
|
|
a.ctx, a.cancel = context.WithCancel(context.Background())
|
|
a.applicationEventHooks = make(map[uint][]*eventHook)
|
|
a.applicationEventListeners = make(map[uint][]*EventListener)
|
|
a.windows = make(map[uint]Window)
|
|
a.systemTrays = make(map[uint]*SystemTray)
|
|
a.contextMenus = make(map[string]*ContextMenu)
|
|
a.keyBindings = make(map[string]func(window *WebviewWindow))
|
|
a.Logger = a.options.Logger
|
|
a.pid = os.Getpid()
|
|
a.wailsEventListeners = make([]WailsEventListener, 0)
|
|
|
|
// Initialize managers
|
|
a.Window = newWindowManager(a)
|
|
a.ContextMenu = newContextMenuManager(a)
|
|
a.KeyBinding = newKeyBindingManager(a)
|
|
a.Browser = newBrowserManager(a)
|
|
a.Env = newEnvironmentManager(a)
|
|
a.Dialog = newDialogManager(a)
|
|
a.Event = newEventManager(a)
|
|
a.Menu = newMenuManager(a)
|
|
a.Screen = newScreenManager(a)
|
|
a.Clipboard = newClipboardManager(a)
|
|
a.SystemTray = newSystemTrayManager(a)
|
|
}
|
|
|
|
func (a *App) Capabilities() capabilities.Capabilities {
|
|
return a.capabilities
|
|
}
|
|
|
|
//func (a *App) RegisterListener(listener WailsEventListener) {
|
|
// a.wailsEventListenerLock.Lock()
|
|
// a.wailsEventListeners = append(a.wailsEventListeners, listener)
|
|
// a.wailsEventListenerLock.Unlock()
|
|
//}
|
|
//
|
|
//func (a *App) RegisterServiceHandler(prefix string, handler http.Handler) {
|
|
// a.assets.AttachServiceHandler(prefix, handler)
|
|
//}
|
|
|
|
func (a *App) GetPID() int {
|
|
return a.pid
|
|
}
|
|
|
|
func (a *App) info(message string, args ...any) {
|
|
if a.Logger != nil {
|
|
go func() {
|
|
defer handlePanic()
|
|
a.Logger.Info(message, args...)
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (a *App) debug(message string, args ...any) {
|
|
if a.Logger != nil {
|
|
go func() {
|
|
defer handlePanic()
|
|
a.Logger.Debug(message, args...)
|
|
}()
|
|
}
|
|
}
|
|
|
|
func (a *App) fatal(message string, args ...any) {
|
|
err := fmt.Errorf(message, args...)
|
|
a.handleFatalError(err)
|
|
}
|
|
func (a *App) warning(message string, args ...any) {
|
|
msg := fmt.Sprintf(message, args...)
|
|
a.handleWarning(msg)
|
|
}
|
|
|
|
func (a *App) error(message string, args ...any) {
|
|
a.handleError(fmt.Errorf(message, args...))
|
|
}
|
|
|
|
func (a *App) Run() error {
|
|
a.runLock.Lock()
|
|
// Prevent double invocations.
|
|
if a.starting || a.running {
|
|
a.runLock.Unlock()
|
|
return errors.New("application is running or a previous run has failed")
|
|
}
|
|
// Block further service registrations.
|
|
a.starting = true
|
|
a.runLock.Unlock()
|
|
|
|
// Ensure application context is canceled in case of failures.
|
|
defer a.cancel()
|
|
|
|
// Call post-create hooks
|
|
err := a.preRun()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
a.impl = newPlatformApp(a)
|
|
|
|
// Ensure services are shut down in case of failures.
|
|
defer a.shutdownServices()
|
|
|
|
// Ensure application context is canceled before service shutdown (duplicate calls don't hurt).
|
|
defer a.cancel()
|
|
|
|
// Startup services before dispatching any events.
|
|
// No need to hold the lock here because a.options.Services may only change when a.running is false.
|
|
services := a.options.Services
|
|
a.options.Services = nil
|
|
for i, service := range services {
|
|
if err := a.startupService(service); err != nil {
|
|
return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err)
|
|
}
|
|
// Schedule started services for shutdown.
|
|
a.options.Services = services[:i+1]
|
|
}
|
|
|
|
go func() {
|
|
for {
|
|
event := <-applicationEvents
|
|
go a.Event.handleApplicationEvent(event)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
event := <-windowEvents
|
|
go a.handleWindowEvent(event)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
request := <-webviewRequests
|
|
go a.handleWebViewRequest(request)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
event := <-windowMessageBuffer
|
|
go a.handleWindowMessage(event)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
event := <-windowKeyEvents
|
|
go a.handleWindowKeyEvent(event)
|
|
}
|
|
}()
|
|
go func() {
|
|
for {
|
|
dragAndDropMessage := <-windowDragAndDropBuffer
|
|
go a.handleDragAndDropMessage(dragAndDropMessage)
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
menuItemID := <-menuItemClicked
|
|
go a.Menu.handleMenuItemClicked(menuItemID)
|
|
}
|
|
}()
|
|
|
|
a.runLock.Lock()
|
|
a.running = true
|
|
a.runLock.Unlock()
|
|
|
|
// No need to hold the lock here because
|
|
// - a.pendingRun may only change while a.running is false.
|
|
// - runnables are scheduled asynchronously anyway.
|
|
for _, pending := range a.pendingRun {
|
|
go func() {
|
|
defer handlePanic()
|
|
pending.Run()
|
|
}()
|
|
}
|
|
a.pendingRun = nil
|
|
|
|
// set the application menu
|
|
if runtime.GOOS == "darwin" {
|
|
a.impl.setApplicationMenu(a.applicationMenu)
|
|
}
|
|
if a.options.Icon != nil {
|
|
a.impl.setIcon(a.options.Icon)
|
|
}
|
|
|
|
// Configure start at login if requested
|
|
if a.options.StartAtLogin {
|
|
if err := a.impl.setStartAtLogin(true); err != nil {
|
|
a.warning("failed to enable start at login: %v", err)
|
|
}
|
|
}
|
|
|
|
return a.impl.run()
|
|
}
|
|
|
|
func (a *App) startupService(service Service) error {
|
|
err := a.bindings.Add(service)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot bind service methods: %w", err)
|
|
}
|
|
|
|
if service.options.Route != "" {
|
|
handler, ok := service.Instance().(http.Handler)
|
|
if !ok {
|
|
handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
http.Error(
|
|
rw,
|
|
fmt.Sprintf("Service '%s' does not handle HTTP requests", getServiceName(service)),
|
|
http.StatusServiceUnavailable,
|
|
)
|
|
})
|
|
}
|
|
a.assets.AttachServiceHandler(service.options.Route, handler)
|
|
}
|
|
|
|
if s, ok := service.instance.(ServiceStartup); ok {
|
|
a.debug("Starting up service:", "name", getServiceName(service))
|
|
return s.ServiceStartup(a.ctx, service.options)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *App) shutdownServices() {
|
|
// Acquire lock to prevent double calls (defer in Run() + OnShutdown)
|
|
a.serviceShutdownLock.Lock()
|
|
defer a.serviceShutdownLock.Unlock()
|
|
|
|
// Ensure app context is canceled first (duplicate calls don't hurt).
|
|
a.cancel()
|
|
|
|
for len(a.options.Services) > 0 {
|
|
last := len(a.options.Services) - 1
|
|
service := a.options.Services[last]
|
|
a.options.Services = a.options.Services[:last] // Prevent double shutdowns
|
|
|
|
if s, ok := service.instance.(ServiceShutdown); ok {
|
|
a.debug("Shutting down service:", "name", getServiceName(service))
|
|
if err := s.ServiceShutdown(); err != nil {
|
|
a.error("error shutting down service '%s': %w", getServiceName(service), err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
|
|
defer handlePanic()
|
|
// Get window from window map
|
|
a.windowsLock.Lock()
|
|
window, ok := a.windows[event.windowId]
|
|
a.windowsLock.Unlock()
|
|
if !ok {
|
|
a.warning("WebviewWindow #%d not found", event.windowId)
|
|
return
|
|
}
|
|
// Get callback from window
|
|
window.HandleDragAndDropMessage(event.filenames)
|
|
}
|
|
|
|
func (a *App) handleWindowMessage(event *windowMessage) {
|
|
defer handlePanic()
|
|
// Get window from window map
|
|
a.windowsLock.RLock()
|
|
window, ok := a.windows[event.windowId]
|
|
a.windowsLock.RUnlock()
|
|
if !ok {
|
|
a.warning("WebviewWindow #%d not found", event.windowId)
|
|
return
|
|
}
|
|
// Check if the message starts with "wails:"
|
|
if strings.HasPrefix(event.message, "wails:") {
|
|
window.HandleMessage(event.message)
|
|
} else {
|
|
if a.options.RawMessageHandler != nil {
|
|
a.options.RawMessageHandler(window, event.message)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (a *App) handleWebViewRequest(request *webViewAssetRequest) {
|
|
defer handlePanic()
|
|
a.assets.ServeWebViewRequest(request)
|
|
}
|
|
|
|
func (a *App) handleWindowEvent(event *windowEvent) {
|
|
defer handlePanic()
|
|
// Get window from window map
|
|
a.windowsLock.RLock()
|
|
window, ok := a.windows[event.WindowID]
|
|
a.windowsLock.RUnlock()
|
|
if !ok {
|
|
a.warning("Window #%d not found", event.WindowID)
|
|
return
|
|
}
|
|
window.HandleWindowEvent(event.EventID)
|
|
}
|
|
|
|
// OnShutdown adds a function to be run when the application is shutting down.
|
|
func (a *App) OnShutdown(f func()) {
|
|
if f == nil {
|
|
return
|
|
}
|
|
|
|
a.shutdownLock.Lock()
|
|
|
|
if !a.performingShutdown {
|
|
defer a.shutdownLock.Unlock()
|
|
a.shutdownTasks = append(a.shutdownTasks, f)
|
|
return
|
|
}
|
|
|
|
a.shutdownLock.Unlock()
|
|
InvokeAsync(f)
|
|
}
|
|
|
|
func (a *App) cleanup() {
|
|
a.shutdownLock.Lock()
|
|
if a.performingShutdown {
|
|
a.shutdownLock.Unlock()
|
|
return
|
|
}
|
|
a.cancel() // Cancel app context before running shutdown hooks.
|
|
a.performingShutdown = true
|
|
a.shutdownLock.Unlock()
|
|
|
|
// No need to hold the lock here because a.shutdownTasks
|
|
// may only change while a.performingShutdown is false.
|
|
for _, shutdownTask := range a.shutdownTasks {
|
|
InvokeSync(shutdownTask)
|
|
}
|
|
InvokeSync(func() {
|
|
a.shutdownServices()
|
|
a.windowsLock.RLock()
|
|
for _, window := range a.windows {
|
|
window.Close()
|
|
}
|
|
a.windows = nil
|
|
a.windowsLock.RUnlock()
|
|
a.systemTraysLock.Lock()
|
|
for _, systray := range a.systemTrays {
|
|
systray.destroy()
|
|
}
|
|
a.systemTrays = nil
|
|
a.systemTraysLock.Unlock()
|
|
|
|
// Cleanup single instance manager
|
|
if a.singleInstanceManager != nil {
|
|
a.singleInstanceManager.cleanup()
|
|
}
|
|
|
|
a.postQuit()
|
|
|
|
if a.options.PostShutdown != nil {
|
|
a.options.PostShutdown()
|
|
}
|
|
})
|
|
}
|
|
|
|
func (a *App) Quit() {
|
|
if a.impl != nil {
|
|
InvokeSync(a.impl.destroy)
|
|
}
|
|
}
|
|
|
|
func (a *App) SetIcon(icon []byte) {
|
|
if a.impl != nil {
|
|
a.impl.setIcon(icon)
|
|
}
|
|
}
|
|
|
|
func InfoDialog() *MessageDialog {
|
|
return newMessageDialog(InfoDialogType)
|
|
}
|
|
|
|
func QuestionDialog() *MessageDialog {
|
|
return newMessageDialog(QuestionDialogType)
|
|
}
|
|
|
|
func WarningDialog() *MessageDialog {
|
|
return newMessageDialog(WarningDialogType)
|
|
}
|
|
|
|
func ErrorDialog() *MessageDialog {
|
|
return newMessageDialog(ErrorDialogType)
|
|
}
|
|
|
|
func OpenFileDialog() *OpenFileDialogStruct {
|
|
return newOpenFileDialog()
|
|
}
|
|
|
|
func SaveFileDialog() *SaveFileDialogStruct {
|
|
return newSaveFileDialog()
|
|
}
|
|
|
|
func (a *App) dispatchOnMainThread(fn func()) {
|
|
// If we are on the main thread, just call the function
|
|
if a.impl.isOnMainThread() {
|
|
fn()
|
|
return
|
|
}
|
|
|
|
mainThreadFunctionStoreLock.Lock()
|
|
id := generateFunctionStoreID()
|
|
mainThreadFunctionStore[id] = fn
|
|
mainThreadFunctionStoreLock.Unlock()
|
|
// Call platform specific dispatch function
|
|
a.impl.dispatchOnMainThread(id)
|
|
}
|
|
|
|
func (a *App) Hide() {
|
|
if a.impl != nil {
|
|
a.impl.hide()
|
|
}
|
|
}
|
|
|
|
func (a *App) Show() {
|
|
if a.impl != nil {
|
|
a.impl.show()
|
|
}
|
|
}
|
|
|
|
func (a *App) runOrDeferToAppRun(r runnable) {
|
|
a.runLock.Lock()
|
|
|
|
if !a.running {
|
|
defer a.runLock.Unlock() // Defer unlocking for panic tolerance.
|
|
a.pendingRun = append(a.pendingRun, r)
|
|
return
|
|
}
|
|
|
|
// Unlock immediately to prevent deadlocks.
|
|
// No TOC/TOU risk here because a.running can never switch back to false.
|
|
a.runLock.Unlock()
|
|
r.Run()
|
|
}
|
|
|
|
func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
|
|
defer handlePanic()
|
|
// Get window from window map
|
|
a.windowsLock.RLock()
|
|
window, ok := a.windows[event.windowId]
|
|
a.windowsLock.RUnlock()
|
|
if !ok {
|
|
a.warning("WebviewWindow #%d not found", event.windowId)
|
|
return
|
|
}
|
|
// Get callback from window
|
|
window.HandleKeyEvent(event.acceleratorString)
|
|
}
|
|
|
|
func (a *App) shouldQuit() bool {
|
|
if a.options.ShouldQuit != nil {
|
|
return a.options.ShouldQuit()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// SetStartAtLogin enables or disables the application to start at login.
|
|
// This allows users to configure the application to launch automatically when they log in.
|
|
//
|
|
// Platform-specific behavior:
|
|
// - macOS: Uses AppleScript to manage login items (requires System Events access)
|
|
// - Windows: Uses registry entries under HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
|
|
// - Linux: Creates/removes .desktop files in ~/.config/autostart/
|
|
//
|
|
// Returns an error if the operation fails due to permissions, invalid paths, or platform limitations.
|
|
func (a *App) SetStartAtLogin(enabled bool) error {
|
|
if a.impl == nil {
|
|
return errors.New("application not initialized")
|
|
}
|
|
return a.impl.setStartAtLogin(enabled)
|
|
}
|
|
|
|
// StartsAtLogin returns whether the application is configured to start at login.
|
|
// This checks the current configuration without modifying it.
|
|
//
|
|
// Returns:
|
|
// - bool: true if the application starts at login, false otherwise
|
|
// - error: if the check fails due to permissions or platform limitations
|
|
func (a *App) StartsAtLogin() (bool, error) {
|
|
if a.impl == nil {
|
|
return false, errors.New("application not initialized")
|
|
}
|
|
return a.impl.startsAtLogin()
|
|
}
|