Major plugin updates

This commit is contained in:
Lea Anthony
2024-04-14 21:41:33 +10:00
parent cf130a6e25
commit c7ed7e72d4
52 changed files with 1155 additions and 326 deletions
@@ -0,0 +1,31 @@
# single-instance Plugin
This plugin provides a way to prevent multiple launches of your application.
## 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).
@@ -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.
@@ -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
}
@@ -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
}
@@ -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
}
@@ -0,0 +1,82 @@
package single_instance
import (
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Config struct {
// Add any configuration options here
LockFileName string
LockFilePath string
ActivateAppOnSubsequentLaunch bool
}
type Plugin struct {
config *Config
lockfile *os.File
}
func (p *Plugin) CallableByJS() []string {
return []string{}
}
func (p *Plugin) Assets() fs.FS {
return nil
}
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() error {
return 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(api application.PluginAPI) error {
var err error
lockfileName := p.config.LockFilePath + "/" + p.config.LockFileName
p.lockfile, err = CreateLockFile(lockfileName, application.Get().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
}
}
return fmt.Errorf("another instance of this application is already running")
}
return nil
}
// Exported returns a list of exported methods that can be called from the frontend
func (p *Plugin) Exported() []string {
return []string{}
}
@@ -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
@@ -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 <AppKit/AppKit.h>
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
}
@@ -0,0 +1,29 @@
//go:build linux
package single_instance
import (
"os"
"os/signal"
"syscall"
"github.com/wailsapp/wails/v3/pkg/application"
)
func init() {
sigc := make(chan os.Signal, 1)
signal.Notify(sigc,
syscall.SIGUSR2,
)
go func() {
for {
<-sigc
application.Get().Show()
}
}()
}
func (p *Plugin) activeInstance(pid int) error {
syscall.Kill(pid, syscall.SIGUSR2)
return nil
}
@@ -0,0 +1,34 @@
//go:build windows
package single_instance
import (
"github.com/wailsapp/wails/v3/pkg/w32"
"syscall"
)
type enumWindowsCallback func(hwnd syscall.Handle, lParam uintptr) uintptr
func enumWindowsProc(hwnd syscall.Handle, lParam uintptr) uintptr {
_, processID := w32.GetWindowThreadProcessId(uintptr(hwnd))
targetProcessID := uint32(lParam)
if uint32(processID) == targetProcessID {
// Bring the window forward
w32.SetForegroundWindow(w32.HWND(hwnd))
}
// Continue enumeration
return 1
}
func (p *Plugin) activeInstance(pid int) error {
// Get the window associated with the process ID.
targetProcessID := uint32(pid) // Replace with the desired process ID
w32.EnumWindows(
syscall.NewCallback(enumWindowsCallback(enumWindowsProc)),
uintptr(targetProcessID),
)
return nil
}