diff --git a/v3/examples/plugins/Taskfile.yml b/v3/examples/plugins/Taskfile.yml
new file mode 100644
index 00000000..3c88c18c
--- /dev/null
+++ b/v3/examples/plugins/Taskfile.yml
@@ -0,0 +1,41 @@
+version: '3'
+
+tasks:
+
+ pre-build:
+ summary: Pre-build hooks
+
+ post-build:
+ summary: Post-build hooks
+
+ build:
+ summary: Builds the application
+ cmds:
+ - task: pre-build
+ - go build -gcflags=all="-N -l" -o bin/testapp main.go
+ - task: post-build
+ env:
+ CGO_CFLAGS: "-mmacosx-version-min=10.13"
+ CGO_LDFLAGS: "-mmacosx-version-min=10.13"
+
+ generate-icons:
+ summary: Generates Windows `.ico` and Mac `.icns` files from an image
+ cmds:
+ # Generates both .ico and .icns files
+ - wails generate icons -input build/appicon.png
+
+ build-prod:
+ summary: Creates a production build of the application
+ cmds:
+ - go build -tags production -ldflags="-w -s" -o bin/testapp
+
+ package-darwin:
+ summary: Packages a production build of the application into a `.app` bundle
+ deps:
+ - build-prod
+ - generate-icons
+ cmds:
+ - mkdir -p buildtest.app/Contents/{MacOS,Resources}
+ - cp build/icons.icns buildtest.app/Contents/Resources
+ - cp bin/testapp buildtest.app/Contents/MacOS
+ - cp build/Info.plist buildtest.app/Contents
\ No newline at end of file
diff --git a/v3/examples/plugins/build/Info.dev.plist b/v3/examples/plugins/build/Info.dev.plist
new file mode 100644
index 00000000..d6d28b17
--- /dev/null
+++ b/v3/examples/plugins/build/Info.dev.plist
@@ -0,0 +1,35 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ My App
+ CFBundleExecutable
+ app
+ CFBundleIdentifier
+ com.wails.app
+ CFBundleVersion
+ v1.0.0
+ CFBundleGetInfoString
+ The ultimate thing
+ CFBundleShortVersionString
+ v1
+ CFBundleIconFile
+ icons
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ (c) Me
+ NSAppTransportSecurity
+
+ NSAllowsLocalNetworking
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v3/examples/plugins/build/Info.plist b/v3/examples/plugins/build/Info.plist
new file mode 100644
index 00000000..ab571ad4
--- /dev/null
+++ b/v3/examples/plugins/build/Info.plist
@@ -0,0 +1,27 @@
+
+
+
+ CFBundlePackageType
+ APPL
+ CFBundleName
+ My App
+ CFBundleExecutable
+ testapp
+ CFBundleIdentifier
+ com.wails.app
+ CFBundleVersion
+ v1.0.0
+ CFBundleGetInfoString
+ The ultimate thing
+ CFBundleShortVersionString
+ v1
+ CFBundleIconFile
+ icons
+ LSMinimumSystemVersion
+ 10.13.0
+ NSHighResolutionCapable
+ true
+ NSHumanReadableCopyright
+ (c) Me
+
+
\ No newline at end of file
diff --git a/v3/examples/plugins/build/appicon.png b/v3/examples/plugins/build/appicon.png
new file mode 100644
index 00000000..e69de29b
diff --git a/v3/examples/plugins/build/icons.icns b/v3/examples/plugins/build/icons.icns
new file mode 100644
index 00000000..e69de29b
diff --git a/v3/examples/plugins/build/icons.ico b/v3/examples/plugins/build/icons.ico
new file mode 100644
index 00000000..e69de29b
diff --git a/v3/examples/plugins/build/info.json b/v3/examples/plugins/build/info.json
new file mode 100644
index 00000000..1005eb5c
--- /dev/null
+++ b/v3/examples/plugins/build/info.json
@@ -0,0 +1,15 @@
+{
+ "fixed": {
+ "file_version": "v1.0.0"
+ },
+ "info": {
+ "0000": {
+ "ProductVersion": "v1.0.0",
+ "CompanyName": "My Company Name",
+ "FileDescription": "A thing that does a thing",
+ "LegalCopyright": "(c) 2023 My Company Name",
+ "ProductName": "My Product Name",
+ "Comments": "This is a comment"
+ }
+ }
+}
\ No newline at end of file
diff --git a/v3/examples/plugins/build/wails.exe.manifest b/v3/examples/plugins/build/wails.exe.manifest
new file mode 100644
index 00000000..fb1ce5bd
--- /dev/null
+++ b/v3/examples/plugins/build/wails.exe.manifest
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+ true/pm
+ permonitorv2,permonitor
+
+
+
\ No newline at end of file
diff --git a/v3/examples/plugins/go.sum b/v3/examples/plugins/go.sum
index de345727..c16c742b 100644
--- a/v3/examples/plugins/go.sum
+++ b/v3/examples/plugins/go.sum
@@ -3,7 +3,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@@ -15,6 +17,7 @@ github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5Az
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
@@ -58,6 +61,8 @@ modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
+modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
modernc.org/libc v1.22.3 h1:D/g6O5ftAfavceqlLOFwaZuA5KYafKwmr30A6iSqoyY=
modernc.org/libc v1.22.3/go.mod h1:MQrloYP209xa2zHome2a8HLiLm6k0UT8CoHpV74tOFw=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
@@ -70,5 +75,7 @@ modernc.org/sqlite v1.21.0 h1:4aP4MdUf15i3R3M2mx6Q90WHKz3nZLoz96zlB6tNdow=
modernc.org/sqlite v1.21.0/go.mod h1:XwQ0wZPIh1iKb5mkvCJ3szzbhk+tykC8ZWqTRTgYRwI=
modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/tcl v1.15.1 h1:mOQwiEK4p7HruMZcwKTZPw/aqtGM4aY00uzWhlKKYws=
modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.7.0 h1:xkDw/KepgEjeizO2sNco+hqYkU12taxQFqPEmgm1GWE=
diff --git a/v3/examples/plugins/main.go b/v3/examples/plugins/main.go
index 6c3092b3..a28398eb 100644
--- a/v3/examples/plugins/main.go
+++ b/v3/examples/plugins/main.go
@@ -6,6 +6,7 @@ import (
"github.com/wailsapp/wails/v3/plugins/browser"
"github.com/wailsapp/wails/v3/plugins/kvstore"
"github.com/wailsapp/wails/v3/plugins/log"
+ "github.com/wailsapp/wails/v3/plugins/single_instance"
"github.com/wailsapp/wails/v3/plugins/sqlite"
"os"
"plugin_demo/plugins/hashes"
@@ -31,6 +32,10 @@ func main() {
Filename: "store.json",
AutoSave: true,
}),
+ "single_instance": single_instance.NewPlugin(&single_instance.Config{
+ // When true, the original app will be activated when a second instance is launched
+ ActivateAppOnSubsequentLaunch: true,
+ }),
},
Assets: application.AssetOptions{
FS: assets,
diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go
index e16a3e32..ad9376fa 100644
--- a/v3/pkg/application/application.go
+++ b/v3/pkg/application/application.go
@@ -37,6 +37,7 @@ func New(appOptions Options) *App {
systemTrays: make(map[uint]*SystemTray),
log: logger.New(appOptions.Logger.CustomLoggers...),
contextMenus: make(map[string]*Menu),
+ pid: os.Getpid(),
}
globalApplication = result
@@ -187,6 +188,7 @@ type App struct {
// Hooks
windowCreatedCallbacks []func(window *WebviewWindow)
+ pid int
}
func (a *App) getSystemTrayID() uint {
@@ -215,6 +217,10 @@ func (a *App) NewWebviewWindow() *WebviewWindow {
return a.NewWebviewWindowWithOptions(nil)
}
+func (a *App) GetPID() int {
+ return a.pid
+}
+
func (a *App) info(message string, args ...any) {
a.Log(&logger.Message{
Level: "INFO",
@@ -458,7 +464,9 @@ func (a *App) Quit() {
wg.Done()
}()
wg.Wait()
- a.impl.destroy()
+ if a.impl != nil {
+ a.impl.destroy()
+ }
}
func (a *App) SetMenu(menu *Menu) {
diff --git a/v3/plugins/single_instance/README.md b/v3/plugins/single_instance/README.md
new file mode 100644
index 00000000..1dc48ddc
--- /dev/null
+++ b/v3/plugins/single_instance/README.md
@@ -0,0 +1,31 @@
+# single-instance Plugin
+
+This example plugin provides a way to generate hashes of strings.
+
+## Installation
+
+Add the plugin to the `Plugins` option in the Applications options:
+
+```go
+ Plugins: map[string]application.Plugin{
+ "single_instance": single_instance.NewPlugin(&single_instance.Config{
+ // When true, the original app will be activated when a second instance is launched
+ ActivateAppOnSubsequentLaunch: true,
+ }
+ },
+```
+
+## Usage
+
+This plugin prevents the launch of multiple copies of your application.
+If you set `ActivateAppOnSubsequentLaunch` to true the original app will be activated when a second instance is launched.
+
+## Support
+
+If you find a bug in this plugin, please raise a ticket [here](https://github.com/plugin/repository).
+Please do not contact the Wails team for support.
+
+## Credit
+
+This plugin contains modified code from the awesome [go-singleinstance](https://github.com/allan-simon/go-singleinstance) module (c) 2015 Allan Simon.
+Original license file has been renamed `go-singleinstance.LICENSE` and is available [here](./singleinstance_LICENSE).
\ No newline at end of file
diff --git a/v3/plugins/single_instance/go-singleinstance.LICENSE b/v3/plugins/single_instance/go-singleinstance.LICENSE
new file mode 100644
index 00000000..67bb9ea5
--- /dev/null
+++ b/v3/plugins/single_instance/go-singleinstance.LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Allan Simon
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/v3/plugins/single_instance/lock.go b/v3/plugins/single_instance/lock.go
new file mode 100644
index 00000000..ba275876
--- /dev/null
+++ b/v3/plugins/single_instance/lock.go
@@ -0,0 +1,16 @@
+package single_instance
+
+import (
+ "os"
+ "strconv"
+)
+
+func GetLockFilePid(filename string) (pid int, err error) {
+ contents, err := os.ReadFile(filename)
+ if err != nil {
+ return
+ }
+
+ pid, err = strconv.Atoi(string(contents))
+ return
+}
diff --git a/v3/plugins/single_instance/lock_posix.go b/v3/plugins/single_instance/lock_posix.go
new file mode 100644
index 00000000..06139d52
--- /dev/null
+++ b/v3/plugins/single_instance/lock_posix.go
@@ -0,0 +1,38 @@
+//go:build !windows
+
+package single_instance
+
+import (
+ "os"
+ "strconv"
+ "syscall"
+)
+
+// 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, PID int) (*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
+ }
+
+ // Write PID to lock file
+ contents := strconv.Itoa(PID)
+ if err := file.Truncate(0); err != nil {
+ file.Close()
+ return nil, err
+ }
+ if _, err := file.WriteString(contents); err != nil {
+ file.Close()
+ return nil, err
+ }
+
+ return file, nil
+}
diff --git a/v3/plugins/single_instance/lock_windows.go b/v3/plugins/single_instance/lock_windows.go
new file mode 100644
index 00000000..67aabe68
--- /dev/null
+++ b/v3/plugins/single_instance/lock_windows.go
@@ -0,0 +1,35 @@
+//go:build windows
+
+package single_instance
+
+import (
+ "os"
+ "strconv"
+)
+
+// 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, PID int) (*os.File, error) {
+ if _, err := os.Stat(filename); err == nil {
+ // If the file exists, we first try to remove it
+ if err = os.Remove(filename); err != nil {
+ return nil, err
+ }
+ } else if !os.IsNotExist(err) {
+ return nil, err
+ }
+
+ file, err := os.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600)
+ if err != nil {
+ return nil, err
+ }
+
+ // Write PID to lock file
+ _, err = file.WriteString(strconv.Itoa(PID))
+ if err != nil {
+ return nil, err
+ }
+
+ return file, nil
+}
diff --git a/v3/plugins/single_instance/plugin.go b/v3/plugins/single_instance/plugin.go
new file mode 100644
index 00000000..a6114d07
--- /dev/null
+++ b/v3/plugins/single_instance/plugin.go
@@ -0,0 +1,82 @@
+package single_instance
+
+import (
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "os"
+ "path/filepath"
+)
+
+type Config struct {
+ // Add any configuration options here
+ LockFileName string
+ LockFilePath string
+ ActivateAppOnSubsequentLaunch bool
+}
+
+type Plugin struct {
+ config *Config
+ app *application.App
+ lockfile *os.File
+}
+
+func (p *Plugin) CallableByJS() []string {
+ return []string{}
+}
+
+func (p *Plugin) InjectJS() string {
+ return ""
+}
+
+func NewPlugin(config *Config) *Plugin {
+ if config.LockFilePath == "" {
+ // Use the system default temp directory
+ config.LockFilePath = os.TempDir()
+ }
+ if config.LockFileName == "" {
+ // Use the executable name
+ config.LockFileName = filepath.Base(os.Args[0]) + ".lock"
+ }
+ return &Plugin{
+ config: config,
+ }
+}
+
+// Shutdown is called when the app is shutting down
+func (p *Plugin) Shutdown() {
+ p.lockfile.Close()
+}
+
+// Name returns the name of the plugin.
+func (p *Plugin) Name() string {
+ return "github.com/wailsapp/wails/v3/plugins/single-instance"
+}
+
+// Init is called when the app is starting up. You can use this to
+// initialise any resources you need. You can also access the application
+// instance via the app property.
+func (p *Plugin) Init(app *application.App) error {
+ p.app = app
+
+ var err error
+ lockfileName := p.config.LockFilePath + "/" + p.config.LockFileName
+ p.lockfile, err = CreateLockFile(lockfileName, p.app.GetPID())
+ if err != nil {
+ if p.config.ActivateAppOnSubsequentLaunch {
+ pid, err := GetLockFilePid(lockfileName)
+ if err != nil {
+ return err
+ }
+ err = p.activeInstance(pid)
+ if err != nil {
+ return err
+ }
+ }
+ os.Exit(0)
+ }
+ return nil
+}
+
+// Exported returns a list of exported methods that can be called from the frontend
+func (p *Plugin) Exported() []string {
+ return []string{}
+}
diff --git a/v3/plugins/single_instance/plugin.toml b/v3/plugins/single_instance/plugin.toml
new file mode 100644
index 00000000..6682e449
--- /dev/null
+++ b/v3/plugins/single_instance/plugin.toml
@@ -0,0 +1,11 @@
+# This is the plugin definition file for the "single-instance" plugin.
+
+Name = "single-instance"
+Description = "Allows only a single instance of your application to run"
+Author = "Lea Anthony"
+Version = "v1.0.0"
+Website = "https://wails.io"
+Repository = "https://github.com/wailsapp/wails/v3/plugins/single-instance
+License = "MIT"
+
+
diff --git a/v3/plugins/single_instance/plugin_darwin.go b/v3/plugins/single_instance/plugin_darwin.go
new file mode 100644
index 00000000..c4f47f71
--- /dev/null
+++ b/v3/plugins/single_instance/plugin_darwin.go
@@ -0,0 +1,24 @@
+//go:build darwin
+
+package single_instance
+
+/*
+#cgo CFLAGS: -mmacosx-version-min=10.13 -x objective-c
+#cgo LDFLAGS: -framework Cocoa -framework AppKit -mmacosx-version-min=10.13
+
+#import
+
+void activateApplicationWithProcessID(int pid) {
+ NSRunningApplication *app = [NSRunningApplication runningApplicationWithProcessIdentifier:pid];
+ if (app != nil) {
+ [app unhide];
+ [app activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
+ }
+}
+*/
+import "C"
+
+func (p *Plugin) activeInstance(pid int) error {
+ C.activateApplicationWithProcessID(C.int(pid))
+ return nil
+}