5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 08:10:56 +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:
Andrey Pshenkin 2023-10-23 11:31:56 +01:00 committed by GitHub
parent a59f8b2cf3
commit c24bd5e3e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 578 additions and 22 deletions

View File

@ -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

View File

@ -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=

View File

@ -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 */

View File

@ -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 {

View File

@ -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);

View File

@ -14,15 +14,15 @@
#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];
WailsContext *result = [WailsContext new];
result.devtoolsEnabled = devtoolsEnabled;
result.defaultContextMenuEnabled = defaultContextMenuEnabled;
if ( windowStartState == WindowStartsFullscreen ) {
fullscreen = 1;
}
@ -30,7 +30,7 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
[result CreateWindow:width :height :frameless :resizable :fullscreen :fullSizeContent :hideTitleBar :titlebarAppearsTransparent :hideTitle :useToolbar :hideToolbarSeparator :webviewIsTransparent :hideWindowOnClose :safeInit(appearance) :windowIsTranslucent :minWidth :minHeight :maxWidth :maxHeight :fraudulentWebsiteWarningEnabled :preferences];
[result SetTitle:safeInit(title)];
[result Center];
switch( windowStartState ) {
case WindowStartsMaximised:
[result.mainWindow zoom:nil];
@ -43,14 +43,19 @@ WailsContext* Create(const char* title, int width, int height, int frameless, in
if ( startsHidden == 1 ) {
result.startHidden = true;
}
if ( fullscreen == 1 ) {
result.startFullscreen = true;
}
if ( singleInstanceLockEnabled == 1 ) {
result.singleInstanceLockEnabled = true;
result.singleInstanceUniqueId = safeInit(singleInstanceUniqueId);
}
result.alwaysOnTop = alwaysOnTop;
result.hideOnClose = hideWindowOnClose;
return result;
}
@ -181,7 +186,7 @@ const char* GetPosition(void *inctx) {
NSString *result = [NSString stringWithFormat:@"%d,%d",x,y];
return [result UTF8String];
}
const bool IsFullScreen(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
return [ctx IsFullScreen];
@ -249,7 +254,7 @@ NSString* safeInit(const char* input) {
void MessageDialog(void *inctx, const char* dialogType, const char* title, const char* message, const char* button1, const char* button2, const char* button3, const char* button4, const char* defaultButton, const char* cancelButton, void* iconData, int iconDataLength) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_dialogType = safeInit(dialogType);
NSString *_title = safeInit(title);
NSString *_message = safeInit(message);
@ -259,33 +264,33 @@ void MessageDialog(void *inctx, const char* dialogType, const char* title, const
NSString *_button4 = safeInit(button4);
NSString *_defaultButton = safeInit(defaultButton);
NSString *_cancelButton = safeInit(cancelButton);
ON_MAIN_THREAD(
[ctx MessageDialog:_dialogType :_title :_message :_button1 :_button2 :_button3 :_button4 :_defaultButton :_cancelButton :iconData :iconDataLength];
)
}
void OpenFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int allowDirectories, int allowFiles, int canCreateDirectories, int treatPackagesAsDirectories, int resolveAliases, int showHiddenFiles, int allowMultipleSelection, const char* filters) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
NSString *_defaultFilename = safeInit(defaultFilename);
NSString *_defaultDirectory = safeInit(defaultDirectory);
NSString *_filters = safeInit(filters);
ON_MAIN_THREAD(
[ctx OpenFileDialog:_title :_defaultFilename :_defaultDirectory :allowDirectories :allowFiles :canCreateDirectories :treatPackagesAsDirectories :resolveAliases :showHiddenFiles :allowMultipleSelection :_filters];
)
}
void SaveFileDialog(void *inctx, const char* title, const char* defaultFilename, const char* defaultDirectory, int canCreateDirectories, int treatPackagesAsDirectories, int showHiddenFiles, const char* filters) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
NSString *_title = safeInit(title);
NSString *_defaultFilename = safeInit(defaultFilename);
NSString *_defaultDirectory = safeInit(defaultDirectory);
NSString *_filters = safeInit(filters);
ON_MAIN_THREAD(
[ctx SaveFileDialog:_title :_defaultFilename :_defaultDirectory :canCreateDirectories :treatPackagesAsDirectories :showHiddenFiles :_filters];
)
@ -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);
@ -395,7 +402,7 @@ void WindowPrint(void *inctx) {
WailsContext *ctx = (__bridge WailsContext*) inctx;
WKWebView* webView = ctx.webview;
// I think this should be exposed as a config
// I think this should be exposed as a config
// It directly affects the printed output/PDF
NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo];
pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic;
@ -417,4 +424,4 @@ void WindowPrint(void *inctx) {
[po runOperationModalForWindow:ctx.mainWindow delegate:ctx.mainWindow.delegate didRunSelector:nil contextInfo:nil];
)
}
}
}

View File

@ -40,6 +40,9 @@
@property bool startHidden;
@property bool startFullscreen;
@property bool singleInstanceLockEnabled;
@property (retain) NSString* singleInstanceUniqueId;
@property (retain) NSEvent* mouseEvent;
@property bool alwaysOnTop;

View File

@ -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")

View 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
}

View File

@ -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{

View File

@ -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)
}
}
}

View 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)
}
}

View File

@ -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)
}
}
}

View 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
}

View File

@ -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() {

View File

@ -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)
}

View File

@ -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)),

View File

@ -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":

View File

@ -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,
},
})
}
```

View 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())
}
}
```

View File

@ -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)