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