mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-03 06:39:30 +08:00
Implement Single instance lock feature with passing arguments to initial instance (#2951)
* implement MacOS openFile/openFiles events * wip: windows file association * fix macro import * add file icon copy * try copy icon * keep only required part of scripts * update config schema * fix json * set fileAssociation for mac via config * proper iconName handling * add fileAssociation icon generator * fix file association icons bundle * don't break compatibility * remove mimeType as not supported linux for now * add documentation * adjust config schema * restore formatting * try implement single instance lock with params passing * fix focusing * fix focusing * formatting * use channel buffer for second instance events * handle errors * add comment * remove unused option in file association * wip: linux single instance lock * wip: linux single instance * some experiments with making window active * try to use unminimise * remove unused * try present for window * try present for window * fix build * cleanup * cleanup * implement single instance lock on mac os * implement proper show for windows * proper unmimimise * get rid of openFiles mac os. change configuration structure * remove unused channel * remove unused function * add documentation for single instance lock * add PR link * changes after review * update docs * changes after review --------- Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
parent
a59f8b2cf3
commit
c24bd5e3e8
@ -12,6 +12,7 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.4.9
|
github.com/fsnotify/fsnotify v1.4.9
|
||||||
github.com/go-git/go-git/v5 v5.3.0
|
github.com/go-git/go-git/v5 v5.3.0
|
||||||
github.com/go-ole/go-ole v1.2.6
|
github.com/go-ole/go-ole v1.2.6
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0
|
||||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
|
||||||
github.com/google/uuid v1.3.0
|
github.com/google/uuid v1.3.0
|
||||||
github.com/jackmordaunt/icns v1.0.0
|
github.com/jackmordaunt/icns v1.0.0
|
||||||
|
@ -71,6 +71,8 @@ github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4
|
|||||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
@property bool alwaysOnTop;
|
@property bool alwaysOnTop;
|
||||||
@property bool startHidden;
|
@property bool startHidden;
|
||||||
|
@property (retain) NSString* singleInstanceUniqueId;
|
||||||
|
@property bool singleInstanceLockEnabled;
|
||||||
@property bool startFullscreen;
|
@property bool startFullscreen;
|
||||||
@property (retain) WailsWindow* mainWindow;
|
@property (retain) WailsWindow* mainWindow;
|
||||||
|
|
||||||
@ -22,4 +24,8 @@
|
|||||||
|
|
||||||
extern void HandleOpenFile(char *);
|
extern void HandleOpenFile(char *);
|
||||||
|
|
||||||
|
extern void HandleSecondInstanceData(char * message);
|
||||||
|
|
||||||
|
void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text);
|
||||||
|
|
||||||
#endif /* AppDelegate_h */
|
#endif /* AppDelegate_h */
|
||||||
|
@ -40,6 +40,28 @@
|
|||||||
[self.mainWindow setCollectionBehavior:behaviour];
|
[self.mainWindow setCollectionBehavior:behaviour];
|
||||||
[self.mainWindow toggleFullScreen:nil];
|
[self.mainWindow toggleFullScreen:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( self.singleInstanceLockEnabled ) {
|
||||||
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:self
|
||||||
|
selector:@selector(handleSecondInstanceNotification:) name:self.singleInstanceUniqueId object:nil];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SendDataToFirstInstance(char * singleInstanceUniqueId, char * message) {
|
||||||
|
[[NSDistributedNotificationCenter defaultCenter]
|
||||||
|
postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId]
|
||||||
|
object:nil
|
||||||
|
userInfo:@{@"message": [NSString stringWithUTF8String:message]}
|
||||||
|
deliverImmediately:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)handleSecondInstanceNotification:(NSNotification *)note;
|
||||||
|
{
|
||||||
|
if (note.userInfo[@"message"] != nil) {
|
||||||
|
NSString *message = note.userInfo[@"message"];
|
||||||
|
const char* utf8Message = message.UTF8String;
|
||||||
|
HandleSecondInstanceData((char*)utf8Message);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)dealloc {
|
- (void)dealloc {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
#define WindowStartsMinimised 2
|
#define WindowStartsMinimised 2
|
||||||
#define WindowStartsFullscreen 3
|
#define WindowStartsFullscreen 3
|
||||||
|
|
||||||
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences);
|
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceEnabled, const char* singleInstanceUniqueId);
|
||||||
void Run(void*, const char* url);
|
void Run(void*, const char* url);
|
||||||
|
|
||||||
void SetTitle(void* ctx, const char *title);
|
void SetTitle(void* ctx, const char *title);
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
#import "WailsMenu.h"
|
#import "WailsMenu.h"
|
||||||
#import "WailsMenuItem.h"
|
#import "WailsMenuItem.h"
|
||||||
|
|
||||||
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences) {
|
WailsContext* Create(const char* title, int width, int height, int frameless, int resizable, int fullscreen, int fullSizeContent, int hideTitleBar, int titlebarAppearsTransparent, int hideTitle, int useToolbar, int hideToolbarSeparator, int webviewIsTransparent, int alwaysOnTop, int hideWindowOnClose, const char *appearance, int windowIsTranslucent, int devtoolsEnabled, int defaultContextMenuEnabled, int windowStartState, int startsHidden, int minWidth, int minHeight, int maxWidth, int maxHeight, bool fraudulentWebsiteWarningEnabled, struct Preferences preferences, int singleInstanceLockEnabled, const char* singleInstanceUniqueId) {
|
||||||
|
|
||||||
[NSApplication sharedApplication];
|
[NSApplication sharedApplication];
|
||||||
|
|
||||||
@ -48,6 +48,11 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
|
|||||||
result.startFullscreen = true;
|
result.startFullscreen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( singleInstanceLockEnabled == 1 ) {
|
||||||
|
result.singleInstanceLockEnabled = true;
|
||||||
|
result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId);
|
||||||
|
}
|
||||||
|
|
||||||
result.alwaysOnTop = alwaysOnTop;
|
result.alwaysOnTop = alwaysOnTop;
|
||||||
result.hideOnClose = hideWindowOnClose;
|
result.hideOnClose = hideWindowOnClose;
|
||||||
|
|
||||||
@ -367,6 +372,8 @@ void Run(void *inctx, const char* url) {
|
|||||||
delegate.mainWindow = ctx.mainWindow;
|
delegate.mainWindow = ctx.mainWindow;
|
||||||
delegate.alwaysOnTop = ctx.alwaysOnTop;
|
delegate.alwaysOnTop = ctx.alwaysOnTop;
|
||||||
delegate.startHidden = ctx.startHidden;
|
delegate.startHidden = ctx.startHidden;
|
||||||
|
delegate.singleInstanceLockEnabled = ctx.singleInstanceLockEnabled;
|
||||||
|
delegate.singleInstanceUniqueId = ctx.singleInstanceUniqueId;
|
||||||
delegate.startFullscreen = ctx.startFullscreen;
|
delegate.startFullscreen = ctx.startFullscreen;
|
||||||
|
|
||||||
NSString *_url = safeInit(url);
|
NSString *_url = safeInit(url);
|
||||||
|
@ -40,6 +40,9 @@
|
|||||||
@property bool startHidden;
|
@property bool startHidden;
|
||||||
@property bool startFullscreen;
|
@property bool startFullscreen;
|
||||||
|
|
||||||
|
@property bool singleInstanceLockEnabled;
|
||||||
|
@property (retain) NSString* singleInstanceUniqueId;
|
||||||
|
|
||||||
@property (retain) NSEvent* mouseEvent;
|
@property (retain) NSEvent* mouseEvent;
|
||||||
|
|
||||||
@property bool alwaysOnTop;
|
@property bool alwaysOnTop;
|
||||||
|
@ -39,6 +39,7 @@ var messageBuffer = make(chan string, 100)
|
|||||||
var requestBuffer = make(chan webview.Request, 100)
|
var requestBuffer = make(chan webview.Request, 100)
|
||||||
var callbackBuffer = make(chan uint, 10)
|
var callbackBuffer = make(chan uint, 10)
|
||||||
var openFilepathBuffer = make(chan string, 100)
|
var openFilepathBuffer = make(chan string, 100)
|
||||||
|
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||||
|
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
|
|
||||||
@ -109,6 +110,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||||||
go result.startMessageProcessor()
|
go result.startMessageProcessor()
|
||||||
go result.startCallbackProcessor()
|
go result.startCallbackProcessor()
|
||||||
go result.startFileOpenProcessor()
|
go result.startFileOpenProcessor()
|
||||||
|
go result.startSecondInstanceProcessor()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -119,6 +121,15 @@ func (f *Frontend) startFileOpenProcessor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Frontend) startSecondInstanceProcessor() {
|
||||||
|
for secondInstanceData := range secondInstanceBuffer {
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil &&
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Frontend) startMessageProcessor() {
|
func (f *Frontend) startMessageProcessor() {
|
||||||
for message := range messageBuffer {
|
for message := range messageBuffer {
|
||||||
f.processMessage(message)
|
f.processMessage(message)
|
||||||
@ -162,6 +173,10 @@ func (f *Frontend) WindowSetDarkTheme() {
|
|||||||
func (f *Frontend) Run(ctx context.Context) error {
|
func (f *Frontend) Run(ctx context.Context) error {
|
||||||
f.ctx = ctx
|
f.ctx = ctx
|
||||||
|
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil {
|
||||||
|
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
|
||||||
|
}
|
||||||
|
|
||||||
var _debug = ctx.Value("debug")
|
var _debug = ctx.Value("debug")
|
||||||
var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
|
var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
|
||||||
|
|
||||||
|
75
v2/internal/frontend/desktop/darwin/single_instance.go
Normal file
75
v2/internal/frontend/desktop/darwin/single_instance.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
//go:build darwin
|
||||||
|
// +build darwin
|
||||||
|
|
||||||
|
package darwin
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework Foundation -framework Cocoa
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetupSingleInstance(uniqueID string) {
|
||||||
|
lockFilePath := os.TempDir()
|
||||||
|
lockFileName := uniqueID + ".lock"
|
||||||
|
_, err := createLockFile(lockFilePath + "/" + lockFileName)
|
||||||
|
|
||||||
|
// if lockFile exist – send notification to second instance
|
||||||
|
if err != nil {
|
||||||
|
c := NewCalloc()
|
||||||
|
defer c.Free()
|
||||||
|
singleInstanceUniqueId := c.String(uniqueID)
|
||||||
|
|
||||||
|
data, err := options.NewSecondInstanceData()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
C.SendDataToFirstInstance(singleInstanceUniqueId, c.String(string(serialized)))
|
||||||
|
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//export HandleSecondInstanceData
|
||||||
|
func HandleSecondInstanceData(secondInstanceMessage *C.char) {
|
||||||
|
message := C.GoString(secondInstanceMessage)
|
||||||
|
|
||||||
|
var secondInstanceData options.SecondInstanceData
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(message), &secondInstanceData)
|
||||||
|
if err == nil {
|
||||||
|
secondInstanceBuffer <- secondInstanceData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateLockFile tries to create a file with given name and acquire an
|
||||||
|
// exclusive lock on it. If the file already exists AND is still locked, it will
|
||||||
|
// fail.
|
||||||
|
func createLockFile(filename string) (*os.File, error) {
|
||||||
|
file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0600)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
@ -58,6 +58,7 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window
|
|||||||
startsHidden := bool2Cint(frontendOptions.StartHidden)
|
startsHidden := bool2Cint(frontendOptions.StartHidden)
|
||||||
devtoolsEnabled := bool2Cint(devtools)
|
devtoolsEnabled := bool2Cint(devtools)
|
||||||
defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu)
|
defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu)
|
||||||
|
singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil)
|
||||||
|
|
||||||
var fullSizeContent, hideTitleBar, hideTitle, useToolbar, webviewIsTransparent C.int
|
var fullSizeContent, hideTitleBar, hideTitle, useToolbar, webviewIsTransparent C.int
|
||||||
var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent C.int
|
var titlebarAppearsTransparent, hideToolbarSeparator, windowIsTranslucent C.int
|
||||||
@ -74,6 +75,12 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window
|
|||||||
|
|
||||||
title = c.String(frontendOptions.Title)
|
title = c.String(frontendOptions.Title)
|
||||||
|
|
||||||
|
singleInstanceUniqueIdStr := ""
|
||||||
|
if frontendOptions.SingleInstanceLock != nil {
|
||||||
|
singleInstanceUniqueIdStr = frontendOptions.SingleInstanceLock.UniqueId
|
||||||
|
}
|
||||||
|
singleInstanceUniqueId := c.String(singleInstanceUniqueIdStr)
|
||||||
|
|
||||||
enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection)
|
enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection)
|
||||||
|
|
||||||
if frontendOptions.Mac != nil {
|
if frontendOptions.Mac != nil {
|
||||||
@ -109,7 +116,9 @@ func NewWindow(frontendOptions *options.App, debug bool, devtools bool) *Window
|
|||||||
var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, fullscreen, fullSizeContent,
|
var context *C.WailsContext = C.Create(title, width, height, frameless, resizable, fullscreen, fullSizeContent,
|
||||||
hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent,
|
hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent,
|
||||||
alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled,
|
alwaysOnTop, hideWindowOnClose, appearance, windowIsTranslucent, devtoolsEnabled, defaultContextMenuEnabled,
|
||||||
windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings, preferences)
|
windowStartState, startsHidden, minWidth, minHeight, maxWidth, maxHeight, enableFraudulentWebsiteWarnings,
|
||||||
|
preferences, singleInstanceEnabled, singleInstanceUniqueId,
|
||||||
|
)
|
||||||
|
|
||||||
// Create menu
|
// Create menu
|
||||||
result := &Window{
|
result := &Window{
|
||||||
|
@ -102,6 +102,8 @@ var initOnce = sync.Once{}
|
|||||||
|
|
||||||
const startURL = "wails://wails/"
|
const startURL = "wails://wails/"
|
||||||
|
|
||||||
|
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||||
|
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
|
|
||||||
// Context
|
// Context
|
||||||
@ -201,6 +203,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||||||
C.free(unsafe.Pointer(prgname))
|
C.free(unsafe.Pointer(prgname))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go result.startSecondInstanceProcessor()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,6 +239,10 @@ func (f *Frontend) Run(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil {
|
||||||
|
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
|
||||||
|
}
|
||||||
|
|
||||||
f.mainWindow.Run(f.startURL.String())
|
f.mainWindow.Run(f.startURL.String())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@ -505,3 +513,12 @@ func (f *Frontend) startRequestProcessor() {
|
|||||||
func processURLRequest(request unsafe.Pointer) {
|
func processURLRequest(request unsafe.Pointer) {
|
||||||
requestBuffer <- webview.NewRequest(request)
|
requestBuffer <- webview.NewRequest(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Frontend) startSecondInstanceProcessor() {
|
||||||
|
for secondInstanceData := range secondInstanceBuffer {
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil &&
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
69
v2/internal/frontend/desktop/linux/single_instance.go
Normal file
69
v2/internal/frontend/desktop/linux/single_instance.go
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package linux
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbusHandler func(string)
|
||||||
|
|
||||||
|
func (f dbusHandler) SendMessage(message string) *dbus.Error {
|
||||||
|
f(message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupSingleInstance(uniqueID string) {
|
||||||
|
id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
|
||||||
|
|
||||||
|
dbusName := "org." + id + ".SingleInstance"
|
||||||
|
dbusPath := "/org/" + id + "/SingleInstance"
|
||||||
|
|
||||||
|
conn, err := dbus.ConnectSessionBus()
|
||||||
|
// if we will reach any error during establishing connection or sending message we will just continue.
|
||||||
|
// It should not be the case that such thing will happen actually, but just in case.
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
f := dbusHandler(func(message string) {
|
||||||
|
var secondInstanceData options.SecondInstanceData
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(message), &secondInstanceData)
|
||||||
|
if err == nil {
|
||||||
|
secondInstanceBuffer <- secondInstanceData
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
err = conn.Export(f, dbus.ObjectPath(dbusPath), dbusName)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply, err := conn.RequestName(dbusName, dbus.NameFlagDoNotQueue)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// if name already taken, try to send args to existing instance, if no success just launch new instance
|
||||||
|
if reply == dbus.RequestNameReplyExists {
|
||||||
|
data := options.SecondInstanceData{
|
||||||
|
Args: os.Args[1:],
|
||||||
|
}
|
||||||
|
serialized, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.Object(dbusName, dbus.ObjectPath(dbusPath)).Call(dbusName+".SendMessage", 0, string(serialized)).Store()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
@ -35,6 +35,8 @@ import (
|
|||||||
|
|
||||||
const startURL = "http://wails.localhost/"
|
const startURL = "http://wails.localhost/"
|
||||||
|
|
||||||
|
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||||
|
|
||||||
type Screen = frontend.Screen
|
type Screen = frontend.Screen
|
||||||
|
|
||||||
type Frontend struct {
|
type Frontend struct {
|
||||||
@ -113,6 +115,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
|||||||
}
|
}
|
||||||
result.assets = assets
|
result.assets = assets
|
||||||
|
|
||||||
|
go result.startSecondInstanceProcessor()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,6 +141,10 @@ func (f *Frontend) Run(ctx context.Context) error {
|
|||||||
|
|
||||||
f.chromium = edge.NewChromium()
|
f.chromium = edge.NewChromium()
|
||||||
|
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil {
|
||||||
|
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
|
||||||
|
}
|
||||||
|
|
||||||
mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium)
|
mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium)
|
||||||
f.mainWindow = mainWindow
|
f.mainWindow = mainWindow
|
||||||
|
|
||||||
@ -826,3 +834,12 @@ func (f *Frontend) ShowWindow() {
|
|||||||
func (f *Frontend) onFocus(arg *winc.Event) {
|
func (f *Frontend) onFocus(arg *winc.Event) {
|
||||||
f.chromium.Focus()
|
f.chromium.Focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Frontend) startSecondInstanceProcessor() {
|
||||||
|
for secondInstanceData := range secondInstanceBuffer {
|
||||||
|
if f.frontendOptions.SingleInstanceLock != nil &&
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch != nil {
|
||||||
|
f.frontendOptions.SingleInstanceLock.OnSecondInstanceLaunch(secondInstanceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
126
v2/internal/frontend/desktop/windows/single_instance.go
Normal file
126
v2/internal/frontend/desktop/windows/single_instance.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package windows
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type COPYDATASTRUCT struct {
|
||||||
|
dwData uintptr
|
||||||
|
cbData uint32
|
||||||
|
lpData uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// WMCOPYDATA_SINGLE_INSTANCE_DATA we define our own type for WM_COPYDATA message
|
||||||
|
const WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
|
||||||
|
|
||||||
|
func SendMessage(hwnd w32.HWND, data string) {
|
||||||
|
arrUtf16, _ := syscall.UTF16FromString(data)
|
||||||
|
|
||||||
|
pCopyData := new(COPYDATASTRUCT)
|
||||||
|
pCopyData.dwData = WMCOPYDATA_SINGLE_INSTANCE_DATA
|
||||||
|
pCopyData.cbData = uint32(len(arrUtf16)*2 + 1)
|
||||||
|
pCopyData.lpData = uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(data)))
|
||||||
|
|
||||||
|
w32.SendMessage(hwnd, w32.WM_COPYDATA, 0, uintptr(unsafe.Pointer(pCopyData)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSingleInstance single instance Windows app
|
||||||
|
func SetupSingleInstance(uniqueId string) {
|
||||||
|
id := "wails-app-" + uniqueId
|
||||||
|
|
||||||
|
className := id + "-sic"
|
||||||
|
windowName := id + "-siw"
|
||||||
|
mutexName := id + "sim"
|
||||||
|
|
||||||
|
_, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err == windows.ERROR_ALREADY_EXISTS {
|
||||||
|
// app is already running
|
||||||
|
hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(className), windows.StringToUTF16Ptr(windowName))
|
||||||
|
|
||||||
|
if hwnd != 0 {
|
||||||
|
data := options.SecondInstanceData{
|
||||||
|
Args: os.Args[1:],
|
||||||
|
}
|
||||||
|
serialized, _ := json.Marshal(data)
|
||||||
|
|
||||||
|
SendMessage(hwnd, string(serialized))
|
||||||
|
// exit second instance of app after sending message
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
// if we got any other unknown error we will just start new application instance
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
createEventTargetWindow(className, windowName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEventTargetWindow(className string, windowName string) w32.HWND {
|
||||||
|
// callback handler in the event target window
|
||||||
|
wndProc := func(
|
||||||
|
hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM,
|
||||||
|
) w32.LRESULT {
|
||||||
|
if msg == w32.WM_COPYDATA {
|
||||||
|
ldata := (*COPYDATASTRUCT)(unsafe.Pointer(lparam))
|
||||||
|
|
||||||
|
if ldata.dwData == WMCOPYDATA_SINGLE_INSTANCE_DATA {
|
||||||
|
serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.lpData)))
|
||||||
|
|
||||||
|
var secondInstanceData options.SecondInstanceData
|
||||||
|
|
||||||
|
err := json.Unmarshal([]byte(serialized), &secondInstanceData)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
secondInstanceBuffer <- secondInstanceData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return w32.LRESULT(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w32.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||||
|
}
|
||||||
|
|
||||||
|
var class w32.WNDCLASSEX
|
||||||
|
class.Size = uint32(unsafe.Sizeof(class))
|
||||||
|
class.Style = 0
|
||||||
|
class.WndProc = syscall.NewCallback(wndProc)
|
||||||
|
class.ClsExtra = 0
|
||||||
|
class.WndExtra = 0
|
||||||
|
class.Instance = w32.GetModuleHandle("")
|
||||||
|
class.Icon = 0
|
||||||
|
class.Cursor = 0
|
||||||
|
class.Background = 0
|
||||||
|
class.MenuName = nil
|
||||||
|
class.ClassName = windows.StringToUTF16Ptr(className)
|
||||||
|
class.IconSm = 0
|
||||||
|
|
||||||
|
w32.RegisterClassEx(&class)
|
||||||
|
|
||||||
|
// create event window that will not be visible for user
|
||||||
|
hwnd := w32.CreateWindowEx(
|
||||||
|
0,
|
||||||
|
windows.StringToUTF16Ptr(className),
|
||||||
|
windows.StringToUTF16Ptr(windowName),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
w32.HWND_MESSAGE,
|
||||||
|
0,
|
||||||
|
w32.GetModuleHandle(""),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return hwnd
|
||||||
|
}
|
@ -334,7 +334,23 @@ func (cba *ControlBase) ClientHeight() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (cba *ControlBase) Show() {
|
func (cba *ControlBase) Show() {
|
||||||
w32.ShowWindow(cba.hwnd, w32.SW_SHOWDEFAULT)
|
// WindowPos is used with HWND_TOPMOST to guarantee bring our app on top
|
||||||
|
// force set our main window on top
|
||||||
|
w32.SetWindowPos(
|
||||||
|
cba.hwnd,
|
||||||
|
w32.HWND_TOPMOST,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE,
|
||||||
|
)
|
||||||
|
// remove topmost to allow normal windows manipulations
|
||||||
|
w32.SetWindowPos(
|
||||||
|
cba.hwnd,
|
||||||
|
w32.HWND_NOTOPMOST,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
w32.SWP_SHOWWINDOW|w32.SWP_NOSIZE|w32.SWP_NOMOVE,
|
||||||
|
)
|
||||||
|
// put main window on tops foreground
|
||||||
|
w32.SetForegroundWindow(cba.hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cba *ControlBase) Hide() {
|
func (cba *ControlBase) Hide() {
|
||||||
|
@ -136,6 +136,15 @@ func (fm *Form) Minimise() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (fm *Form) Restore() {
|
func (fm *Form) Restore() {
|
||||||
|
// SC_RESTORE param for WM_SYSCOMMAND to restore app if it is minimized
|
||||||
|
const SC_RESTORE = 0xF120
|
||||||
|
// restore the minimized window, if it is
|
||||||
|
w32.SendMessage(
|
||||||
|
fm.hwnd,
|
||||||
|
w32.WM_SYSCOMMAND,
|
||||||
|
SC_RESTORE,
|
||||||
|
0,
|
||||||
|
)
|
||||||
w32.ShowWindow(fm.hwnd, w32.SW_RESTORE)
|
w32.ShowWindow(fm.hwnd, w32.SW_RESTORE)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ var (
|
|||||||
procShowWindowAsync = moduser32.NewProc("ShowWindowAsync")
|
procShowWindowAsync = moduser32.NewProc("ShowWindowAsync")
|
||||||
procUpdateWindow = moduser32.NewProc("UpdateWindow")
|
procUpdateWindow = moduser32.NewProc("UpdateWindow")
|
||||||
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
|
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
|
||||||
|
procFindWindowW = moduser32.NewProc("FindWindowW")
|
||||||
procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect")
|
procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect")
|
||||||
procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx")
|
procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx")
|
||||||
procDestroyWindow = moduser32.NewProc("DestroyWindow")
|
procDestroyWindow = moduser32.NewProc("DestroyWindow")
|
||||||
@ -263,6 +264,14 @@ func CreateWindowEx(exStyle uint, className, windowName *uint16,
|
|||||||
return HWND(ret)
|
return HWND(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindWindowW(className, windowName *uint16) HWND {
|
||||||
|
ret, _, _ := procFindWindowW.Call(
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(windowName)))
|
||||||
|
|
||||||
|
return HWND(ret)
|
||||||
|
}
|
||||||
|
|
||||||
func AdjustWindowRectEx(rect *RECT, style uint, menu bool, exStyle uint) bool {
|
func AdjustWindowRectEx(rect *RECT, style uint, menu bool, exStyle uint) bool {
|
||||||
ret, _, _ := procAdjustWindowRectEx.Call(
|
ret, _, _ := procAdjustWindowRectEx.Call(
|
||||||
uintptr(unsafe.Pointer(rect)),
|
uintptr(unsafe.Pointer(rect)),
|
||||||
|
@ -5,6 +5,8 @@ import (
|
|||||||
"html"
|
"html"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||||
@ -82,6 +84,8 @@ type App struct {
|
|||||||
// services of Apple and Microsoft.
|
// services of Apple and Microsoft.
|
||||||
EnableFraudulentWebsiteDetection bool
|
EnableFraudulentWebsiteDetection bool
|
||||||
|
|
||||||
|
SingleInstanceLock *SingleInstanceLock
|
||||||
|
|
||||||
Windows *windows.Options
|
Windows *windows.Options
|
||||||
Mac *mac.Options
|
Mac *mac.Options
|
||||||
Linux *linux.Options
|
Linux *linux.Options
|
||||||
@ -165,6 +169,30 @@ func MergeDefaults(appoptions *App) {
|
|||||||
processDragOptions(appoptions)
|
processDragOptions(appoptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SingleInstanceLock struct {
|
||||||
|
// uniqueId that will be used for setting up messaging between instances
|
||||||
|
UniqueId string
|
||||||
|
OnSecondInstanceLaunch func(secondInstanceData SecondInstanceData)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SecondInstanceData struct {
|
||||||
|
Args []string
|
||||||
|
WorkingDirectory string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSecondInstanceData() (*SecondInstanceData, error) {
|
||||||
|
ex, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
workingDirectory := filepath.Dir(ex)
|
||||||
|
|
||||||
|
return &SecondInstanceData{
|
||||||
|
Args: os.Args[1:],
|
||||||
|
WorkingDirectory: workingDirectory,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func processMenus(appoptions *App) {
|
func processMenus(appoptions *App) {
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
|
@ -85,6 +85,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched
|
||||||
|
and arguments are passed to already running instance. Check single instance lock guide for details. Example:
|
||||||
|
```go title="main.go"
|
||||||
|
func main() {
|
||||||
|
// Create application with options
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "wails-open-file",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
SingleInstanceLock: &options.SingleInstanceLock{
|
||||||
|
UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc",
|
||||||
|
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
|
||||||
|
},
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### Linux
|
### Linux
|
||||||
Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually.
|
Currently, Wails doesn't support bundling for Linux. So, you need to create file associations manually.
|
||||||
For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle.
|
For example if you distribute your app as a .deb package, you can create file associations by adding required files in you bundle.
|
||||||
@ -179,6 +203,26 @@ func main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations:
|
You also can enable single instance lock for your app. In this case, when you open file with your app, new instance of app is not launched
|
||||||
On Windows and Linux when associated file is opened, new instance of your app is launched.
|
and arguments are passed to already running instance. Check single instance lock guide for details. Example:
|
||||||
Currently, Wails doesn't support opening files in already running app. There is plugin for single instance support for v3 in development.
|
```go title="main.go"
|
||||||
|
func main() {
|
||||||
|
// Create application with options
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "wails-open-file",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
SingleInstanceLock: &options.SingleInstanceLock{
|
||||||
|
UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc",
|
||||||
|
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
|
||||||
|
},
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
80
website/docs/guides/single-instance-lock.mdx
Normal file
80
website/docs/guides/single-instance-lock.mdx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
# Single Instance Lock
|
||||||
|
|
||||||
|
Single instance lock is a mechanism that allows you to prevent multiple instances of your app from running at the same time.
|
||||||
|
It is useful for apps that are designed to open files from the command line or from the OS file explorer.
|
||||||
|
|
||||||
|
## Important
|
||||||
|
|
||||||
|
Single Instance Lock does not implement a secure communications protocol between instances. When using single instance lock,
|
||||||
|
your app should treat any data passed to it from second instance callback as untrusted.
|
||||||
|
You should verify that args that you receive are valid and don't contain any malicious data.
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
Windows: Single instance lock is implemented using a named mutex. The mutex name is generated from the unique id that you provide. Data is passed to the first instance via a shared window using [SendMessage](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-sendmessage)
|
||||||
|
macOS: Single instance lock is implemented using a named mutex. The mutex name is generated from the unique id that you provide. Data is passed to the first instance via [NSDistributedNotificationCenter](https://developer.apple.com/documentation/foundation/nsdistributednotificationcenter)
|
||||||
|
Linux: Single instance lock is implemented using [dbus](https://www.freedesktop.org/wiki/Software/dbus/). The dbus name is generated from the unique id that you provide. Data is passed to the first instance via [dbus](https://www.freedesktop.org/wiki/Software/dbus/)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
When creating your app, you can enable single instance lock by passing a `SingleInstanceLock` struct to the `App` struct.
|
||||||
|
Use the `UniqueId` field to specify a unique id for your app.
|
||||||
|
This id is used to generate the mutex name on Windows and macOS and the dbus name on Linux. Use a UUID to ensure that the id is unique.
|
||||||
|
The `OnSecondInstanceLaunch` field is used to specify a callback that is called when a second instance of your app is launched.
|
||||||
|
The callback receives a `SecondInstanceData` struct that contains the command line arguments passed to the second instance and the working directory of the second instance.
|
||||||
|
|
||||||
|
Note that OnSecondInstanceLaunch don't trigger windows focus.
|
||||||
|
You need to call `runtime.WindowUnminimise` and `runtime.Show` to bring your app to the front.
|
||||||
|
Note that on linux systems window managers may prevent your app from being brought to the front to avoid stealing focus.
|
||||||
|
|
||||||
|
```go title="main.go"
|
||||||
|
var wailsContext *context.Context
|
||||||
|
|
||||||
|
// NewApp creates a new App application struct
|
||||||
|
func NewApp() *App {
|
||||||
|
return &App{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startup is called when the app starts. The context is saved
|
||||||
|
// so we can call the runtime methods
|
||||||
|
func (a *App) startup(ctx context.Context) {
|
||||||
|
wailsContext = &ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
|
||||||
|
secondInstanceArgs = secondInstanceData.Args
|
||||||
|
|
||||||
|
println("user opened second instance", strings.Join(secondInstanceData.Args, ","))
|
||||||
|
println("user opened second from", secondInstanceData.WorkingDirectory)
|
||||||
|
runtime.WindowUnminimise(*wailsContext)
|
||||||
|
runtime.Show(*wailsContext)
|
||||||
|
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Create an instance of the app structure
|
||||||
|
app := NewApp()
|
||||||
|
|
||||||
|
// Create application with options
|
||||||
|
err := wails.Run(&options.App{
|
||||||
|
Title: "wails-open-file",
|
||||||
|
Width: 1024,
|
||||||
|
Height: 768,
|
||||||
|
AssetServer: &assetserver.Options{
|
||||||
|
Assets: assets,
|
||||||
|
},
|
||||||
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
|
OnStartup: app.startup,
|
||||||
|
SingleInstanceLock: &options.SingleInstanceLock{
|
||||||
|
UniqueId: "e3984e08-28dc-4e3d-b70a-45e961589cdc",
|
||||||
|
OnSecondInstanceLaunch: app.onSecondInstanceLaunch,
|
||||||
|
},
|
||||||
|
Bind: []interface{}{
|
||||||
|
app,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
println("Error:", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added Single Instance Lock support with passing arguments to first instance. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2951)
|
||||||
- Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878)
|
- Added support for enabling/disabling swipe gestures for Windows WebView2. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2878)
|
||||||
- When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915)
|
- When building with `-devtools` flag, CMD/CTRL+SHIFT+F12 can be used to open the devtools. Added by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/2915)
|
||||||
– Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918)
|
– Added file association support for macOS and Windows. Added by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/2918)
|
||||||
|
Loading…
Reference in New Issue
Block a user