mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 17:52:29 +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/go-git/go-git/v5 v5.3.0
|
||||
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/uuid v1.3.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.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
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.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=
|
||||
|
@ -15,6 +15,8 @@
|
||||
|
||||
@property bool alwaysOnTop;
|
||||
@property bool startHidden;
|
||||
@property (retain) NSString* singleInstanceUniqueId;
|
||||
@property bool singleInstanceLockEnabled;
|
||||
@property bool startFullscreen;
|
||||
@property (retain) WailsWindow* mainWindow;
|
||||
|
||||
@ -22,4 +24,8 @@
|
||||
|
||||
extern void HandleOpenFile(char *);
|
||||
|
||||
extern void HandleSecondInstanceData(char * message);
|
||||
|
||||
void SendDataToFirstInstance(char * singleInstanceUniqueId, char * text);
|
||||
|
||||
#endif /* AppDelegate_h */
|
||||
|
@ -40,6 +40,28 @@
|
||||
[self.mainWindow setCollectionBehavior:behaviour];
|
||||
[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 {
|
||||
|
@ -17,7 +17,7 @@
|
||||
#define WindowStartsMinimised 2
|
||||
#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 SetTitle(void* ctx, const char *title);
|
||||
|
@ -14,7 +14,7 @@
|
||||
#import "WailsMenu.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];
|
||||
|
||||
@ -48,6 +48,11 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
|
||||
result.startFullscreen = true;
|
||||
}
|
||||
|
||||
if ( singleInstanceLockEnabled == 1 ) {
|
||||
result.singleInstanceLockEnabled = true;
|
||||
result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId);
|
||||
}
|
||||
|
||||
result.alwaysOnTop = alwaysOnTop;
|
||||
result.hideOnClose = hideWindowOnClose;
|
||||
|
||||
@ -367,6 +372,8 @@ void Run(void *inctx, const char* url) {
|
||||
delegate.mainWindow = ctx.mainWindow;
|
||||
delegate.alwaysOnTop = ctx.alwaysOnTop;
|
||||
delegate.startHidden = ctx.startHidden;
|
||||
delegate.singleInstanceLockEnabled = ctx.singleInstanceLockEnabled;
|
||||
delegate.singleInstanceUniqueId = ctx.singleInstanceUniqueId;
|
||||
delegate.startFullscreen = ctx.startFullscreen;
|
||||
|
||||
NSString *_url = safeInit(url);
|
||||
|
@ -40,6 +40,9 @@
|
||||
@property bool startHidden;
|
||||
@property bool startFullscreen;
|
||||
|
||||
@property bool singleInstanceLockEnabled;
|
||||
@property (retain) NSString* singleInstanceUniqueId;
|
||||
|
||||
@property (retain) NSEvent* mouseEvent;
|
||||
|
||||
@property bool alwaysOnTop;
|
||||
|
@ -39,6 +39,7 @@ var messageBuffer = make(chan string, 100)
|
||||
var requestBuffer = make(chan webview.Request, 100)
|
||||
var callbackBuffer = make(chan uint, 10)
|
||||
var openFilepathBuffer = make(chan string, 100)
|
||||
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||
|
||||
type Frontend struct {
|
||||
|
||||
@ -109,6 +110,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
go result.startMessageProcessor()
|
||||
go result.startCallbackProcessor()
|
||||
go result.startFileOpenProcessor()
|
||||
go result.startSecondInstanceProcessor()
|
||||
|
||||
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() {
|
||||
for message := range messageBuffer {
|
||||
f.processMessage(message)
|
||||
@ -162,6 +173,10 @@ func (f *Frontend) WindowSetDarkTheme() {
|
||||
func (f *Frontend) Run(ctx context.Context) error {
|
||||
f.ctx = ctx
|
||||
|
||||
if f.frontendOptions.SingleInstanceLock != nil {
|
||||
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
|
||||
}
|
||||
|
||||
var _debug = ctx.Value("debug")
|
||||
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)
|
||||
devtoolsEnabled := bool2Cint(devtools)
|
||||
defaultContextMenuEnabled := bool2Cint(debug || frontendOptions.EnableDefaultContextMenu)
|
||||
singleInstanceEnabled := bool2Cint(frontendOptions.SingleInstanceLock != nil)
|
||||
|
||||
var fullSizeContent, hideTitleBar, hideTitle, useToolbar, webviewIsTransparent 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)
|
||||
|
||||
singleInstanceUniqueIdStr := ""
|
||||
if frontendOptions.SingleInstanceLock != nil {
|
||||
singleInstanceUniqueIdStr = frontendOptions.SingleInstanceLock.UniqueId
|
||||
}
|
||||
singleInstanceUniqueId := c.String(singleInstanceUniqueIdStr)
|
||||
|
||||
enableFraudulentWebsiteWarnings := C.bool(frontendOptions.EnableFraudulentWebsiteDetection)
|
||||
|
||||
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,
|
||||
hideTitleBar, titlebarAppearsTransparent, hideTitle, useToolbar, hideToolbarSeparator, webviewIsTransparent,
|
||||
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
|
||||
result := &Window{
|
||||
|
@ -102,6 +102,8 @@ var initOnce = sync.Once{}
|
||||
|
||||
const startURL = "wails://wails/"
|
||||
|
||||
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||
|
||||
type Frontend struct {
|
||||
|
||||
// Context
|
||||
@ -201,6 +203,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
C.free(unsafe.Pointer(prgname))
|
||||
}
|
||||
|
||||
go result.startSecondInstanceProcessor()
|
||||
|
||||
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())
|
||||
|
||||
return nil
|
||||
@ -505,3 +513,12 @@ func (f *Frontend) startRequestProcessor() {
|
||||
func processURLRequest(request unsafe.Pointer) {
|
||||
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/"
|
||||
|
||||
var secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
|
||||
|
||||
type Screen = frontend.Screen
|
||||
|
||||
type Frontend struct {
|
||||
@ -113,6 +115,8 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
|
||||
}
|
||||
result.assets = assets
|
||||
|
||||
go result.startSecondInstanceProcessor()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@ -137,6 +141,10 @@ func (f *Frontend) Run(ctx context.Context) error {
|
||||
|
||||
f.chromium = edge.NewChromium()
|
||||
|
||||
if f.frontendOptions.SingleInstanceLock != nil {
|
||||
SetupSingleInstance(f.frontendOptions.SingleInstanceLock.UniqueId)
|
||||
}
|
||||
|
||||
mainWindow := NewWindow(nil, f.frontendOptions, f.versionInfo, f.chromium)
|
||||
f.mainWindow = mainWindow
|
||||
|
||||
@ -826,3 +834,12 @@ func (f *Frontend) ShowWindow() {
|
||||
func (f *Frontend) onFocus(arg *winc.Event) {
|
||||
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() {
|
||||
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() {
|
||||
|
@ -136,6 +136,15 @@ func (fm *Form) Minimise() {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ var (
|
||||
procShowWindowAsync = moduser32.NewProc("ShowWindowAsync")
|
||||
procUpdateWindow = moduser32.NewProc("UpdateWindow")
|
||||
procCreateWindowEx = moduser32.NewProc("CreateWindowExW")
|
||||
procFindWindowW = moduser32.NewProc("FindWindowW")
|
||||
procAdjustWindowRect = moduser32.NewProc("AdjustWindowRect")
|
||||
procAdjustWindowRectEx = moduser32.NewProc("AdjustWindowRectEx")
|
||||
procDestroyWindow = moduser32.NewProc("DestroyWindow")
|
||||
@ -263,6 +264,14 @@ func CreateWindowEx(exStyle uint, className, windowName *uint16,
|
||||
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 {
|
||||
ret, _, _ := procAdjustWindowRectEx.Call(
|
||||
uintptr(unsafe.Pointer(rect)),
|
||||
|
@ -5,6 +5,8 @@ import (
|
||||
"html"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
@ -82,6 +84,8 @@ type App struct {
|
||||
// services of Apple and Microsoft.
|
||||
EnableFraudulentWebsiteDetection bool
|
||||
|
||||
SingleInstanceLock *SingleInstanceLock
|
||||
|
||||
Windows *windows.Options
|
||||
Mac *mac.Options
|
||||
Linux *linux.Options
|
||||
@ -165,6 +169,30 @@ func MergeDefaults(appoptions *App) {
|
||||
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) {
|
||||
switch runtime.GOOS {
|
||||
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
|
||||
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.
|
||||
@ -179,6 +203,26 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
## Limitations:
|
||||
On Windows and Linux when associated file is opened, new instance of your app is launched.
|
||||
Currently, Wails doesn't support opening files in already running app. There is plugin for single instance support for v3 in development.
|
||||
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,
|
||||
},
|
||||
})
|
||||
}
|
||||
```
|
||||
|
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 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)
|
||||
- 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)
|
||||
|
Loading…
Reference in New Issue
Block a user