mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-15 15:15:51 +01:00
* fix(v3): warm up dialog types in go-json cache to prevent Windows panic Add FileFilter, OpenFileDialogOptions, SaveFileDialogOptions, and MessageDialogOptions to the init() warmup to prevent index out of bounds panic on Windows when these types are first unmarshaled. Fixes goccy/go-json#474 for Wails internal dialog types. * fix(v3): revert goccy/go-json to stdlib encoding/json to fix Windows panic goccy/go-json has a type address calculation bug on Windows that causes index out of bounds panic when decoding user-defined types for the first time. This reverts all runtime usages of goccy/go-json back to stdlib encoding/json. Test and benchmark files are left unchanged. Partially reverts PR #4843.
216 lines
5.2 KiB
Go
216 lines
5.2 KiB
Go
package application
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
|
|
"encoding/json"
|
|
)
|
|
|
|
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() {
|
|
defer handlePanic()
|
|
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)
|
|
}
|