mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-06 10:19:31 +08:00
Single Instance feature.
Fix missing events on darwin.
This commit is contained in:
parent
7cee957161
commit
773dca77d4
@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- New `wails3 generate webview2bootstrapper` command by [@leaanthony](https://github.com/leaanthony)
|
- New `wails3 generate webview2bootstrapper` command by [@leaanthony](https://github.com/leaanthony)
|
||||||
- Added `init()` method in runtime to allow manual initialisation of the runtime by [@leaanthony](https://github.com/leaanthony)
|
- Added `init()` method in runtime to allow manual initialisation of the runtime by [@leaanthony](https://github.com/leaanthony)
|
||||||
- Added `WindowDidMoveDebounceMS` option to Window's WindowOptions by [@leaanthony](https://github.com/leaanthony)
|
- Added `WindowDidMoveDebounceMS` option to Window's WindowOptions by [@leaanthony](https://github.com/leaanthony)
|
||||||
|
- Added Single Instance feature by [@leaanthony](https://github.com/leaanthony). Based on the [v2 PR](https://github.com/wailsapp/wails/pull/2951) by @APshenkin.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -5,8 +5,6 @@ sidebar:
|
|||||||
order: 1
|
order: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
import { Tabs, TabItem } from "@astrojs/starlight/components";
|
|
||||||
|
|
||||||
The Wails CLI provides a comprehensive set of commands to help you develop, build, and maintain your Wails applications.
|
The Wails CLI provides a comprehensive set of commands to help you develop, build, and maintain your Wails applications.
|
||||||
|
|
||||||
## Core Commands
|
## Core Commands
|
||||||
@ -133,21 +131,21 @@ wails3 generate bindings [flags] [patterns...]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|-------------|---------------------------|---------------------|
|
||||||
| `-f` | Additional Go build flags | |
|
| `-f` | Additional Go build flags | |
|
||||||
| `-d` | Output directory | `frontend/bindings` |
|
| `-d` | Output directory | `frontend/bindings` |
|
||||||
| `-models` | Models filename | `models` |
|
| `-models` | Models filename | `models` |
|
||||||
| `-internal` | Internal filename | `internal` |
|
| `-internal` | Internal filename | `internal` |
|
||||||
| `-index` | Index filename | `index` |
|
| `-index` | Index filename | `index` |
|
||||||
| `-ts` | Generate TypeScript | `false` |
|
| `-ts` | Generate TypeScript | `false` |
|
||||||
| `-i` | Use TS interfaces | `false` |
|
| `-i` | Use TS interfaces | `false` |
|
||||||
| `-b` | Use bundled runtime | `false` |
|
| `-b` | Use bundled runtime | `false` |
|
||||||
| `-names` | Use names instead of IDs | `false` |
|
| `-names` | Use names instead of IDs | `false` |
|
||||||
| `-noindex` | Skip index files | `false` |
|
| `-noindex` | Skip index files | `false` |
|
||||||
| `-dry` | Dry run | `false` |
|
| `-dry` | Dry run | `false` |
|
||||||
| `-silent` | Silent mode | `false` |
|
| `-silent` | Silent mode | `false` |
|
||||||
| `-v` | Debug output | `false` |
|
| `-v` | Debug output | `false` |
|
||||||
|
|
||||||
### `generate build-assets`
|
### `generate build-assets`
|
||||||
Generates build assets for your application.
|
Generates build assets for your application.
|
||||||
@ -157,18 +155,18 @@ wails3 generate build-assets [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|----------------|---------------------|--------------------|
|
||||||
| `-name` | Project name | |
|
| `-name` | Project name | |
|
||||||
| `-dir` | Output directory | `build` |
|
| `-dir` | Output directory | `build` |
|
||||||
| `-silent` | Suppress output | `false` |
|
| `-silent` | Suppress output | `false` |
|
||||||
| `-company` | Company name | |
|
| `-company` | Company name | |
|
||||||
| `-productname` | Product name | |
|
| `-productname` | Product name | |
|
||||||
| `-description` | Product description | |
|
| `-description` | Product description | |
|
||||||
| `-version` | Product version | |
|
| `-version` | Product version | |
|
||||||
| `-identifier` | Product identifier | `com.wails.[name]` |
|
| `-identifier` | Product identifier | `com.wails.[name]` |
|
||||||
| `-copyright` | Copyright notice | |
|
| `-copyright` | Copyright notice | |
|
||||||
| `-comments` | File comments | |
|
| `-comments` | File comments | |
|
||||||
|
|
||||||
### `generate icons`
|
### `generate icons`
|
||||||
Generates application icons.
|
Generates application icons.
|
||||||
@ -178,13 +176,13 @@ wails3 generate icons [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|------------|------------------------------|-----------------------|
|
||||||
| `-input` | Input PNG file | Required |
|
| `-input` | Input PNG file | Required |
|
||||||
| `-windows` | Windows output filename | |
|
| `-windows` | Windows output filename | |
|
||||||
| `-mac` | macOS output filename | |
|
| `-mac` | macOS output filename | |
|
||||||
| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
|
| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
|
||||||
| `-example` | Generate example icon | `false` |
|
| `-example` | Generate example icon | `false` |
|
||||||
|
|
||||||
### `generate syso`
|
### `generate syso`
|
||||||
Generates Windows .syso file.
|
Generates Windows .syso file.
|
||||||
@ -194,13 +192,13 @@ wails3 generate syso [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|-------------|---------------------------|----------------------------|
|
||||||
| `-manifest` | Path to manifest file | Required |
|
| `-manifest` | Path to manifest file | Required |
|
||||||
| `-icon` | Path to icon file | Required |
|
| `-icon` | Path to icon file | Required |
|
||||||
| `-info` | Path to version info file | |
|
| `-info` | Path to version info file | |
|
||||||
| `-arch` | Target architecture | Current GOARCH |
|
| `-arch` | Target architecture | Current GOARCH |
|
||||||
| `-out` | Output filename | `rsrc_windows_[arch].syso` |
|
| `-out` | Output filename | `rsrc_windows_[arch].syso` |
|
||||||
|
|
||||||
### `generate .desktop`
|
### `generate .desktop`
|
||||||
Generates a Linux .desktop file.
|
Generates a Linux .desktop file.
|
||||||
@ -210,20 +208,20 @@ wails3 generate .desktop [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|------------------|---------------------------|------------------|
|
||||||
| `-name` | Application name | Required |
|
| `-name` | Application name | Required |
|
||||||
| `-exec` | Executable path | Required |
|
| `-exec` | Executable path | Required |
|
||||||
| `-icon` | Icon path | |
|
| `-icon` | Icon path | |
|
||||||
| `-categories` | Application categories | `Utility` |
|
| `-categories` | Application categories | `Utility` |
|
||||||
| `-comment` | Application comment | |
|
| `-comment` | Application comment | |
|
||||||
| `-terminal` | Run in terminal | `false` |
|
| `-terminal` | Run in terminal | `false` |
|
||||||
| `-keywords` | Search keywords | |
|
| `-keywords` | Search keywords | |
|
||||||
| `-version` | Application version | |
|
| `-version` | Application version | |
|
||||||
| `-genericname` | Generic name | |
|
| `-genericname` | Generic name | |
|
||||||
| `-startupnotify` | Show startup notification | `false` |
|
| `-startupnotify` | Show startup notification | `false` |
|
||||||
| `-mimetype` | Supported MIME types | |
|
| `-mimetype` | Supported MIME types | |
|
||||||
| `-output` | Output filename | `[name].desktop` |
|
| `-output` | Output filename | `[name].desktop` |
|
||||||
|
|
||||||
### `generate runtime`
|
### `generate runtime`
|
||||||
Generates the pre-built version of the runtime.
|
Generates the pre-built version of the runtime.
|
||||||
@ -247,13 +245,13 @@ wails3 generate appimage [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|-------------|-----------------------|----------------|
|
||||||
| `-binary` | Path to binary | Required |
|
| `-binary` | Path to binary | Required |
|
||||||
| `-icon` | Path to icon file | Required |
|
| `-icon` | Path to icon file | Required |
|
||||||
| `-desktop` | Path to .desktop file | Required |
|
| `-desktop` | Path to .desktop file | Required |
|
||||||
| `-builddir` | Build directory | Temp directory |
|
| `-builddir` | Build directory | Temp directory |
|
||||||
| `-output` | Output directory | `.` |
|
| `-output` | Output directory | `.` |
|
||||||
|
|
||||||
Base command: `wails3 service`
|
Base command: `wails3 service`
|
||||||
|
|
||||||
@ -269,18 +267,18 @@ wails3 service init [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|------|---------------------|-------------------|
|
||||||
| `-n` | Service name | `example_service` |
|
| `-n` | Service name | `example_service` |
|
||||||
| `-d` | Service description | `Example service` |
|
| `-d` | Service description | `Example service` |
|
||||||
| `-p` | Package name | |
|
| `-p` | Package name | |
|
||||||
| `-o` | Output directory | `.` |
|
| `-o` | Output directory | `.` |
|
||||||
| `-q` | Suppress output | `false` |
|
| `-q` | Suppress output | `false` |
|
||||||
| `-a` | Author name | |
|
| `-a` | Author name | |
|
||||||
| `-v` | Version | |
|
| `-v` | Version | |
|
||||||
| `-w` | Website URL | |
|
| `-w` | Website URL | |
|
||||||
| `-r` | Repository URL | |
|
| `-r` | Repository URL | |
|
||||||
| `-l` | License | |
|
| `-l` | License | |
|
||||||
|
|
||||||
Base command: `wails3 tool`
|
Base command: `wails3 tool`
|
||||||
|
|
||||||
@ -296,9 +294,9 @@ wails3 tool checkport [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|---------|---------------|-------------|
|
||||||
| `-port` | Port to check | `9245` |
|
| `-port` | Port to check | `9245` |
|
||||||
| `-host` | Host to check | `localhost` |
|
| `-host` | Host to check | `localhost` |
|
||||||
|
|
||||||
### `tool watcher`
|
### `tool watcher`
|
||||||
@ -309,11 +307,11 @@ wails3 tool watcher [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|------------|---------------------|----------------------|
|
||||||
| `-config` | Config file path | `./build/config.yml` |
|
| `-config` | Config file path | `./build/config.yml` |
|
||||||
| `-ignore` | Patterns to ignore | |
|
| `-ignore` | Patterns to ignore | |
|
||||||
| `-include` | Patterns to include | |
|
| `-include` | Patterns to include | |
|
||||||
|
|
||||||
### `tool cp`
|
### `tool cp`
|
||||||
Copies files.
|
Copies files.
|
||||||
@ -337,20 +335,20 @@ wails3 tool package [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|-----------|--------------------------------------|----------|
|
||||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||||
| `-name` | Executable name | Required |
|
| `-name` | Executable name | Required |
|
||||||
| `-config` | Config file path | Required |
|
| `-config` | Config file path | Required |
|
||||||
| `-out` | Output directory | `.` |
|
| `-out` | Output directory | `.` |
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|-----------|--------------------------------------|---------|
|
||||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||||
| `-name` | Executable name | `myapp` |
|
| `-name` | Executable name | `myapp` |
|
||||||
| `-config` | Config file path | |
|
| `-config` | Config file path | |
|
||||||
| `-out` | Output directory | `.` |
|
| `-out` | Output directory | `.` |
|
||||||
|
|
||||||
Base command: `wails3 update`
|
Base command: `wails3 update`
|
||||||
|
|
||||||
@ -366,18 +364,18 @@ wails3 update build-assets [flags]
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### Flags
|
#### Flags
|
||||||
| Flag | Description | Default |
|
| Flag | Description | Default |
|
||||||
|------|-------------|---------|
|
|----------------|---------------------|---------|
|
||||||
| `-config` | Config file path | |
|
| `-config` | Config file path | |
|
||||||
| `-dir` | Output directory | `build` |
|
| `-dir` | Output directory | `build` |
|
||||||
| `-silent` | Suppress output | `false` |
|
| `-silent` | Suppress output | `false` |
|
||||||
| `-company` | Company name | |
|
| `-company` | Company name | |
|
||||||
| `-productname` | Product name | |
|
| `-productname` | Product name | |
|
||||||
| `-description` | Product description | |
|
| `-description` | Product description | |
|
||||||
| `-version` | Product version | |
|
| `-version` | Product version | |
|
||||||
| `-identifier` | Product identifier | |
|
| `-identifier` | Product identifier | |
|
||||||
| `-copyright` | Copyright notice | |
|
| `-copyright` | Copyright notice | |
|
||||||
| `-comments` | File comments | |
|
| `-comments` | File comments | |
|
||||||
|
|
||||||
Base command: `wails3`
|
Base command: `wails3`
|
||||||
|
|
||||||
|
119
docs/src/content/docs/guides/single-instance.mdx
Normal file
119
docs/src/content/docs/guides/single-instance.mdx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
---
|
||||||
|
title: Single Instance
|
||||||
|
description: Limiting your app to a single running instance
|
||||||
|
sidebar:
|
||||||
|
order: 40
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Tabs, TabItem } from "@astrojs/starlight/components";
|
||||||
|
|
||||||
|
Single instance locking is a mechanism that prevents 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.
|
||||||
|
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To enable single instance functionality in your app, provide a `SingleInstanceOptions` struct when creating your application:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app := application.New(application.Options{
|
||||||
|
// ... other options ...
|
||||||
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
|
UniqueID: "com.myapp.unique-id",
|
||||||
|
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||||
|
log.Printf("Second instance launched with args: %v", data.Args)
|
||||||
|
log.Printf("Working directory: %s", data.WorkingDir)
|
||||||
|
log.Printf("Additional data: %v", data.AdditionalData)
|
||||||
|
},
|
||||||
|
// Optional: Pass additional data to second instance
|
||||||
|
AdditionalData: map[string]string{
|
||||||
|
"launchtime": time.Now().String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The `SingleInstanceOptions` struct has the following fields:
|
||||||
|
|
||||||
|
- `UniqueID`: A unique identifier for your application. This should be a unique string, typically in reverse domain notation (e.g., "com.company.appname").
|
||||||
|
- `EncryptionKey`: Optional 32-byte array for encrypting data passed between instances using AES-256-GCM. If provided as a non-zero array, all communication between instances will be encrypted.
|
||||||
|
- `OnSecondInstanceLaunch`: A callback function that is called when a second instance of your app is launched. The callback receives a `SecondInstanceData` struct containing:
|
||||||
|
- `Args`: The command line arguments passed to the second instance
|
||||||
|
- `WorkingDir`: The working directory of the second instance
|
||||||
|
- `AdditionalData`: Any additional data passed from the second instance (if provided)
|
||||||
|
- `AdditionalData`: Optional map of string key-value pairs that will be passed to the first instance when subsequent instances are launched
|
||||||
|
|
||||||
|
:::danger[Warning]
|
||||||
|
The Single Instance feature implements an optional encryption protocol using AES-256-GCM. Without encryption enabled,
|
||||||
|
data passed between instances is not secure. When using the single instance feature without encryption,
|
||||||
|
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.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Secure Communication
|
||||||
|
|
||||||
|
To enable secure communication between instances, provide a 32-byte encryption key. This key must be the same for all instances of your application:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Define your encryption key (must be exactly 32 bytes)
|
||||||
|
var encryptionKey = [32]byte{
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||||
|
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
|
||||||
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
|
||||||
|
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the key in SingleInstanceOptions
|
||||||
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
|
UniqueID: "com.myapp.unique-id",
|
||||||
|
// Enable encryption for instance communication
|
||||||
|
EncryptionKey: encryptionKey,
|
||||||
|
// ... other options ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
:::tip[Security Best Practices]
|
||||||
|
- Use a unique key for your application
|
||||||
|
- Store the key securely if loading it from configuration
|
||||||
|
- Do not use the example key shown above - create your own!
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Window Management
|
||||||
|
|
||||||
|
When handling second instance launches, you'll often want to bring your application window to the front. You can do this using the window's `Focus()` method. If your window is minimized, you may need to restore it first:
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
var mainWindow *application.WebviewWindow
|
||||||
|
|
||||||
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
|
// Other options...
|
||||||
|
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||||
|
// Focus the window if needed
|
||||||
|
if mainWindow != nil {
|
||||||
|
mainWindow.Restore()
|
||||||
|
mainWindow.Focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
<Tabs syncKey="platform">
|
||||||
|
<TabItem label="Mac" icon="apple">
|
||||||
|
|
||||||
|
Single instance lock 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)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Windows" icon="seti:windows">
|
||||||
|
|
||||||
|
Single instance lock 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)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
<TabItem label="Linux" icon="linux">
|
||||||
|
|
||||||
|
Single instance lock 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/)
|
||||||
|
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
62
v3/examples/single-instance/README.md
Normal file
62
v3/examples/single-instance/README.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# Single Instance Example
|
||||||
|
|
||||||
|
This example demonstrates the single instance functionality in Wails v3. It shows how to:
|
||||||
|
|
||||||
|
1. Ensure only one instance of your application can run at a time
|
||||||
|
2. Notify the first instance when a second instance is launched
|
||||||
|
3. Pass data between instances
|
||||||
|
4. Handle command line arguments and working directory information from second instances
|
||||||
|
|
||||||
|
## Running the Example
|
||||||
|
|
||||||
|
1. Build and run the application:
|
||||||
|
```bash
|
||||||
|
go build
|
||||||
|
./single-instance
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Try launching a second instance of the application. You'll notice:
|
||||||
|
- The second instance will exit immediately
|
||||||
|
- The first instance will receive and display:
|
||||||
|
- Command line arguments from the second instance
|
||||||
|
- Working directory of the second instance
|
||||||
|
- Additional data passed from the second instance
|
||||||
|
|
||||||
|
3. Check the application logs to see the information received from second instances.
|
||||||
|
|
||||||
|
## Features Demonstrated
|
||||||
|
|
||||||
|
- Setting up single instance lock with a unique identifier
|
||||||
|
- Handling second instance launches through callbacks
|
||||||
|
- Passing custom data between instances
|
||||||
|
- Displaying instance information in a web UI
|
||||||
|
- Cross-platform support (Windows, macOS, Linux)
|
||||||
|
|
||||||
|
## Code Overview
|
||||||
|
|
||||||
|
The example consists of:
|
||||||
|
|
||||||
|
- `main.go`: The main application code demonstrating single instance setup
|
||||||
|
- A simple web UI showing current instance information
|
||||||
|
- Callback handling for second instance launches
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
The application uses the Wails v3 single instance feature:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app := application.New(&application.Options{
|
||||||
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
|
UniqueID: "com.wails.example.single-instance",
|
||||||
|
OnSecondInstance: func(data application.SecondInstanceData) {
|
||||||
|
// Handle second instance launch
|
||||||
|
},
|
||||||
|
AdditionalData: map[string]string{
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The implementation uses platform-specific mechanisms:
|
||||||
|
- Windows: Named mutex and window messages
|
||||||
|
- Unix (Linux/macOS): File locking with flock and signals
|
141
v3/examples/single-instance/assets/index.html
Normal file
141
v3/examples/single-instance/assets/index.html
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
|
<title>Single Instance Demo</title>
|
||||||
|
<script src="/wails/runtime.js" type="module"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c5282;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-box {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.info-box h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #4a5568;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
.info-item {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.info-label {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #4a5568;
|
||||||
|
}
|
||||||
|
.args-list {
|
||||||
|
margin: 5px 0;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
.args-list li {
|
||||||
|
word-break: break-all;
|
||||||
|
margin: 3px 0;
|
||||||
|
}
|
||||||
|
.instructions {
|
||||||
|
background-color: #ebf8ff;
|
||||||
|
border: 1px solid #bee3f8;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 15px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Single Instance Demo</h1>
|
||||||
|
|
||||||
|
<div class="info-box">
|
||||||
|
<h2>Current Instance Information</h2>
|
||||||
|
<div id="instanceInfo">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="instructions">
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<p>Try launching another instance of this application. The first instance will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Receive notification of the second instance launch</li>
|
||||||
|
<li>Get the command line arguments of the second instance</li>
|
||||||
|
<li>Get the working directory of the second instance</li>
|
||||||
|
<li>Receive any additional data passed from the second instance</li>
|
||||||
|
</ul>
|
||||||
|
<p>Check the application logs to see the information received from second instances.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
let lastNotificationTime = "";
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
updateInstanceInfo();
|
||||||
|
// Listen for second instance launch
|
||||||
|
wails.Events.On('secondInstanceLaunched', (info) => {
|
||||||
|
console.log('Second instance launched');
|
||||||
|
lastNotificationTime = new Date().toLocaleString();
|
||||||
|
debugger;
|
||||||
|
updateInstanceInfo(info.data[0]);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update instance information
|
||||||
|
async function updateInstanceInfo(info) {
|
||||||
|
if (!info) {
|
||||||
|
info = await wails.Call.ByName('main.App.GetCurrentInstanceInfo');
|
||||||
|
}
|
||||||
|
const infoDiv = document.getElementById('instanceInfo');
|
||||||
|
infoDiv.innerHTML = '';
|
||||||
|
if (lastNotificationTime) {
|
||||||
|
infoDiv.innerHTML += `<div class="info-item">
|
||||||
|
<span class="info-label">Second Instance Launch Time:</span> ${lastNotificationTime}
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
infoDiv.innerHTML += `
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Arguments:</span>
|
||||||
|
<ul class="args-list">
|
||||||
|
${info.args.map(arg => `<li>${arg}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Working Directory:</span> ${info.workingDir}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (info.additionalData) {
|
||||||
|
infoDiv.innerHTML += `
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">Additional Data:</span>
|
||||||
|
<ul class="args-list">
|
||||||
|
${Object.entries(info.additionalData).map(([key, value]) => `<li>${key}: ${value}</li>`).join('')}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update info when page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
82
v3/examples/single-instance/main.go
Normal file
82
v3/examples/single-instance/main.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"log"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed assets/index.html
|
||||||
|
var assets embed.FS
|
||||||
|
|
||||||
|
type App struct{}
|
||||||
|
|
||||||
|
func (a *App) GetCurrentInstanceInfo() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"args": os.Args,
|
||||||
|
"workingDir": getCurrentWorkingDir(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var encryptionKey = [32]byte{
|
||||||
|
0x1e, 0x1f, 0x1c, 0x1d, 0x1a, 0x1b, 0x18, 0x19,
|
||||||
|
0x16, 0x17, 0x14, 0x15, 0x12, 0x13, 0x10, 0x11,
|
||||||
|
0x0e, 0x0f, 0x0c, 0x0d, 0x0a, 0x0b, 0x08, 0x09,
|
||||||
|
0x06, 0x07, 0x04, 0x05, 0x02, 0x03, 0x00, 0x01,
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
var window *application.WebviewWindow
|
||||||
|
app := application.New(application.Options{
|
||||||
|
Name: "Single Instance Example",
|
||||||
|
LogLevel: slog.LevelDebug,
|
||||||
|
Description: "An example of single instance functionality in Wails v3",
|
||||||
|
Services: []application.Service{
|
||||||
|
application.NewService(&App{}),
|
||||||
|
},
|
||||||
|
SingleInstance: &application.SingleInstanceOptions{
|
||||||
|
UniqueID: "com.wails.example.single-instance",
|
||||||
|
EncryptionKey: encryptionKey,
|
||||||
|
OnSecondInstanceLaunch: func(data application.SecondInstanceData) {
|
||||||
|
if window != nil {
|
||||||
|
window.EmitEvent("secondInstanceLaunched", data)
|
||||||
|
window.Restore()
|
||||||
|
window.Focus()
|
||||||
|
}
|
||||||
|
log.Printf("Second instance launched with args: %v\n", data.Args)
|
||||||
|
log.Printf("Working directory: %s\n", data.WorkingDir)
|
||||||
|
if data.AdditionalData != nil {
|
||||||
|
log.Printf("Additional data: %v\n", data.AdditionalData)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
AdditionalData: map[string]string{
|
||||||
|
"launchtime": time.Now().Local().String(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Assets: application.AssetOptions{
|
||||||
|
Handler: application.BundledAssetFileServer(assets),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
window = app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
|
||||||
|
Title: "Single Instance Demo",
|
||||||
|
Width: 800,
|
||||||
|
Height: 700,
|
||||||
|
URL: "/",
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentWorkingDir() string {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@wailsio/runtime",
|
"name": "@wailsio/runtime",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "3.0.0-alpha.35",
|
"version": "3.0.0-alpha.36",
|
||||||
"description": "Wails Runtime",
|
"description": "Wails Runtime",
|
||||||
"types": "types/index.d.ts",
|
"types": "types/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
@ -174,6 +174,8 @@ export const EventTypes = {
|
|||||||
WindowFileDraggingEntered: "mac:WindowFileDraggingEntered",
|
WindowFileDraggingEntered: "mac:WindowFileDraggingEntered",
|
||||||
WindowFileDraggingPerformed: "mac:WindowFileDraggingPerformed",
|
WindowFileDraggingPerformed: "mac:WindowFileDraggingPerformed",
|
||||||
WindowFileDraggingExited: "mac:WindowFileDraggingExited",
|
WindowFileDraggingExited: "mac:WindowFileDraggingExited",
|
||||||
|
WindowShow: "mac:WindowShow",
|
||||||
|
WindowHide: "mac:WindowHide",
|
||||||
},
|
},
|
||||||
Linux: {
|
Linux: {
|
||||||
SystemThemeChanged: "linux:SystemThemeChanged",
|
SystemThemeChanged: "linux:SystemThemeChanged",
|
||||||
|
@ -174,6 +174,8 @@ export declare const EventTypes: {
|
|||||||
WindowFileDraggingEntered: string,
|
WindowFileDraggingEntered: string,
|
||||||
WindowFileDraggingPerformed: string,
|
WindowFileDraggingPerformed: string,
|
||||||
WindowFileDraggingExited: string,
|
WindowFileDraggingExited: string,
|
||||||
|
WindowShow: string,
|
||||||
|
WindowHide: string,
|
||||||
},
|
},
|
||||||
Linux: {
|
Linux: {
|
||||||
SystemThemeChanged: string,
|
SystemThemeChanged: string,
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@ -174,6 +175,23 @@ func New(appOptions Options) *App {
|
|||||||
result.OnShutdown(appOptions.OnShutdown)
|
result.OnShutdown(appOptions.OnShutdown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize single instance manager if enabled
|
||||||
|
if appOptions.SingleInstance != nil {
|
||||||
|
manager, err := newSingleInstanceManager(result, appOptions.SingleInstance)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, alreadyRunningError) && manager != nil {
|
||||||
|
err = manager.notifyFirstInstance()
|
||||||
|
if err != nil {
|
||||||
|
globalApplication.error("Failed to notify first instance: " + err.Error())
|
||||||
|
}
|
||||||
|
os.Exit(appOptions.SingleInstance.ExitCode)
|
||||||
|
}
|
||||||
|
result.handleFatalError(fmt.Errorf("failed to initialize single instance manager: %w", err))
|
||||||
|
} else {
|
||||||
|
result.singleInstanceManager = manager
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -357,6 +375,9 @@ type App struct {
|
|||||||
// Wails ApplicationEvent Listener related
|
// Wails ApplicationEvent Listener related
|
||||||
wailsEventListenerLock sync.Mutex
|
wailsEventListenerLock sync.Mutex
|
||||||
wailsEventListeners []WailsEventListener
|
wailsEventListeners []WailsEventListener
|
||||||
|
|
||||||
|
// singleInstanceManager handles single instance functionality
|
||||||
|
singleInstanceManager *singleInstanceManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) handleWarning(msg string) {
|
func (a *App) handleWarning(msg string) {
|
||||||
@ -792,6 +813,10 @@ func (a *App) cleanup() {
|
|||||||
a.systemTrays = nil
|
a.systemTrays = nil
|
||||||
a.systemTraysLock.Unlock()
|
a.systemTraysLock.Unlock()
|
||||||
})
|
})
|
||||||
|
// Cleanup single instance manager
|
||||||
|
if a.singleInstanceManager != nil {
|
||||||
|
a.singleInstanceManager.cleanup()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Quit() {
|
func (a *App) Quit() {
|
||||||
|
@ -15,6 +15,7 @@ package application
|
|||||||
extern void registerListener(unsigned int event);
|
extern void registerListener(unsigned int event);
|
||||||
|
|
||||||
#import <Cocoa/Cocoa.h>
|
#import <Cocoa/Cocoa.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
static AppDelegate *appDelegate = nil;
|
static AppDelegate *appDelegate = nil;
|
||||||
|
|
||||||
@ -157,6 +158,13 @@ static const char* serializationNSDictionary(void *dict) {
|
|||||||
|
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void startSingleInstanceListener(const char *uniqueID) {
|
||||||
|
// Convert to NSString
|
||||||
|
NSString *uid = [NSString stringWithUTF8String:uniqueID];
|
||||||
|
[[NSDistributedNotificationCenter defaultCenter] addObserver:appDelegate
|
||||||
|
selector:@selector(handleSecondInstanceNotification:) name:uid object:nil];
|
||||||
|
}
|
||||||
*/
|
*/
|
||||||
import "C"
|
import "C"
|
||||||
import (
|
import (
|
||||||
@ -221,6 +229,11 @@ func (m *macosApp) setApplicationMenu(menu *Menu) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *macosApp) run() error {
|
func (m *macosApp) run() error {
|
||||||
|
if m.parent.options.SingleInstance != nil {
|
||||||
|
cUniqueID := C.CString(m.parent.options.SingleInstance.UniqueID)
|
||||||
|
defer C.free(unsafe.Pointer(cUniqueID))
|
||||||
|
C.startSingleInstanceListener(cUniqueID)
|
||||||
|
}
|
||||||
// Add a hook to the ApplicationDidFinishLaunching event
|
// Add a hook to the ApplicationDidFinishLaunching event
|
||||||
m.parent.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) {
|
m.parent.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) {
|
||||||
C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed))
|
C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed))
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
extern bool hasListeners(unsigned int);
|
extern bool hasListeners(unsigned int);
|
||||||
extern bool shouldQuitApplication();
|
extern bool shouldQuitApplication();
|
||||||
extern void cleanup();
|
extern void cleanup();
|
||||||
|
extern void handleSecondInstanceData(char * message);
|
||||||
@implementation AppDelegate
|
@implementation AppDelegate
|
||||||
- (void)dealloc
|
- (void)dealloc
|
||||||
{
|
{
|
||||||
@ -47,6 +48,15 @@ extern void cleanup();
|
|||||||
|
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
- (void)handleSecondInstanceNotification:(NSNotification *)note;
|
||||||
|
{
|
||||||
|
if (note.userInfo[@"message"] != nil) {
|
||||||
|
NSString *message = note.userInfo[@"message"];
|
||||||
|
const char* utf8Message = message.UTF8String;
|
||||||
|
handleSecondInstanceData((char*)utf8Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GENERATED EVENTS START
|
// GENERATED EVENTS START
|
||||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||||
if( hasListeners(EventApplicationDidBecomeActive) ) {
|
if( hasListeners(EventApplicationDidBecomeActive) ) {
|
||||||
|
@ -117,6 +117,9 @@ type Options struct {
|
|||||||
// The '.' is required
|
// The '.' is required
|
||||||
FileAssociations []string
|
FileAssociations []string
|
||||||
|
|
||||||
|
// SingleInstance options for single instance functionality
|
||||||
|
SingleInstance *SingleInstanceOptions
|
||||||
|
|
||||||
// This blank field ensures types from other packages
|
// This blank field ensures types from other packages
|
||||||
// are never convertible to Options.
|
// are never convertible to Options.
|
||||||
// This property, in turn, improves the accuracy of the binding generator.
|
// This property, in turn, improves the accuracy of the binding generator.
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package application
|
package application
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@ -294,9 +293,7 @@ func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error) {
|
|||||||
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
||||||
|
|
||||||
var result []string
|
var result []string
|
||||||
fmt.Println("Waiting for results:")
|
|
||||||
for filename := range selections {
|
for filename := range selections {
|
||||||
fmt.Println(filename)
|
|
||||||
result = append(result, filename)
|
result = append(result, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
214
v3/pkg/application/single_instance.go
Normal file
214
v3/pkg/application/single_instance.go
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var alreadyRunningError = errors.New("application is already running")
|
||||||
|
var secondInstanceBuffer = make(chan string, 1)
|
||||||
|
var once sync.Once
|
||||||
|
|
||||||
|
// SecondInstanceData contains information about the second instance launch
|
||||||
|
type SecondInstanceData struct {
|
||||||
|
Args []string `json:"args"`
|
||||||
|
WorkingDir string `json:"workingDir"`
|
||||||
|
AdditionalData map[string]string `json:"additionalData,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleInstanceOptions defines options for single instance functionality
|
||||||
|
type SingleInstanceOptions struct {
|
||||||
|
// UniqueID is used to identify the application instance
|
||||||
|
// This should be unique per application, e.g. "com.myapp.myapplication"
|
||||||
|
UniqueID string
|
||||||
|
|
||||||
|
// OnSecondInstanceLaunch is called when a second instance of the application is launched
|
||||||
|
// The callback receives data about the second instance launch
|
||||||
|
OnSecondInstanceLaunch func(data SecondInstanceData)
|
||||||
|
|
||||||
|
// AdditionalData allows passing custom data from second instance to first
|
||||||
|
AdditionalData map[string]string
|
||||||
|
|
||||||
|
// ExitCode is the exit code to use when the second instance exits
|
||||||
|
ExitCode int
|
||||||
|
|
||||||
|
// EncryptionKey is a 32-byte key used for encrypting instance communication
|
||||||
|
// If not provided (zero array), data will be sent unencrypted
|
||||||
|
EncryptionKey [32]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// platformLock is the interface that platform-specific lock implementations must implement
|
||||||
|
type platformLock interface {
|
||||||
|
// acquire attempts to acquire the lock
|
||||||
|
acquire(uniqueID string) error
|
||||||
|
// release releases the lock and cleans up resources
|
||||||
|
release()
|
||||||
|
// notify sends data to the first instance
|
||||||
|
notify(data string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleInstanceManager handles the single instance functionality
|
||||||
|
type singleInstanceManager struct {
|
||||||
|
options *SingleInstanceOptions
|
||||||
|
lock platformLock
|
||||||
|
app *App
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSingleInstanceManager(app *App, options *SingleInstanceOptions) (*singleInstanceManager, error) {
|
||||||
|
if options == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
manager := &singleInstanceManager{
|
||||||
|
options: options,
|
||||||
|
app: app,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch second instance data listener
|
||||||
|
once.Do(func() {
|
||||||
|
go func() {
|
||||||
|
for encryptedData := range secondInstanceBuffer {
|
||||||
|
var secondInstanceData SecondInstanceData
|
||||||
|
var jsonData []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Check if encryption key is non-zero
|
||||||
|
var zeroKey [32]byte
|
||||||
|
if options.EncryptionKey != zeroKey {
|
||||||
|
// Try to decrypt the data
|
||||||
|
jsonData, err = decrypt(options.EncryptionKey, encryptedData)
|
||||||
|
if err != nil {
|
||||||
|
continue // Skip invalid data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
jsonData = []byte(encryptedData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(jsonData, &secondInstanceData); err == nil && manager.options.OnSecondInstanceLaunch != nil {
|
||||||
|
manager.options.OnSecondInstanceLaunch(secondInstanceData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create platform-specific lock
|
||||||
|
lock, err := newPlatformLock(manager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.lock = lock
|
||||||
|
|
||||||
|
// Try to acquire the lock
|
||||||
|
err = lock.acquire(options.UniqueID)
|
||||||
|
if err != nil {
|
||||||
|
return manager, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manager, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *singleInstanceManager) cleanup() {
|
||||||
|
if m == nil || m.lock == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.lock.release()
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt encrypts data using AES-256-GCM
|
||||||
|
func encrypt(key [32]byte, plaintext []byte) (string, error) {
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := make([]byte, 12)
|
||||||
|
if _, err := rand.Read(nonce); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext := aesgcm.Seal(nil, nonce, plaintext, nil)
|
||||||
|
encrypted := append(nonce, ciphertext...)
|
||||||
|
return base64.StdEncoding.EncodeToString(encrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrypt decrypts data using AES-256-GCM
|
||||||
|
func decrypt(key [32]byte, encrypted string) ([]byte, error) {
|
||||||
|
data, err := base64.StdEncoding.DecodeString(encrypted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(data) < 12 {
|
||||||
|
return nil, errors.New("invalid encrypted data")
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(key[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
aesgcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce := data[:12]
|
||||||
|
ciphertext := data[12:]
|
||||||
|
|
||||||
|
return aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// notifyFirstInstance sends data to the first instance of the application
|
||||||
|
func (m *singleInstanceManager) notifyFirstInstance() error {
|
||||||
|
data := SecondInstanceData{
|
||||||
|
Args: os.Args,
|
||||||
|
WorkingDir: getCurrentWorkingDir(),
|
||||||
|
AdditionalData: m.options.AdditionalData,
|
||||||
|
}
|
||||||
|
|
||||||
|
serialized, err := json.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if encryption key is non-zero
|
||||||
|
var zeroKey [32]byte
|
||||||
|
if m.options.EncryptionKey != zeroKey {
|
||||||
|
encrypted, err := encrypt(m.options.EncryptionKey, serialized)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.lock.notify(encrypted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.lock.notify(string(serialized))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCurrentWorkingDir() string {
|
||||||
|
dir, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLockPath returns the path to the lock file for Unix systems
|
||||||
|
func getLockPath(uniqueID string) string {
|
||||||
|
// Use system temp directory
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
lockFileName := uniqueID + ".lock"
|
||||||
|
return filepath.Join(tmpDir, lockFileName)
|
||||||
|
}
|
96
v3/pkg/application/single_instance_darwin.go
Normal file
96
v3/pkg/application/single_instance_darwin.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework Foundation -framework Cocoa
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
static void SendDataToFirstInstance(char *singleInstanceUniqueId, char* message) {
|
||||||
|
[[NSDistributedNotificationCenter defaultCenter]
|
||||||
|
postNotificationName:[NSString stringWithUTF8String:singleInstanceUniqueId]
|
||||||
|
object:nil
|
||||||
|
userInfo:@{@"message": [NSString stringWithUTF8String:message]}
|
||||||
|
deliverImmediately:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
type darwinLock struct {
|
||||||
|
file *os.File
|
||||||
|
uniqueID string
|
||||||
|
manager *singleInstanceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
|
||||||
|
return &darwinLock{
|
||||||
|
manager: manager,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *darwinLock) acquire(uniqueID string) error {
|
||||||
|
l.uniqueID = uniqueID
|
||||||
|
lockFilePath := os.TempDir()
|
||||||
|
lockFileName := uniqueID + ".lock"
|
||||||
|
var err error
|
||||||
|
l.file, err = createLockFile(lockFilePath + "/" + lockFileName)
|
||||||
|
if err != nil {
|
||||||
|
return alreadyRunningError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *darwinLock) release() {
|
||||||
|
if l.file != nil {
|
||||||
|
syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
|
||||||
|
l.file.Close()
|
||||||
|
os.Remove(l.file.Name())
|
||||||
|
l.file = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *darwinLock) notify(data string) error {
|
||||||
|
singleInstanceUniqueId := C.CString(l.uniqueID)
|
||||||
|
defer C.free(unsafe.Pointer(singleInstanceUniqueId))
|
||||||
|
cData := C.CString(data)
|
||||||
|
defer C.free(unsafe.Pointer(cData))
|
||||||
|
|
||||||
|
C.SendDataToFirstInstance(singleInstanceUniqueId, cData)
|
||||||
|
|
||||||
|
os.Exit(l.manager.options.ExitCode)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
//export handleSecondInstanceData
|
||||||
|
func handleSecondInstanceData(secondInstanceMessage *C.char) {
|
||||||
|
message := C.GoString(secondInstanceMessage)
|
||||||
|
secondInstanceBuffer <- message
|
||||||
|
}
|
100
v3/pkg/application/single_instance_linux.go
Normal file
100
v3/pkg/application/single_instance_linux.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/godbus/dbus/v5"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dbusHandler func(string)
|
||||||
|
|
||||||
|
var setup sync.Once
|
||||||
|
|
||||||
|
func (f dbusHandler) SendMessage(message string) *dbus.Error {
|
||||||
|
f(message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type linuxLock struct {
|
||||||
|
file *os.File
|
||||||
|
uniqueID string
|
||||||
|
dbusPath string
|
||||||
|
dbusName string
|
||||||
|
manager *singleInstanceManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
|
||||||
|
return &linuxLock{
|
||||||
|
manager: manager,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *linuxLock) acquire(uniqueID string) error {
|
||||||
|
if uniqueID == "" {
|
||||||
|
return fmt.Errorf("UniqueID is required for single instance lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
|
||||||
|
|
||||||
|
l.dbusName = "org." + id + ".SingleInstance"
|
||||||
|
l.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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
setup.Do(func() {
|
||||||
|
f := dbusHandler(func(message string) {
|
||||||
|
secondInstanceBuffer <- message
|
||||||
|
})
|
||||||
|
|
||||||
|
err := conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName)
|
||||||
|
if err != nil {
|
||||||
|
globalApplication.error(err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if name already taken, try to send args to existing instance, if no success just launch new instance
|
||||||
|
if reply == dbus.RequestNameReplyExists {
|
||||||
|
return alreadyRunningError
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *linuxLock) release() {
|
||||||
|
if l.file != nil {
|
||||||
|
syscall.Flock(int(l.file.Fd()), syscall.LOCK_UN)
|
||||||
|
l.file.Close()
|
||||||
|
os.Remove(l.file.Name())
|
||||||
|
l.file = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *linuxLock) notify(data string) error {
|
||||||
|
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 err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = conn.Object(l.dbusName, dbus.ObjectPath(l.dbusPath)).Call(l.dbusName+".SendMessage", 0, data).Store()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
os.Exit(l.manager.options.ExitCode)
|
||||||
|
return nil
|
||||||
|
}
|
129
v3/pkg/application/single_instance_windows.go
Normal file
129
v3/pkg/application/single_instance_windows.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package application
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
|
)
|
||||||
|
|
||||||
|
type windowsLock struct {
|
||||||
|
handle syscall.Handle
|
||||||
|
uniqueID string
|
||||||
|
msgString string
|
||||||
|
hwnd w32.HWND
|
||||||
|
manager *singleInstanceManager
|
||||||
|
className string
|
||||||
|
windowName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
|
||||||
|
return &windowsLock{
|
||||||
|
manager: manager,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *windowsLock) acquire(uniqueID string) error {
|
||||||
|
if uniqueID == "" {
|
||||||
|
return fmt.Errorf("UniqueID is required for single instance lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
l.uniqueID = uniqueID
|
||||||
|
id := "wails-app-" + uniqueID
|
||||||
|
l.className = id + "-sic"
|
||||||
|
l.windowName = id + "-siw"
|
||||||
|
mutexName := id + "-sim"
|
||||||
|
|
||||||
|
_, err := windows.CreateMutex(nil, false, windows.StringToUTF16Ptr(mutexName))
|
||||||
|
if err != nil {
|
||||||
|
// Find the window
|
||||||
|
return alreadyRunningError
|
||||||
|
} else {
|
||||||
|
l.hwnd = createEventTargetWindow(l.className, l.windowName)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *windowsLock) release() {
|
||||||
|
if l.handle != 0 {
|
||||||
|
syscall.CloseHandle(l.handle)
|
||||||
|
l.handle = 0
|
||||||
|
}
|
||||||
|
if l.hwnd != 0 {
|
||||||
|
w32.DestroyWindow(l.hwnd)
|
||||||
|
l.hwnd = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *windowsLock) notify(data string) error {
|
||||||
|
|
||||||
|
// app is already running
|
||||||
|
hwnd := w32.FindWindowW(windows.StringToUTF16Ptr(l.className), windows.StringToUTF16Ptr(l.windowName))
|
||||||
|
|
||||||
|
if hwnd == 0 {
|
||||||
|
return errors.New("unable to notify other instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
w32.SendMessageToWindow(hwnd, data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createEventTargetWindow(className string, windowName string) w32.HWND {
|
||||||
|
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 = w32.MustStringToUTF16Ptr(className)
|
||||||
|
class.IconSm = 0
|
||||||
|
|
||||||
|
w32.RegisterClassEx(&class)
|
||||||
|
|
||||||
|
// Create hidden message-only window
|
||||||
|
hwnd := w32.CreateWindowEx(
|
||||||
|
0,
|
||||||
|
w32.MustStringToUTF16Ptr(className),
|
||||||
|
w32.MustStringToUTF16Ptr(windowName),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
w32.HWND_MESSAGE,
|
||||||
|
0,
|
||||||
|
w32.GetModuleHandle(""),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
return hwnd
|
||||||
|
}
|
||||||
|
|
||||||
|
func wndProc(hwnd w32.HWND, msg uint32, wparam w32.WPARAM, lparam w32.LPARAM) w32.LRESULT {
|
||||||
|
if msg == w32.WM_COPYDATA {
|
||||||
|
ldata := (*w32.COPYDATASTRUCT)(unsafe.Pointer(lparam))
|
||||||
|
|
||||||
|
if ldata.DwData == w32.WMCOPYDATA_SINGLE_INSTANCE_DATA {
|
||||||
|
serialized := windows.UTF16PtrToString((*uint16)(unsafe.Pointer(ldata.LpData)))
|
||||||
|
secondInstanceBuffer <- serialized
|
||||||
|
}
|
||||||
|
return w32.LRESULT(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return w32.DefWindowProc(hwnd, msg, wparam, lparam)
|
||||||
|
}
|
@ -195,38 +195,43 @@ extern bool hasListeners(unsigned int);
|
|||||||
}
|
}
|
||||||
- (void)windowDidZoom:(NSNotification *)notification {
|
- (void)windowDidZoom:(NSNotification *)notification {
|
||||||
NSWindow *window = notification.object;
|
NSWindow *window = notification.object;
|
||||||
|
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[window delegate];
|
||||||
if ([window isZoomed]) {
|
if ([window isZoomed]) {
|
||||||
if (hasListeners(EventWindowMaximise)) {
|
if (hasListeners(EventWindowMaximise)) {
|
||||||
processWindowEvent(self.windowId, EventWindowMaximise);
|
processWindowEvent(delegate.windowId, EventWindowMaximise);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hasListeners(EventWindowUnMaximise)) {
|
if (hasListeners(EventWindowUnMaximise)) {
|
||||||
processWindowEvent(self.windowId, EventWindowUnMaximise);
|
processWindowEvent(delegate.windowId, EventWindowUnMaximise);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)performZoomIn:(id)sender {
|
- (void)performZoomIn:(id)sender {
|
||||||
[super zoom:sender];
|
[super zoom:sender];
|
||||||
if (hasListeners(EventWindowZoomIn)) {
|
if (hasListeners(EventWindowZoomIn)) {
|
||||||
processWindowEvent(self.windowId, EventWindowZoomIn);
|
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||||
|
processWindowEvent(delegate.windowId, EventWindowZoomIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)performZoomOut:(id)sender {
|
- (void)performZoomOut:(id)sender {
|
||||||
[super zoom:sender];
|
[super zoom:sender];
|
||||||
if (hasListeners(EventWindowZoomOut)) {
|
if (hasListeners(EventWindowZoomOut)) {
|
||||||
processWindowEvent(self.windowId, EventWindowZoomOut);
|
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||||
|
processWindowEvent(delegate.windowId, EventWindowZoomOut);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
- (void)performZoomReset:(id)sender {
|
- (void)performZoomReset:(id)sender {
|
||||||
[self setFrame:[self frameRectForContentRect:[[self screen] visibleFrame]] display:YES];
|
[self setFrame:[self frameRectForContentRect:[[self screen] visibleFrame]] display:YES];
|
||||||
if (hasListeners(EventWindowZoomReset)) {
|
if (hasListeners(EventWindowZoomReset)) {
|
||||||
processWindowEvent(self.windowId, EventWindowZoomReset);
|
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||||
|
processWindowEvent(delegate.windowId, EventWindowZoomReset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
@implementation WebviewWindowDelegate
|
@implementation WebviewWindowDelegate
|
||||||
- (BOOL)windowShouldClose:(NSWindow *)sender {
|
- (BOOL)windowShouldClose:(NSWindow *)sender {
|
||||||
processWindowEvent(self.windowId, EventWindowShouldClose);
|
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||||
|
processWindowEvent(delegate.windowId, EventWindowShouldClose);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
- (void) dealloc {
|
- (void) dealloc {
|
||||||
@ -730,6 +735,18 @@ extern bool hasListeners(unsigned int);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)windowShow:(NSNotification *)notification {
|
||||||
|
if( hasListeners(EventWindowShow) ) {
|
||||||
|
processWindowEvent(self.windowId, EventWindowShow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)windowHide:(NSNotification *)notification {
|
||||||
|
if( hasListeners(EventWindowHide) ) {
|
||||||
|
processWindowEvent(self.windowId, EventWindowHide);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- (void)webView:(WKWebView *)webview didStartProvisionalNavigation:(WKNavigation *)navigation {
|
- (void)webView:(WKWebView *)webview didStartProvisionalNavigation:(WKNavigation *)navigation {
|
||||||
if( hasListeners(EventWebViewDidStartProvisionalNavigation) ) {
|
if( hasListeners(EventWebViewDidStartProvisionalNavigation) ) {
|
||||||
processWindowEvent(self.windowId, EventWebViewDidStartProvisionalNavigation);
|
processWindowEvent(self.windowId, EventWebViewDidStartProvisionalNavigation);
|
||||||
|
@ -34,30 +34,30 @@ type commonEvents struct {
|
|||||||
|
|
||||||
func newCommonEvents() commonEvents {
|
func newCommonEvents() commonEvents {
|
||||||
return commonEvents{
|
return commonEvents{
|
||||||
ApplicationStarted: 1203,
|
ApplicationStarted: 1205,
|
||||||
WindowMaximise: 1204,
|
WindowMaximise: 1206,
|
||||||
WindowUnMaximise: 1205,
|
WindowUnMaximise: 1207,
|
||||||
WindowFullscreen: 1206,
|
WindowFullscreen: 1208,
|
||||||
WindowUnFullscreen: 1207,
|
WindowUnFullscreen: 1209,
|
||||||
WindowRestore: 1208,
|
WindowRestore: 1210,
|
||||||
WindowMinimise: 1209,
|
WindowMinimise: 1211,
|
||||||
WindowUnMinimise: 1210,
|
WindowUnMinimise: 1212,
|
||||||
WindowClosing: 1211,
|
WindowClosing: 1213,
|
||||||
WindowZoom: 1212,
|
WindowZoom: 1214,
|
||||||
WindowZoomIn: 1213,
|
WindowZoomIn: 1215,
|
||||||
WindowZoomOut: 1214,
|
WindowZoomOut: 1216,
|
||||||
WindowZoomReset: 1215,
|
WindowZoomReset: 1217,
|
||||||
WindowFocus: 1216,
|
WindowFocus: 1218,
|
||||||
WindowLostFocus: 1217,
|
WindowLostFocus: 1219,
|
||||||
WindowShow: 1218,
|
WindowShow: 1220,
|
||||||
WindowHide: 1219,
|
WindowHide: 1221,
|
||||||
WindowDPIChanged: 1220,
|
WindowDPIChanged: 1222,
|
||||||
WindowFilesDropped: 1221,
|
WindowFilesDropped: 1223,
|
||||||
WindowRuntimeReady: 1222,
|
WindowRuntimeReady: 1224,
|
||||||
ThemeChanged: 1223,
|
ThemeChanged: 1225,
|
||||||
WindowDidMove: 1224,
|
WindowDidMove: 1226,
|
||||||
WindowDidResize: 1225,
|
WindowDidResize: 1227,
|
||||||
ApplicationOpenedWithFile: 1226,
|
ApplicationOpenedWithFile: 1228,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +218,8 @@ type macEvents struct {
|
|||||||
WindowFileDraggingEntered WindowEventType
|
WindowFileDraggingEntered WindowEventType
|
||||||
WindowFileDraggingPerformed WindowEventType
|
WindowFileDraggingPerformed WindowEventType
|
||||||
WindowFileDraggingExited WindowEventType
|
WindowFileDraggingExited WindowEventType
|
||||||
|
WindowShow WindowEventType
|
||||||
|
WindowHide WindowEventType
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMacEvents() macEvents {
|
func newMacEvents() macEvents {
|
||||||
@ -350,6 +352,8 @@ func newMacEvents() macEvents {
|
|||||||
WindowFileDraggingEntered: 1157,
|
WindowFileDraggingEntered: 1157,
|
||||||
WindowFileDraggingPerformed: 1158,
|
WindowFileDraggingPerformed: 1158,
|
||||||
WindowFileDraggingExited: 1159,
|
WindowFileDraggingExited: 1159,
|
||||||
|
WindowShow: 1160,
|
||||||
|
WindowHide: 1161,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,49 +407,49 @@ type windowsEvents struct {
|
|||||||
|
|
||||||
func newWindowsEvents() windowsEvents {
|
func newWindowsEvents() windowsEvents {
|
||||||
return windowsEvents{
|
return windowsEvents{
|
||||||
SystemThemeChanged: 1160,
|
SystemThemeChanged: 1162,
|
||||||
APMPowerStatusChange: 1161,
|
APMPowerStatusChange: 1163,
|
||||||
APMSuspend: 1162,
|
APMSuspend: 1164,
|
||||||
APMResumeAutomatic: 1163,
|
APMResumeAutomatic: 1165,
|
||||||
APMResumeSuspend: 1164,
|
APMResumeSuspend: 1166,
|
||||||
APMPowerSettingChange: 1165,
|
APMPowerSettingChange: 1167,
|
||||||
ApplicationStarted: 1166,
|
ApplicationStarted: 1168,
|
||||||
WebViewNavigationCompleted: 1167,
|
WebViewNavigationCompleted: 1169,
|
||||||
WindowInactive: 1168,
|
WindowInactive: 1170,
|
||||||
WindowActive: 1169,
|
WindowActive: 1171,
|
||||||
WindowClickActive: 1170,
|
WindowClickActive: 1172,
|
||||||
WindowMaximise: 1171,
|
WindowMaximise: 1173,
|
||||||
WindowUnMaximise: 1172,
|
WindowUnMaximise: 1174,
|
||||||
WindowFullscreen: 1173,
|
WindowFullscreen: 1175,
|
||||||
WindowUnFullscreen: 1174,
|
WindowUnFullscreen: 1176,
|
||||||
WindowRestore: 1175,
|
WindowRestore: 1177,
|
||||||
WindowMinimise: 1176,
|
WindowMinimise: 1178,
|
||||||
WindowUnMinimise: 1177,
|
WindowUnMinimise: 1179,
|
||||||
WindowClosing: 1178,
|
WindowClosing: 1180,
|
||||||
WindowSetFocus: 1179,
|
WindowSetFocus: 1181,
|
||||||
WindowKillFocus: 1180,
|
WindowKillFocus: 1182,
|
||||||
WindowDragDrop: 1181,
|
WindowDragDrop: 1183,
|
||||||
WindowDragEnter: 1182,
|
WindowDragEnter: 1184,
|
||||||
WindowDragLeave: 1183,
|
WindowDragLeave: 1185,
|
||||||
WindowDragOver: 1184,
|
WindowDragOver: 1186,
|
||||||
WindowDidMove: 1185,
|
WindowDidMove: 1187,
|
||||||
WindowDidResize: 1186,
|
WindowDidResize: 1188,
|
||||||
WindowShow: 1187,
|
WindowShow: 1189,
|
||||||
WindowHide: 1188,
|
WindowHide: 1190,
|
||||||
WindowStartMove: 1189,
|
WindowStartMove: 1191,
|
||||||
WindowEndMove: 1190,
|
WindowEndMove: 1192,
|
||||||
WindowStartResize: 1191,
|
WindowStartResize: 1193,
|
||||||
WindowEndResize: 1192,
|
WindowEndResize: 1194,
|
||||||
WindowKeyDown: 1193,
|
WindowKeyDown: 1195,
|
||||||
WindowKeyUp: 1194,
|
WindowKeyUp: 1196,
|
||||||
WindowZOrderChanged: 1195,
|
WindowZOrderChanged: 1197,
|
||||||
WindowPaint: 1196,
|
WindowPaint: 1198,
|
||||||
WindowBackgroundErase: 1197,
|
WindowBackgroundErase: 1199,
|
||||||
WindowNonClientHit: 1198,
|
WindowNonClientHit: 1200,
|
||||||
WindowNonClientMouseDown: 1199,
|
WindowNonClientMouseDown: 1201,
|
||||||
WindowNonClientMouseUp: 1200,
|
WindowNonClientMouseUp: 1202,
|
||||||
WindowNonClientMouseMove: 1201,
|
WindowNonClientMouseMove: 1203,
|
||||||
WindowNonClientMouseLeave: 1202,
|
WindowNonClientMouseLeave: 1204,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,71 +594,73 @@ var eventToJS = map[uint]string{
|
|||||||
1157: "mac:WindowFileDraggingEntered",
|
1157: "mac:WindowFileDraggingEntered",
|
||||||
1158: "mac:WindowFileDraggingPerformed",
|
1158: "mac:WindowFileDraggingPerformed",
|
||||||
1159: "mac:WindowFileDraggingExited",
|
1159: "mac:WindowFileDraggingExited",
|
||||||
1160: "windows:SystemThemeChanged",
|
1160: "mac:WindowShow",
|
||||||
1161: "windows:APMPowerStatusChange",
|
1161: "mac:WindowHide",
|
||||||
1162: "windows:APMSuspend",
|
1162: "windows:SystemThemeChanged",
|
||||||
1163: "windows:APMResumeAutomatic",
|
1163: "windows:APMPowerStatusChange",
|
||||||
1164: "windows:APMResumeSuspend",
|
1164: "windows:APMSuspend",
|
||||||
1165: "windows:APMPowerSettingChange",
|
1165: "windows:APMResumeAutomatic",
|
||||||
1166: "windows:ApplicationStarted",
|
1166: "windows:APMResumeSuspend",
|
||||||
1167: "windows:WebViewNavigationCompleted",
|
1167: "windows:APMPowerSettingChange",
|
||||||
1168: "windows:WindowInactive",
|
1168: "windows:ApplicationStarted",
|
||||||
1169: "windows:WindowActive",
|
1169: "windows:WebViewNavigationCompleted",
|
||||||
1170: "windows:WindowClickActive",
|
1170: "windows:WindowInactive",
|
||||||
1171: "windows:WindowMaximise",
|
1171: "windows:WindowActive",
|
||||||
1172: "windows:WindowUnMaximise",
|
1172: "windows:WindowClickActive",
|
||||||
1173: "windows:WindowFullscreen",
|
1173: "windows:WindowMaximise",
|
||||||
1174: "windows:WindowUnFullscreen",
|
1174: "windows:WindowUnMaximise",
|
||||||
1175: "windows:WindowRestore",
|
1175: "windows:WindowFullscreen",
|
||||||
1176: "windows:WindowMinimise",
|
1176: "windows:WindowUnFullscreen",
|
||||||
1177: "windows:WindowUnMinimise",
|
1177: "windows:WindowRestore",
|
||||||
1178: "windows:WindowClosing",
|
1178: "windows:WindowMinimise",
|
||||||
1179: "windows:WindowSetFocus",
|
1179: "windows:WindowUnMinimise",
|
||||||
1180: "windows:WindowKillFocus",
|
1180: "windows:WindowClosing",
|
||||||
1181: "windows:WindowDragDrop",
|
1181: "windows:WindowSetFocus",
|
||||||
1182: "windows:WindowDragEnter",
|
1182: "windows:WindowKillFocus",
|
||||||
1183: "windows:WindowDragLeave",
|
1183: "windows:WindowDragDrop",
|
||||||
1184: "windows:WindowDragOver",
|
1184: "windows:WindowDragEnter",
|
||||||
1185: "windows:WindowDidMove",
|
1185: "windows:WindowDragLeave",
|
||||||
1186: "windows:WindowDidResize",
|
1186: "windows:WindowDragOver",
|
||||||
1187: "windows:WindowShow",
|
1187: "windows:WindowDidMove",
|
||||||
1188: "windows:WindowHide",
|
1188: "windows:WindowDidResize",
|
||||||
1189: "windows:WindowStartMove",
|
1189: "windows:WindowShow",
|
||||||
1190: "windows:WindowEndMove",
|
1190: "windows:WindowHide",
|
||||||
1191: "windows:WindowStartResize",
|
1191: "windows:WindowStartMove",
|
||||||
1192: "windows:WindowEndResize",
|
1192: "windows:WindowEndMove",
|
||||||
1193: "windows:WindowKeyDown",
|
1193: "windows:WindowStartResize",
|
||||||
1194: "windows:WindowKeyUp",
|
1194: "windows:WindowEndResize",
|
||||||
1195: "windows:WindowZOrderChanged",
|
1195: "windows:WindowKeyDown",
|
||||||
1196: "windows:WindowPaint",
|
1196: "windows:WindowKeyUp",
|
||||||
1197: "windows:WindowBackgroundErase",
|
1197: "windows:WindowZOrderChanged",
|
||||||
1198: "windows:WindowNonClientHit",
|
1198: "windows:WindowPaint",
|
||||||
1199: "windows:WindowNonClientMouseDown",
|
1199: "windows:WindowBackgroundErase",
|
||||||
1200: "windows:WindowNonClientMouseUp",
|
1200: "windows:WindowNonClientHit",
|
||||||
1201: "windows:WindowNonClientMouseMove",
|
1201: "windows:WindowNonClientMouseDown",
|
||||||
1202: "windows:WindowNonClientMouseLeave",
|
1202: "windows:WindowNonClientMouseUp",
|
||||||
1203: "common:ApplicationStarted",
|
1203: "windows:WindowNonClientMouseMove",
|
||||||
1204: "common:WindowMaximise",
|
1204: "windows:WindowNonClientMouseLeave",
|
||||||
1205: "common:WindowUnMaximise",
|
1205: "common:ApplicationStarted",
|
||||||
1206: "common:WindowFullscreen",
|
1206: "common:WindowMaximise",
|
||||||
1207: "common:WindowUnFullscreen",
|
1207: "common:WindowUnMaximise",
|
||||||
1208: "common:WindowRestore",
|
1208: "common:WindowFullscreen",
|
||||||
1209: "common:WindowMinimise",
|
1209: "common:WindowUnFullscreen",
|
||||||
1210: "common:WindowUnMinimise",
|
1210: "common:WindowRestore",
|
||||||
1211: "common:WindowClosing",
|
1211: "common:WindowMinimise",
|
||||||
1212: "common:WindowZoom",
|
1212: "common:WindowUnMinimise",
|
||||||
1213: "common:WindowZoomIn",
|
1213: "common:WindowClosing",
|
||||||
1214: "common:WindowZoomOut",
|
1214: "common:WindowZoom",
|
||||||
1215: "common:WindowZoomReset",
|
1215: "common:WindowZoomIn",
|
||||||
1216: "common:WindowFocus",
|
1216: "common:WindowZoomOut",
|
||||||
1217: "common:WindowLostFocus",
|
1217: "common:WindowZoomReset",
|
||||||
1218: "common:WindowShow",
|
1218: "common:WindowFocus",
|
||||||
1219: "common:WindowHide",
|
1219: "common:WindowLostFocus",
|
||||||
1220: "common:WindowDPIChanged",
|
1220: "common:WindowShow",
|
||||||
1221: "common:WindowFilesDropped",
|
1221: "common:WindowHide",
|
||||||
1222: "common:WindowRuntimeReady",
|
1222: "common:WindowDPIChanged",
|
||||||
1223: "common:ThemeChanged",
|
1223: "common:WindowFilesDropped",
|
||||||
1224: "common:WindowDidMove",
|
1224: "common:WindowRuntimeReady",
|
||||||
1225: "common:WindowDidResize",
|
1225: "common:ThemeChanged",
|
||||||
1226: "common:ApplicationOpenedWithFile",
|
1226: "common:WindowDidMove",
|
||||||
|
1227: "common:WindowDidResize",
|
||||||
|
1228: "common:ApplicationOpenedWithFile",
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,8 @@ mac:WebViewDidCommitNavigation
|
|||||||
mac:WindowFileDraggingEntered
|
mac:WindowFileDraggingEntered
|
||||||
mac:WindowFileDraggingPerformed
|
mac:WindowFileDraggingPerformed
|
||||||
mac:WindowFileDraggingExited
|
mac:WindowFileDraggingExited
|
||||||
|
mac:WindowShow
|
||||||
|
mac:WindowHide
|
||||||
windows:SystemThemeChanged
|
windows:SystemThemeChanged
|
||||||
windows:APMPowerStatusChange
|
windows:APMPowerStatusChange
|
||||||
windows:APMSuspend
|
windows:APMSuspend
|
||||||
|
@ -134,8 +134,10 @@ extern void processWindowEvent(unsigned int, unsigned int);
|
|||||||
#define EventWindowFileDraggingEntered 1157
|
#define EventWindowFileDraggingEntered 1157
|
||||||
#define EventWindowFileDraggingPerformed 1158
|
#define EventWindowFileDraggingPerformed 1158
|
||||||
#define EventWindowFileDraggingExited 1159
|
#define EventWindowFileDraggingExited 1159
|
||||||
|
#define EventWindowShow 1160
|
||||||
|
#define EventWindowHide 1161
|
||||||
|
|
||||||
#define MAX_EVENTS 1160
|
#define MAX_EVENTS 1162
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
@ -25,8 +25,20 @@ var (
|
|||||||
user32 = syscall.NewLazyDLL("user32.dll")
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
getSystemMenu = user32.NewProc("GetSystemMenu")
|
getSystemMenu = user32.NewProc("GetSystemMenu")
|
||||||
enableMenuItem = user32.NewProc("EnableMenuItem")
|
enableMenuItem = user32.NewProc("EnableMenuItem")
|
||||||
|
findWindow = user32.NewProc("FindWindowW")
|
||||||
|
sendMessage = user32.NewProc("SendMessageW")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WMCOPYDATA_SINGLE_INSTANCE_DATA = 1542
|
||||||
|
)
|
||||||
|
|
||||||
|
type COPYDATASTRUCT struct {
|
||||||
|
DwData uintptr
|
||||||
|
CbData uint32
|
||||||
|
LpData uintptr
|
||||||
|
}
|
||||||
|
|
||||||
var Fatal func(error)
|
var Fatal func(error)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -305,3 +317,31 @@ func EnableCloseButton(hwnd HWND) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindWindowW(className, windowName *uint16) HWND {
|
||||||
|
ret, _, _ := findWindow.Call(
|
||||||
|
uintptr(unsafe.Pointer(className)),
|
||||||
|
uintptr(unsafe.Pointer(windowName)),
|
||||||
|
)
|
||||||
|
return HWND(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMessageToWindow(hwnd HWND, msg string) {
|
||||||
|
// Convert data to UTF16 string
|
||||||
|
dataUTF16 := MustStringToUTF16(msg)
|
||||||
|
|
||||||
|
// Prepare COPYDATASTRUCT
|
||||||
|
cds := COPYDATASTRUCT{
|
||||||
|
DwData: WMCOPYDATA_SINGLE_INSTANCE_DATA,
|
||||||
|
CbData: uint32((len(dataUTF16) * 2) + 1), // +1 for null terminator
|
||||||
|
LpData: uintptr(unsafe.Pointer(&dataUTF16[0])),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send message to first instance
|
||||||
|
_, _, _ = procSendMessage.Call(
|
||||||
|
hwnd,
|
||||||
|
WM_COPYDATA,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&cds)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user