mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 00:29:36 +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)
|
||||
- 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 Single Instance feature by [@leaanthony](https://github.com/leaanthony). Based on the [v2 PR](https://github.com/wailsapp/wails/pull/2951) by @APshenkin.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@ -5,8 +5,6 @@ sidebar:
|
||||
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.
|
||||
|
||||
## Core Commands
|
||||
@ -133,21 +131,21 @@ wails3 generate bindings [flags] [patterns...]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-f` | Additional Go build flags | |
|
||||
| `-d` | Output directory | `frontend/bindings` |
|
||||
| `-models` | Models filename | `models` |
|
||||
| `-internal` | Internal filename | `internal` |
|
||||
| `-index` | Index filename | `index` |
|
||||
| `-ts` | Generate TypeScript | `false` |
|
||||
| `-i` | Use TS interfaces | `false` |
|
||||
| `-b` | Use bundled runtime | `false` |
|
||||
| `-names` | Use names instead of IDs | `false` |
|
||||
| `-noindex` | Skip index files | `false` |
|
||||
| `-dry` | Dry run | `false` |
|
||||
| `-silent` | Silent mode | `false` |
|
||||
| `-v` | Debug output | `false` |
|
||||
| Flag | Description | Default |
|
||||
|-------------|---------------------------|---------------------|
|
||||
| `-f` | Additional Go build flags | |
|
||||
| `-d` | Output directory | `frontend/bindings` |
|
||||
| `-models` | Models filename | `models` |
|
||||
| `-internal` | Internal filename | `internal` |
|
||||
| `-index` | Index filename | `index` |
|
||||
| `-ts` | Generate TypeScript | `false` |
|
||||
| `-i` | Use TS interfaces | `false` |
|
||||
| `-b` | Use bundled runtime | `false` |
|
||||
| `-names` | Use names instead of IDs | `false` |
|
||||
| `-noindex` | Skip index files | `false` |
|
||||
| `-dry` | Dry run | `false` |
|
||||
| `-silent` | Silent mode | `false` |
|
||||
| `-v` | Debug output | `false` |
|
||||
|
||||
### `generate build-assets`
|
||||
Generates build assets for your application.
|
||||
@ -157,18 +155,18 @@ wails3 generate build-assets [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-name` | Project name | |
|
||||
| `-dir` | Output directory | `build` |
|
||||
| `-silent` | Suppress output | `false` |
|
||||
| `-company` | Company name | |
|
||||
| `-productname` | Product name | |
|
||||
| `-description` | Product description | |
|
||||
| `-version` | Product version | |
|
||||
| `-identifier` | Product identifier | `com.wails.[name]` |
|
||||
| `-copyright` | Copyright notice | |
|
||||
| `-comments` | File comments | |
|
||||
| Flag | Description | Default |
|
||||
|----------------|---------------------|--------------------|
|
||||
| `-name` | Project name | |
|
||||
| `-dir` | Output directory | `build` |
|
||||
| `-silent` | Suppress output | `false` |
|
||||
| `-company` | Company name | |
|
||||
| `-productname` | Product name | |
|
||||
| `-description` | Product description | |
|
||||
| `-version` | Product version | |
|
||||
| `-identifier` | Product identifier | `com.wails.[name]` |
|
||||
| `-copyright` | Copyright notice | |
|
||||
| `-comments` | File comments | |
|
||||
|
||||
### `generate icons`
|
||||
Generates application icons.
|
||||
@ -178,13 +176,13 @@ wails3 generate icons [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-input` | Input PNG file | Required |
|
||||
| `-windows` | Windows output filename | |
|
||||
| `-mac` | macOS output filename | |
|
||||
| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
|
||||
| `-example` | Generate example icon | `false` |
|
||||
| Flag | Description | Default |
|
||||
|------------|------------------------------|-----------------------|
|
||||
| `-input` | Input PNG file | Required |
|
||||
| `-windows` | Windows output filename | |
|
||||
| `-mac` | macOS output filename | |
|
||||
| `-sizes` | Icon sizes (comma-separated) | `256,128,64,48,32,16` |
|
||||
| `-example` | Generate example icon | `false` |
|
||||
|
||||
### `generate syso`
|
||||
Generates Windows .syso file.
|
||||
@ -194,13 +192,13 @@ wails3 generate syso [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-manifest` | Path to manifest file | Required |
|
||||
| `-icon` | Path to icon file | Required |
|
||||
| `-info` | Path to version info file | |
|
||||
| `-arch` | Target architecture | Current GOARCH |
|
||||
| `-out` | Output filename | `rsrc_windows_[arch].syso` |
|
||||
| Flag | Description | Default |
|
||||
|-------------|---------------------------|----------------------------|
|
||||
| `-manifest` | Path to manifest file | Required |
|
||||
| `-icon` | Path to icon file | Required |
|
||||
| `-info` | Path to version info file | |
|
||||
| `-arch` | Target architecture | Current GOARCH |
|
||||
| `-out` | Output filename | `rsrc_windows_[arch].syso` |
|
||||
|
||||
### `generate .desktop`
|
||||
Generates a Linux .desktop file.
|
||||
@ -210,20 +208,20 @@ wails3 generate .desktop [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-name` | Application name | Required |
|
||||
| `-exec` | Executable path | Required |
|
||||
| `-icon` | Icon path | |
|
||||
| `-categories` | Application categories | `Utility` |
|
||||
| `-comment` | Application comment | |
|
||||
| `-terminal` | Run in terminal | `false` |
|
||||
| `-keywords` | Search keywords | |
|
||||
| `-version` | Application version | |
|
||||
| `-genericname` | Generic name | |
|
||||
| `-startupnotify` | Show startup notification | `false` |
|
||||
| `-mimetype` | Supported MIME types | |
|
||||
| `-output` | Output filename | `[name].desktop` |
|
||||
| Flag | Description | Default |
|
||||
|------------------|---------------------------|------------------|
|
||||
| `-name` | Application name | Required |
|
||||
| `-exec` | Executable path | Required |
|
||||
| `-icon` | Icon path | |
|
||||
| `-categories` | Application categories | `Utility` |
|
||||
| `-comment` | Application comment | |
|
||||
| `-terminal` | Run in terminal | `false` |
|
||||
| `-keywords` | Search keywords | |
|
||||
| `-version` | Application version | |
|
||||
| `-genericname` | Generic name | |
|
||||
| `-startupnotify` | Show startup notification | `false` |
|
||||
| `-mimetype` | Supported MIME types | |
|
||||
| `-output` | Output filename | `[name].desktop` |
|
||||
|
||||
### `generate runtime`
|
||||
Generates the pre-built version of the runtime.
|
||||
@ -247,13 +245,13 @@ wails3 generate appimage [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-binary` | Path to binary | Required |
|
||||
| `-icon` | Path to icon file | Required |
|
||||
| `-desktop` | Path to .desktop file | Required |
|
||||
| `-builddir` | Build directory | Temp directory |
|
||||
| `-output` | Output directory | `.` |
|
||||
| Flag | Description | Default |
|
||||
|-------------|-----------------------|----------------|
|
||||
| `-binary` | Path to binary | Required |
|
||||
| `-icon` | Path to icon file | Required |
|
||||
| `-desktop` | Path to .desktop file | Required |
|
||||
| `-builddir` | Build directory | Temp directory |
|
||||
| `-output` | Output directory | `.` |
|
||||
|
||||
Base command: `wails3 service`
|
||||
|
||||
@ -269,18 +267,18 @@ wails3 service init [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-n` | Service name | `example_service` |
|
||||
| Flag | Description | Default |
|
||||
|------|---------------------|-------------------|
|
||||
| `-n` | Service name | `example_service` |
|
||||
| `-d` | Service description | `Example service` |
|
||||
| `-p` | Package name | |
|
||||
| `-o` | Output directory | `.` |
|
||||
| `-q` | Suppress output | `false` |
|
||||
| `-a` | Author name | |
|
||||
| `-v` | Version | |
|
||||
| `-w` | Website URL | |
|
||||
| `-r` | Repository URL | |
|
||||
| `-l` | License | |
|
||||
| `-p` | Package name | |
|
||||
| `-o` | Output directory | `.` |
|
||||
| `-q` | Suppress output | `false` |
|
||||
| `-a` | Author name | |
|
||||
| `-v` | Version | |
|
||||
| `-w` | Website URL | |
|
||||
| `-r` | Repository URL | |
|
||||
| `-l` | License | |
|
||||
|
||||
Base command: `wails3 tool`
|
||||
|
||||
@ -296,9 +294,9 @@ wails3 tool checkport [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-port` | Port to check | `9245` |
|
||||
| Flag | Description | Default |
|
||||
|---------|---------------|-------------|
|
||||
| `-port` | Port to check | `9245` |
|
||||
| `-host` | Host to check | `localhost` |
|
||||
|
||||
### `tool watcher`
|
||||
@ -309,11 +307,11 @@ wails3 tool watcher [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-config` | Config file path | `./build/config.yml` |
|
||||
| `-ignore` | Patterns to ignore | |
|
||||
| `-include` | Patterns to include | |
|
||||
| Flag | Description | Default |
|
||||
|------------|---------------------|----------------------|
|
||||
| `-config` | Config file path | `./build/config.yml` |
|
||||
| `-ignore` | Patterns to ignore | |
|
||||
| `-include` | Patterns to include | |
|
||||
|
||||
### `tool cp`
|
||||
Copies files.
|
||||
@ -337,20 +335,20 @@ wails3 tool package [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||
| `-name` | Executable name | Required |
|
||||
| `-config` | Config file path | Required |
|
||||
| `-out` | Output directory | `.` |
|
||||
| Flag | Description | Default |
|
||||
|-----------|--------------------------------------|----------|
|
||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||
| `-name` | Executable name | Required |
|
||||
| `-config` | Config file path | Required |
|
||||
| `-out` | Output directory | `.` |
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||
| `-name` | Executable name | `myapp` |
|
||||
| `-config` | Config file path | |
|
||||
| `-out` | Output directory | `.` |
|
||||
| Flag | Description | Default |
|
||||
|-----------|--------------------------------------|---------|
|
||||
| `-format` | Package format (deb, rpm, archlinux) | `deb` |
|
||||
| `-name` | Executable name | `myapp` |
|
||||
| `-config` | Config file path | |
|
||||
| `-out` | Output directory | `.` |
|
||||
|
||||
Base command: `wails3 update`
|
||||
|
||||
@ -366,18 +364,18 @@ wails3 update build-assets [flags]
|
||||
```
|
||||
|
||||
#### Flags
|
||||
| Flag | Description | Default |
|
||||
|------|-------------|---------|
|
||||
| `-config` | Config file path | |
|
||||
| `-dir` | Output directory | `build` |
|
||||
| `-silent` | Suppress output | `false` |
|
||||
| `-company` | Company name | |
|
||||
| `-productname` | Product name | |
|
||||
| `-description` | Product description | |
|
||||
| `-version` | Product version | |
|
||||
| `-identifier` | Product identifier | |
|
||||
| `-copyright` | Copyright notice | |
|
||||
| `-comments` | File comments | |
|
||||
| Flag | Description | Default |
|
||||
|----------------|---------------------|---------|
|
||||
| `-config` | Config file path | |
|
||||
| `-dir` | Output directory | `build` |
|
||||
| `-silent` | Suppress output | `false` |
|
||||
| `-company` | Company name | |
|
||||
| `-productname` | Product name | |
|
||||
| `-description` | Product description | |
|
||||
| `-version` | Product version | |
|
||||
| `-identifier` | Product identifier | |
|
||||
| `-copyright` | Copyright notice | |
|
||||
| `-comments` | File comments | |
|
||||
|
||||
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",
|
||||
"type": "module",
|
||||
"version": "3.0.0-alpha.35",
|
||||
"version": "3.0.0-alpha.36",
|
||||
"description": "Wails Runtime",
|
||||
"types": "types/index.d.ts",
|
||||
"exports": {
|
||||
|
@ -174,6 +174,8 @@ export const EventTypes = {
|
||||
WindowFileDraggingEntered: "mac:WindowFileDraggingEntered",
|
||||
WindowFileDraggingPerformed: "mac:WindowFileDraggingPerformed",
|
||||
WindowFileDraggingExited: "mac:WindowFileDraggingExited",
|
||||
WindowShow: "mac:WindowShow",
|
||||
WindowHide: "mac:WindowHide",
|
||||
},
|
||||
Linux: {
|
||||
SystemThemeChanged: "linux:SystemThemeChanged",
|
||||
|
@ -174,6 +174,8 @@ export declare const EventTypes: {
|
||||
WindowFileDraggingEntered: string,
|
||||
WindowFileDraggingPerformed: string,
|
||||
WindowFileDraggingExited: string,
|
||||
WindowShow: string,
|
||||
WindowHide: string,
|
||||
},
|
||||
Linux: {
|
||||
SystemThemeChanged: string,
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@ -174,6 +175,23 @@ func New(appOptions Options) *App {
|
||||
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
|
||||
}
|
||||
|
||||
@ -357,6 +375,9 @@ type App struct {
|
||||
// Wails ApplicationEvent Listener related
|
||||
wailsEventListenerLock sync.Mutex
|
||||
wailsEventListeners []WailsEventListener
|
||||
|
||||
// singleInstanceManager handles single instance functionality
|
||||
singleInstanceManager *singleInstanceManager
|
||||
}
|
||||
|
||||
func (a *App) handleWarning(msg string) {
|
||||
@ -792,6 +813,10 @@ func (a *App) cleanup() {
|
||||
a.systemTrays = nil
|
||||
a.systemTraysLock.Unlock()
|
||||
})
|
||||
// Cleanup single instance manager
|
||||
if a.singleInstanceManager != nil {
|
||||
a.singleInstanceManager.cleanup()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) Quit() {
|
||||
|
@ -15,6 +15,7 @@ package application
|
||||
extern void registerListener(unsigned int event);
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static AppDelegate *appDelegate = nil;
|
||||
|
||||
@ -157,6 +158,13 @@ static const char* serializationNSDictionary(void *dict) {
|
||||
|
||||
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 (
|
||||
@ -221,6 +229,11 @@ func (m *macosApp) setApplicationMenu(menu *Menu) {
|
||||
}
|
||||
|
||||
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
|
||||
m.parent.OnApplicationEvent(events.Mac.ApplicationDidFinishLaunching, func(*ApplicationEvent) {
|
||||
C.setApplicationShouldTerminateAfterLastWindowClosed(C.bool(m.parent.options.Mac.ApplicationShouldTerminateAfterLastWindowClosed))
|
||||
|
@ -4,6 +4,7 @@
|
||||
extern bool hasListeners(unsigned int);
|
||||
extern bool shouldQuitApplication();
|
||||
extern void cleanup();
|
||||
extern void handleSecondInstanceData(char * message);
|
||||
@implementation AppDelegate
|
||||
- (void)dealloc
|
||||
{
|
||||
@ -47,6 +48,15 @@ extern void cleanup();
|
||||
|
||||
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
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification {
|
||||
if( hasListeners(EventApplicationDidBecomeActive) ) {
|
||||
|
@ -117,6 +117,9 @@ type Options struct {
|
||||
// The '.' is required
|
||||
FileAssociations []string
|
||||
|
||||
// SingleInstance options for single instance functionality
|
||||
SingleInstance *SingleInstanceOptions
|
||||
|
||||
// This blank field ensures types from other packages
|
||||
// are never convertible to Options.
|
||||
// This property, in turn, improves the accuracy of the binding generator.
|
||||
|
@ -1,7 +1,6 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@ -294,9 +293,7 @@ func (d *OpenFileDialogStruct) PromptForMultipleSelection() ([]string, error) {
|
||||
selections, err := InvokeSyncWithResultAndError(d.impl.show)
|
||||
|
||||
var result []string
|
||||
fmt.Println("Waiting for results:")
|
||||
for filename := range selections {
|
||||
fmt.Println(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 {
|
||||
NSWindow *window = notification.object;
|
||||
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[window delegate];
|
||||
if ([window isZoomed]) {
|
||||
if (hasListeners(EventWindowMaximise)) {
|
||||
processWindowEvent(self.windowId, EventWindowMaximise);
|
||||
processWindowEvent(delegate.windowId, EventWindowMaximise);
|
||||
}
|
||||
} else {
|
||||
if (hasListeners(EventWindowUnMaximise)) {
|
||||
processWindowEvent(self.windowId, EventWindowUnMaximise);
|
||||
processWindowEvent(delegate.windowId, EventWindowUnMaximise);
|
||||
}
|
||||
}
|
||||
}
|
||||
- (void)performZoomIn:(id)sender {
|
||||
[super zoom:sender];
|
||||
if (hasListeners(EventWindowZoomIn)) {
|
||||
processWindowEvent(self.windowId, EventWindowZoomIn);
|
||||
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||
processWindowEvent(delegate.windowId, EventWindowZoomIn);
|
||||
}
|
||||
}
|
||||
- (void)performZoomOut:(id)sender {
|
||||
[super zoom:sender];
|
||||
if (hasListeners(EventWindowZoomOut)) {
|
||||
processWindowEvent(self.windowId, EventWindowZoomOut);
|
||||
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||
processWindowEvent(delegate.windowId, EventWindowZoomOut);
|
||||
}
|
||||
}
|
||||
- (void)performZoomReset:(id)sender {
|
||||
[self setFrame:[self frameRectForContentRect:[[self screen] visibleFrame]] display:YES];
|
||||
if (hasListeners(EventWindowZoomReset)) {
|
||||
processWindowEvent(self.windowId, EventWindowZoomReset);
|
||||
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||
processWindowEvent(delegate.windowId, EventWindowZoomReset);
|
||||
}
|
||||
}
|
||||
@end
|
||||
@implementation WebviewWindowDelegate
|
||||
- (BOOL)windowShouldClose:(NSWindow *)sender {
|
||||
processWindowEvent(self.windowId, EventWindowShouldClose);
|
||||
WebviewWindowDelegate* delegate = (WebviewWindowDelegate*)[sender delegate];
|
||||
processWindowEvent(delegate.windowId, EventWindowShouldClose);
|
||||
return false;
|
||||
}
|
||||
- (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 {
|
||||
if( hasListeners(EventWebViewDidStartProvisionalNavigation) ) {
|
||||
processWindowEvent(self.windowId, EventWebViewDidStartProvisionalNavigation);
|
||||
|
@ -34,30 +34,30 @@ type commonEvents struct {
|
||||
|
||||
func newCommonEvents() commonEvents {
|
||||
return commonEvents{
|
||||
ApplicationStarted: 1203,
|
||||
WindowMaximise: 1204,
|
||||
WindowUnMaximise: 1205,
|
||||
WindowFullscreen: 1206,
|
||||
WindowUnFullscreen: 1207,
|
||||
WindowRestore: 1208,
|
||||
WindowMinimise: 1209,
|
||||
WindowUnMinimise: 1210,
|
||||
WindowClosing: 1211,
|
||||
WindowZoom: 1212,
|
||||
WindowZoomIn: 1213,
|
||||
WindowZoomOut: 1214,
|
||||
WindowZoomReset: 1215,
|
||||
WindowFocus: 1216,
|
||||
WindowLostFocus: 1217,
|
||||
WindowShow: 1218,
|
||||
WindowHide: 1219,
|
||||
WindowDPIChanged: 1220,
|
||||
WindowFilesDropped: 1221,
|
||||
WindowRuntimeReady: 1222,
|
||||
ThemeChanged: 1223,
|
||||
WindowDidMove: 1224,
|
||||
WindowDidResize: 1225,
|
||||
ApplicationOpenedWithFile: 1226,
|
||||
ApplicationStarted: 1205,
|
||||
WindowMaximise: 1206,
|
||||
WindowUnMaximise: 1207,
|
||||
WindowFullscreen: 1208,
|
||||
WindowUnFullscreen: 1209,
|
||||
WindowRestore: 1210,
|
||||
WindowMinimise: 1211,
|
||||
WindowUnMinimise: 1212,
|
||||
WindowClosing: 1213,
|
||||
WindowZoom: 1214,
|
||||
WindowZoomIn: 1215,
|
||||
WindowZoomOut: 1216,
|
||||
WindowZoomReset: 1217,
|
||||
WindowFocus: 1218,
|
||||
WindowLostFocus: 1219,
|
||||
WindowShow: 1220,
|
||||
WindowHide: 1221,
|
||||
WindowDPIChanged: 1222,
|
||||
WindowFilesDropped: 1223,
|
||||
WindowRuntimeReady: 1224,
|
||||
ThemeChanged: 1225,
|
||||
WindowDidMove: 1226,
|
||||
WindowDidResize: 1227,
|
||||
ApplicationOpenedWithFile: 1228,
|
||||
}
|
||||
}
|
||||
|
||||
@ -218,6 +218,8 @@ type macEvents struct {
|
||||
WindowFileDraggingEntered WindowEventType
|
||||
WindowFileDraggingPerformed WindowEventType
|
||||
WindowFileDraggingExited WindowEventType
|
||||
WindowShow WindowEventType
|
||||
WindowHide WindowEventType
|
||||
}
|
||||
|
||||
func newMacEvents() macEvents {
|
||||
@ -350,6 +352,8 @@ func newMacEvents() macEvents {
|
||||
WindowFileDraggingEntered: 1157,
|
||||
WindowFileDraggingPerformed: 1158,
|
||||
WindowFileDraggingExited: 1159,
|
||||
WindowShow: 1160,
|
||||
WindowHide: 1161,
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,49 +407,49 @@ type windowsEvents struct {
|
||||
|
||||
func newWindowsEvents() windowsEvents {
|
||||
return windowsEvents{
|
||||
SystemThemeChanged: 1160,
|
||||
APMPowerStatusChange: 1161,
|
||||
APMSuspend: 1162,
|
||||
APMResumeAutomatic: 1163,
|
||||
APMResumeSuspend: 1164,
|
||||
APMPowerSettingChange: 1165,
|
||||
ApplicationStarted: 1166,
|
||||
WebViewNavigationCompleted: 1167,
|
||||
WindowInactive: 1168,
|
||||
WindowActive: 1169,
|
||||
WindowClickActive: 1170,
|
||||
WindowMaximise: 1171,
|
||||
WindowUnMaximise: 1172,
|
||||
WindowFullscreen: 1173,
|
||||
WindowUnFullscreen: 1174,
|
||||
WindowRestore: 1175,
|
||||
WindowMinimise: 1176,
|
||||
WindowUnMinimise: 1177,
|
||||
WindowClosing: 1178,
|
||||
WindowSetFocus: 1179,
|
||||
WindowKillFocus: 1180,
|
||||
WindowDragDrop: 1181,
|
||||
WindowDragEnter: 1182,
|
||||
WindowDragLeave: 1183,
|
||||
WindowDragOver: 1184,
|
||||
WindowDidMove: 1185,
|
||||
WindowDidResize: 1186,
|
||||
WindowShow: 1187,
|
||||
WindowHide: 1188,
|
||||
WindowStartMove: 1189,
|
||||
WindowEndMove: 1190,
|
||||
WindowStartResize: 1191,
|
||||
WindowEndResize: 1192,
|
||||
WindowKeyDown: 1193,
|
||||
WindowKeyUp: 1194,
|
||||
WindowZOrderChanged: 1195,
|
||||
WindowPaint: 1196,
|
||||
WindowBackgroundErase: 1197,
|
||||
WindowNonClientHit: 1198,
|
||||
WindowNonClientMouseDown: 1199,
|
||||
WindowNonClientMouseUp: 1200,
|
||||
WindowNonClientMouseMove: 1201,
|
||||
WindowNonClientMouseLeave: 1202,
|
||||
SystemThemeChanged: 1162,
|
||||
APMPowerStatusChange: 1163,
|
||||
APMSuspend: 1164,
|
||||
APMResumeAutomatic: 1165,
|
||||
APMResumeSuspend: 1166,
|
||||
APMPowerSettingChange: 1167,
|
||||
ApplicationStarted: 1168,
|
||||
WebViewNavigationCompleted: 1169,
|
||||
WindowInactive: 1170,
|
||||
WindowActive: 1171,
|
||||
WindowClickActive: 1172,
|
||||
WindowMaximise: 1173,
|
||||
WindowUnMaximise: 1174,
|
||||
WindowFullscreen: 1175,
|
||||
WindowUnFullscreen: 1176,
|
||||
WindowRestore: 1177,
|
||||
WindowMinimise: 1178,
|
||||
WindowUnMinimise: 1179,
|
||||
WindowClosing: 1180,
|
||||
WindowSetFocus: 1181,
|
||||
WindowKillFocus: 1182,
|
||||
WindowDragDrop: 1183,
|
||||
WindowDragEnter: 1184,
|
||||
WindowDragLeave: 1185,
|
||||
WindowDragOver: 1186,
|
||||
WindowDidMove: 1187,
|
||||
WindowDidResize: 1188,
|
||||
WindowShow: 1189,
|
||||
WindowHide: 1190,
|
||||
WindowStartMove: 1191,
|
||||
WindowEndMove: 1192,
|
||||
WindowStartResize: 1193,
|
||||
WindowEndResize: 1194,
|
||||
WindowKeyDown: 1195,
|
||||
WindowKeyUp: 1196,
|
||||
WindowZOrderChanged: 1197,
|
||||
WindowPaint: 1198,
|
||||
WindowBackgroundErase: 1199,
|
||||
WindowNonClientHit: 1200,
|
||||
WindowNonClientMouseDown: 1201,
|
||||
WindowNonClientMouseUp: 1202,
|
||||
WindowNonClientMouseMove: 1203,
|
||||
WindowNonClientMouseLeave: 1204,
|
||||
}
|
||||
}
|
||||
|
||||
@ -590,71 +594,73 @@ var eventToJS = map[uint]string{
|
||||
1157: "mac:WindowFileDraggingEntered",
|
||||
1158: "mac:WindowFileDraggingPerformed",
|
||||
1159: "mac:WindowFileDraggingExited",
|
||||
1160: "windows:SystemThemeChanged",
|
||||
1161: "windows:APMPowerStatusChange",
|
||||
1162: "windows:APMSuspend",
|
||||
1163: "windows:APMResumeAutomatic",
|
||||
1164: "windows:APMResumeSuspend",
|
||||
1165: "windows:APMPowerSettingChange",
|
||||
1166: "windows:ApplicationStarted",
|
||||
1167: "windows:WebViewNavigationCompleted",
|
||||
1168: "windows:WindowInactive",
|
||||
1169: "windows:WindowActive",
|
||||
1170: "windows:WindowClickActive",
|
||||
1171: "windows:WindowMaximise",
|
||||
1172: "windows:WindowUnMaximise",
|
||||
1173: "windows:WindowFullscreen",
|
||||
1174: "windows:WindowUnFullscreen",
|
||||
1175: "windows:WindowRestore",
|
||||
1176: "windows:WindowMinimise",
|
||||
1177: "windows:WindowUnMinimise",
|
||||
1178: "windows:WindowClosing",
|
||||
1179: "windows:WindowSetFocus",
|
||||
1180: "windows:WindowKillFocus",
|
||||
1181: "windows:WindowDragDrop",
|
||||
1182: "windows:WindowDragEnter",
|
||||
1183: "windows:WindowDragLeave",
|
||||
1184: "windows:WindowDragOver",
|
||||
1185: "windows:WindowDidMove",
|
||||
1186: "windows:WindowDidResize",
|
||||
1187: "windows:WindowShow",
|
||||
1188: "windows:WindowHide",
|
||||
1189: "windows:WindowStartMove",
|
||||
1190: "windows:WindowEndMove",
|
||||
1191: "windows:WindowStartResize",
|
||||
1192: "windows:WindowEndResize",
|
||||
1193: "windows:WindowKeyDown",
|
||||
1194: "windows:WindowKeyUp",
|
||||
1195: "windows:WindowZOrderChanged",
|
||||
1196: "windows:WindowPaint",
|
||||
1197: "windows:WindowBackgroundErase",
|
||||
1198: "windows:WindowNonClientHit",
|
||||
1199: "windows:WindowNonClientMouseDown",
|
||||
1200: "windows:WindowNonClientMouseUp",
|
||||
1201: "windows:WindowNonClientMouseMove",
|
||||
1202: "windows:WindowNonClientMouseLeave",
|
||||
1203: "common:ApplicationStarted",
|
||||
1204: "common:WindowMaximise",
|
||||
1205: "common:WindowUnMaximise",
|
||||
1206: "common:WindowFullscreen",
|
||||
1207: "common:WindowUnFullscreen",
|
||||
1208: "common:WindowRestore",
|
||||
1209: "common:WindowMinimise",
|
||||
1210: "common:WindowUnMinimise",
|
||||
1211: "common:WindowClosing",
|
||||
1212: "common:WindowZoom",
|
||||
1213: "common:WindowZoomIn",
|
||||
1214: "common:WindowZoomOut",
|
||||
1215: "common:WindowZoomReset",
|
||||
1216: "common:WindowFocus",
|
||||
1217: "common:WindowLostFocus",
|
||||
1218: "common:WindowShow",
|
||||
1219: "common:WindowHide",
|
||||
1220: "common:WindowDPIChanged",
|
||||
1221: "common:WindowFilesDropped",
|
||||
1222: "common:WindowRuntimeReady",
|
||||
1223: "common:ThemeChanged",
|
||||
1224: "common:WindowDidMove",
|
||||
1225: "common:WindowDidResize",
|
||||
1226: "common:ApplicationOpenedWithFile",
|
||||
1160: "mac:WindowShow",
|
||||
1161: "mac:WindowHide",
|
||||
1162: "windows:SystemThemeChanged",
|
||||
1163: "windows:APMPowerStatusChange",
|
||||
1164: "windows:APMSuspend",
|
||||
1165: "windows:APMResumeAutomatic",
|
||||
1166: "windows:APMResumeSuspend",
|
||||
1167: "windows:APMPowerSettingChange",
|
||||
1168: "windows:ApplicationStarted",
|
||||
1169: "windows:WebViewNavigationCompleted",
|
||||
1170: "windows:WindowInactive",
|
||||
1171: "windows:WindowActive",
|
||||
1172: "windows:WindowClickActive",
|
||||
1173: "windows:WindowMaximise",
|
||||
1174: "windows:WindowUnMaximise",
|
||||
1175: "windows:WindowFullscreen",
|
||||
1176: "windows:WindowUnFullscreen",
|
||||
1177: "windows:WindowRestore",
|
||||
1178: "windows:WindowMinimise",
|
||||
1179: "windows:WindowUnMinimise",
|
||||
1180: "windows:WindowClosing",
|
||||
1181: "windows:WindowSetFocus",
|
||||
1182: "windows:WindowKillFocus",
|
||||
1183: "windows:WindowDragDrop",
|
||||
1184: "windows:WindowDragEnter",
|
||||
1185: "windows:WindowDragLeave",
|
||||
1186: "windows:WindowDragOver",
|
||||
1187: "windows:WindowDidMove",
|
||||
1188: "windows:WindowDidResize",
|
||||
1189: "windows:WindowShow",
|
||||
1190: "windows:WindowHide",
|
||||
1191: "windows:WindowStartMove",
|
||||
1192: "windows:WindowEndMove",
|
||||
1193: "windows:WindowStartResize",
|
||||
1194: "windows:WindowEndResize",
|
||||
1195: "windows:WindowKeyDown",
|
||||
1196: "windows:WindowKeyUp",
|
||||
1197: "windows:WindowZOrderChanged",
|
||||
1198: "windows:WindowPaint",
|
||||
1199: "windows:WindowBackgroundErase",
|
||||
1200: "windows:WindowNonClientHit",
|
||||
1201: "windows:WindowNonClientMouseDown",
|
||||
1202: "windows:WindowNonClientMouseUp",
|
||||
1203: "windows:WindowNonClientMouseMove",
|
||||
1204: "windows:WindowNonClientMouseLeave",
|
||||
1205: "common:ApplicationStarted",
|
||||
1206: "common:WindowMaximise",
|
||||
1207: "common:WindowUnMaximise",
|
||||
1208: "common:WindowFullscreen",
|
||||
1209: "common:WindowUnFullscreen",
|
||||
1210: "common:WindowRestore",
|
||||
1211: "common:WindowMinimise",
|
||||
1212: "common:WindowUnMinimise",
|
||||
1213: "common:WindowClosing",
|
||||
1214: "common:WindowZoom",
|
||||
1215: "common:WindowZoomIn",
|
||||
1216: "common:WindowZoomOut",
|
||||
1217: "common:WindowZoomReset",
|
||||
1218: "common:WindowFocus",
|
||||
1219: "common:WindowLostFocus",
|
||||
1220: "common:WindowShow",
|
||||
1221: "common:WindowHide",
|
||||
1222: "common:WindowDPIChanged",
|
||||
1223: "common:WindowFilesDropped",
|
||||
1224: "common:WindowRuntimeReady",
|
||||
1225: "common:ThemeChanged",
|
||||
1226: "common:WindowDidMove",
|
||||
1227: "common:WindowDidResize",
|
||||
1228: "common:ApplicationOpenedWithFile",
|
||||
}
|
||||
|
@ -134,6 +134,8 @@ mac:WebViewDidCommitNavigation
|
||||
mac:WindowFileDraggingEntered
|
||||
mac:WindowFileDraggingPerformed
|
||||
mac:WindowFileDraggingExited
|
||||
mac:WindowShow
|
||||
mac:WindowHide
|
||||
windows:SystemThemeChanged
|
||||
windows:APMPowerStatusChange
|
||||
windows:APMSuspend
|
||||
|
@ -134,8 +134,10 @@ extern void processWindowEvent(unsigned int, unsigned int);
|
||||
#define EventWindowFileDraggingEntered 1157
|
||||
#define EventWindowFileDraggingPerformed 1158
|
||||
#define EventWindowFileDraggingExited 1159
|
||||
#define EventWindowShow 1160
|
||||
#define EventWindowHide 1161
|
||||
|
||||
#define MAX_EVENTS 1160
|
||||
#define MAX_EVENTS 1162
|
||||
|
||||
|
||||
#endif
|
@ -25,8 +25,20 @@ var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
getSystemMenu = user32.NewProc("GetSystemMenu")
|
||||
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)
|
||||
|
||||
const (
|
||||
@ -305,3 +317,31 @@ func EnableCloseButton(hwnd HWND) error {
|
||||
|
||||
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