5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-03 22:33:46 +08:00

[v3] Late service registration and error handling overhaul (#4066)

* Add service registration method

* Fix error handling and formatting in messageprocessor

* Add configurable error handling

* Improve error strings

* Fix service shutdown on macOS

* Add post shutdown hook

* Better fatal errors

* Add startup/shutdown sequence tests

* Improve debug messages

* Update JS runtime

* Update docs

* Update changelog

* Fix log message in clipboard message processor

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Remove panic in RegisterService

* Fix linux tests (hopefully)

* Fix error formatting everywhere

* Fix typo in windows webview

* Tidy example mods

* Set application name in tests

* Fix ubuntu test workflow

* Cleanup template test pipeline

* Fix dev build detection on Go 1.24

* Update template go.mod/sum to Go 1.24

* Remove redundant caching in template tests

* Final format string cleanup

* Fix wails3 tool references

* Fix legacy log calls

* Remove formatJS and simplify format strings

* Fix indirect import

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Fabio Massaioli 2025-02-19 09:27:41 +01:00 committed by GitHub
parent 5059adc561
commit e7c134de4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 2602 additions and 963 deletions

View File

@ -30,7 +30,7 @@ jobs:
fail-fast: false
matrix:
os: [windows-latest, macos-latest, ubuntu-latest]
go-version: [1.23]
go-version: [1.24]
steps:
- name: Checkout code
@ -40,7 +40,7 @@ jobs:
uses: awalsh128/cache-apt-pkgs-action@latest
if: matrix.os == 'ubuntu-latest'
with:
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config xvfb x11-xserver-utils at-spi2-core xdg-desktop-portal-gtk
version: 1.0
- name: Setup Go
@ -66,11 +66,25 @@ jobs:
working-directory: ./v3
run: go test -v ./...
- name: Run tests (!mac)
if: matrix.os != 'macos-latest'
- name: Run tests (windows)
if: matrix.os == 'windows-latest'
working-directory: ./v3
run: go test -v ./...
- name: Run tests (ubuntu)
if: matrix.os == 'ubuntu-latest'
working-directory: ./v3
run: >
xvfb-run --auto-servernum
sh -c '
dbus-update-activation-environment --systemd --all &&
go test -v ./...
'
- name: Typecheck binding generator output
working-directory: ./v3
run: task generator:test:check
test_js:
name: Run JS Tests
needs: check_approval
@ -105,41 +119,23 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
template:
[
svelte,
svelte-ts,
vue,
vue-ts,
react,
react-ts,
preact,
preact-ts,
lit,
lit-ts,
vanilla,
vanilla-ts,
]
go-version: [1.23]
- svelte
- svelte-ts
- vue
- vue-ts
- react
- react-ts
- preact
- preact-ts
- lit
- lit-ts
- vanilla
- vanilla-ts
go-version: [1.24]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache-dependency-path: "v3/go.sum"
- name: Setup Golang caches
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-golang-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-golang-
- name: Install linux dependencies
uses: awalsh128/cache-apt-pkgs-action@latest
if: matrix.os == 'ubuntu-latest'
@ -147,17 +143,28 @@ jobs:
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
version: 1.0
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache-dependency-path: "v3/go.sum"
- name: Install Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Build Wails3 CLI
working-directory: ./v3
run: |
cd ./v3/cmd/wails3
go install
wails3 -help
task install
wails3 doctor
- name: Generate template '${{ matrix.template }}'
run: |
go install github.com/go-task/task/v3/cmd/task@latest
mkdir -p ./test-${{ matrix.template }}
cd ./test-${{ matrix.template }}
wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }}
cd ${{ matrix.template }}
wails3 build
wails3 build

View File

@ -54,6 +54,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `//wails:internal` directive on services and models to allow for types that are exported in Go but not in JS/TS by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
- Add binding generator support for constants of alias type to allow for weakly typed enums by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
- Add support for macOS 15 "Sequoia" to `OSInfo.Branding` for improved OS version detection in [#4065](https://github.com/wailsapp/wails/pull/4065)
- Add `PostShutdown` hook for running custom code after the shutdown process completes by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Add `FatalError` struct to support detection of fatal errors in custom error handlers by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Standardise and document service startup and shutdown order by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Add test harness for application startup/shutdown sequence and service startup/shutdown tests by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Add `RegisterService` method for registering services after the application has been created by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Add `MarshalError` field in application and service options for custom error handling in binding calls by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
### Fixed
@ -81,6 +87,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Suppressed warnings for services that define lifecycle or http methods but no other bound methods by [@fbbdev](https://github.com/fbbdev) in [#4045](https://github.com/wailsapp/wails/pull/4045)
- Fixed non-React templates failing to display Hello World footer when using light system colour scheme by [@marcus-crane](https://github.com/marcus-crane) in [#4056](https://github.com/wailsapp/wails/pull/4056)
- Fixed hidden menu items on macOS by [@leaanthony](https://github.com/leaanthony)
- Fixed handling and formatting of errors in message processors by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
-  Fixed skipped service shutdown when quitting application by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
### Changed
@ -98,6 +106,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Update copyright date to 2025 by [@IanVS](https://github.com/IanVS) in [#4037](https://github.com/wailsapp/wails/pull/4037)
- Add docs for event.Sender by [@IanVS](https://github.com/IanVS) in [#4075](https://github.com/wailsapp/wails/pull/4075)
- Go 1.24 support by [@leaanthony](https://github.com/leaanthony)
- `ServiceStartup` hooks are now invoked when `App.Run` is called, not in `application.New` by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- `ServiceStartup` errors are now returned from `App.Run` instead of terminating the process by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
- Binding and dialog calls from JS now reject with error objects instead of strings by [@fbbdev](https://github.com/fbbdev) in [#4066](https://github.com/wailsapp/wails/pull/4066)
## v3.0.0-alpha.9 - 2025-01-13

View File

@ -448,3 +448,149 @@ const promise = MyService.LongRunningTask("input");
// This will cause the context to be cancelled in the Go method
promise.cancel();
```
### Handling errors
As you may have noticed above, bound methods can return errors, which are handled specially.
When a result field has type `error`, it is omitted by default from the values returned to JS.
When such a field is _non-nil_, the promise rejects with a `RuntimeError` exception
that wraps the Go error message:
```go
func (*MyService) FailingMethod(name string) error {
return fmt.Errorf("Welcome to an imperfect world, %s", name)
}
```
```js
import { MyService } from './bindings/changeme';
try {
await MyService.FailingMethod("CLU")
} catch (err) {
if (err.name === 'RuntimeError') {
console.log(err.message); // Prints 'Welcome to an imperfect world, CLU'
}
}
```
The exception will be an instance of the `Call.RuntimeError` class from the wails runtime,
hence you can also test its type like this:
```js
import { Call } from '@wailsio/runtime';
try {
// ...
} catch (err) {
if (err instanceof Call.RuntimeError) {
// ...
}
}
```
If the Go error value supports JSON marshaling, the exception's `cause` property
will hold the marshaled version of the error:
```go
type ImperfectWorldError struct {
Name string `json:"name"`
}
func (err *ImperfectWorldError) Error() {
return fmt.Sprintf("Welcome to an imperfect world, %s", err.Name)
}
func (*MyService) FailingMethod(name string) error {
return &ImperfectWorldError{
Name: name,
}
}
```
```js
import { MyService } from './bindings/changeme';
try {
await MyService.FailingMethod("CLU")
} catch (err) {
if (err.name === 'RuntimeError') {
console.log(err.cause.name); // Prints 'CLU'
}
}
```
Generally, many Go error values will only have limited or no support for marshaling to JSON.
If you so wish, you can customise the value provided as cause
by specifying either a global or per-service error marshaling function:
```go
app := application.New(application.Options{
MarshalError: func(err error) []byte {
// ...
},
Services: []application.Service{
application.NewServiceWithOptions(&MyService{}, application.ServiceOptions{
MarshalError: func(err error) []byte {
// ...
},
}),
},
})
```
Per-service functions override the global function,
which in turn overrides the default behaviour of using `json.Marshal`.
If a marshaling function returns `nil`, it falls back to the outer function:
per-service functions fall back to the global function,
which in turn falls back to the default behaviour.
:::tip
If you wish to omit the `cause` property on the resulting exception,
let the marshaling function return a falsy JSON value like `[]byte("null")`.
:::
Here's an example marshaling function that unwraps path errors and reports the file path:
```go
app := application.New(application.Options{
MarshalError: func(err error) []byte {
var perr *fs.PathError
if !errors.As(err, &perr) {
// Not a path error, fall back to default handling.
return nil
}
// Marshal path string
path, err := json.Marshal(&perr.Path)
if err != nil {
// String marshaling failed, fall back to default handling.
return nil
}
return []byte(fmt.Sprintf(`{"path":%s}`, path))
},
})
```
:::note
Error marshaling functions are not allowed to fail.
If they are not able to process a given error and return valid JSON,
they should return `nil` and fall back to a more generic handler.
If no strategy succeeds, the exception will not have a `cause` property.
:::
Binding call promises may also reject with a `TypeError`
when the method has been passed the wrong number of arguments,
when the conversion of arguments from JSON to their Go types fails,
or when the conversion of results to JSON fails.
These problems will usually be caught early by the type system.
If your code typechecks but you still get type errors,
it might be that some of your Go types are not supported by the `encoding/json` package:
look for warnings from the binding generator to catch these.
:::caution
If you see a `ReferenceError` complaining about unknown methods,
it could mean that your JS bindings have gotten out of sync with Go code
and must be regenerated.
:::

View File

@ -40,9 +40,9 @@ greeting.
## Registering a Service
To register a service with the application, you need to provide an instance of
the service to the `Services` field of the `application.Options` struct (All
services need to be wrapped by an `application.NewService` call. Here's an
example:
the service to the `Services` field of the `application.Options` struct.
All services need to be wrapped by an `application.NewService` call.
Here's an example:
```go
app := application.New(application.Options{
@ -70,6 +70,25 @@ ServiceOptions has the following fields:
- Name - Specify a custom name for the Service
- Route - A route to bind the Service to the frontend (more on this below)
After the application has been created but not yet started,
you can register more services using the `RegisterService` method.
This is useful when you need to feed a service some value
that is only available after the application has been created.
For example, let's wire application's logger into your own service:
```go
app := application.New(application.Options{})
app.RegisterService(application.NewService(NewMyService(app.Logger)))
// ...
err := app.Run()
```
Services may only be registered before running the application:
`RegisterService` will panic if called after the `Run` method.
## Optional Methods
Services can implement optional methods to hook into the application lifecycle.
@ -98,8 +117,12 @@ func (s *Service) ServiceStartup(ctx context.Context, options application.Servic
This method is called when the application is starting up. You can use it to
initialize resources, set up connections, or perform any necessary setup tasks.
The context is the application context, and the `options` parameter provides
additional information about the service.
The context is the application context that will be canceled upon shutdown,
and the `options` parameter provides additional information about the service.
Services are initialised in the exact order of registration:
first those listed in the `Services` field of the `application.Options` struct,
then those added through the `RegisterService` method.
### ServiceShutdown
@ -110,6 +133,9 @@ func (s *Service) ServiceShutdown() error
This method is called when the application is shutting down. Use it to clean up
resources, close connections, or perform any necessary cleanup tasks.
Services are shut down in reverse registration order.
The application context will be canceled before `ServiceShutdown` is called.
### ServeHTTP
```go

View File

@ -1,17 +1,17 @@
module changeme
go 1.23.4
go 1.24.0
require github.com/wailsapp/wails/v3 v3.0.0-alpha.0
require (
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.1 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
@ -22,26 +22,26 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

View File

@ -18,6 +18,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
@ -39,6 +40,7 @@ github.com/go-git/go-git/v5 v5.3.0 h1:8WKMtJR2j8RntEXR/uvTKagfEt4GYlwQ7mntE4+0GW
github.com/go-git/go-git/v5 v5.3.0/go.mod h1:xdX4bWJ48aOrdhnl2XqHYstHbbp6+LFS4r4X+lNVprw=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -76,14 +78,17 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.0 h1:fzEj70K1L58uyoePQxKe+ezDZJ5pybiWGdA0JeFvvyw=
github.com/lmittmann/tint v1.0.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@ -97,6 +102,7 @@ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjY
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -114,6 +120,7 @@ github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUz
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
@ -145,9 +152,11 @@ golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -157,6 +166,7 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -180,6 +190,7 @@ golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=

View File

@ -1,21 +1,21 @@
module changeme
go 1.23.4
go 1.24.0
require github.com/wailsapp/wails/v3 v3.0.0-alpha.7
require (
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.4 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.5.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.0 // indirect
github.com/ebitengine/purego v0.4.0-alpha.4 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.1 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
@ -24,26 +24,26 @@ require (
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect
github.com/lmittmann/tint v1.0.4 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/tools v0.29.0 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

View File

@ -8,6 +8,7 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/ProtonMail/go-crypto v1.1.4/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@ -19,14 +20,17 @@ github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUK
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/cyphar/filepath-securejoin v0.4.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -43,6 +47,7 @@ github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-git/go-git/v5 v5.13.1/go.mod h1:qryJB4cSBoq3FRoBRf5A77joojuBcmPJ0qu3XXXVixc=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@ -73,12 +78,15 @@ github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
@ -87,8 +95,10 @@ github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3ev
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pjbgf/sha1cd v0.3.1/go.mod h1:Y8t7jSB/dEI/lQE04A1HVKteqjj9bX5O4+Cex0TCu8s=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -100,6 +110,7 @@ github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDN
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
@ -107,6 +118,7 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@ -128,14 +140,17 @@ golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -147,12 +162,14 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -174,6 +191,7 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -197,6 +215,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -19,12 +19,15 @@ type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
}
type service struct {
Route string
Handler http.Handler
}
type AssetServer struct {
options *Options
handler http.Handler
services map[string]http.Handler
options *Options
handler http.Handler
services []service
assetServerWebView
}
@ -112,11 +115,11 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
userHandler.ServeHTTP(wrapped, req)
default:
// Check if the path matches the keys in the services map
for route, handler := range a.services {
if strings.HasPrefix(reqPath, route) {
req.URL.Path = strings.TrimPrefix(reqPath, route)
handler.ServeHTTP(rw, req)
// Check if the path matches a service route
for _, svc := range a.services {
if strings.HasPrefix(reqPath, svc.Route) {
req.URL.Path = strings.TrimPrefix(reqPath, svc.Route)
svc.Handler.ServeHTTP(rw, req)
return
}
}
@ -126,11 +129,8 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
}
}
func (a *AssetServer) AttachServiceHandler(prefix string, handler http.Handler) {
if a.services == nil {
a.services = make(map[string]http.Handler)
}
a.services[prefix] = handler
func (a *AssetServer) AttachServiceHandler(route string, handler http.Handler) {
a.services = append(a.services, service{route, handler})
}
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,8 +2,10 @@ package buildinfo
import (
"fmt"
"github.com/samber/lo"
"runtime/debug"
"slices"
"github.com/samber/lo"
)
type Info struct {
@ -29,7 +31,9 @@ func Get() (*Info, error) {
return setting.Key, setting.Value
})
result.Version = BuildInfo.Main.Version
result.Development = BuildInfo.Main.Version == "(devel)"
result.Development = -1 != slices.IndexFunc(BuildInfo.Settings, func(setting debug.BuildSetting) bool {
return setting.Key == "vcs" && setting.Value == "git"
})
return &result, nil

View File

@ -2,14 +2,15 @@ package commands
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/debug"
"github.com/wailsapp/wails/v3/internal/github"
"github.com/wailsapp/wails/v3/internal/term"
"github.com/wailsapp/wails/v3/internal/version"
"os"
"os/exec"
"path/filepath"
)
type UpdateCLIOptions struct {
@ -31,9 +32,9 @@ func UpdateCLI(options *UpdateCLIOptions) error {
v3Path := filepath.ToSlash(debug.LocalModulePath + "/v3")
term.Println("This Wails CLI has been installed from source. To update to the latest stable release, run the following commands in the `" + v3Path + "` directory:")
term.Println(" - git pull")
term.Println(" - go install")
term.Println(" - wails3 task install")
term.Println("")
term.Println("If you want to install the latest release, please run `wails update cli -latest`")
term.Println("If you want to install the latest release, please run `wails3 update cli -latest`")
return nil
}
@ -69,7 +70,7 @@ func UpdateCLI(options *UpdateCLIOptions) error {
if err != nil {
pterm.Println("")
pterm.Println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:")
pterm.Println(" wails update -pre")
pterm.Println(" wails3 update cli -pre")
return nil
}
}
@ -142,7 +143,7 @@ func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentV
// Compare
if !success {
pterm.Println("Error: The requested version is lower than the current version.")
pterm.Printf("If this is what you really want to do, use `wails update -version %s`\n", targetVersionString)
pterm.Printf("If this is what you really want to do, use `wails3 update cli -version %s`\n", targetVersionString)
return nil
}

View File

@ -220,7 +220,7 @@ func (info *ServiceInfo) collectMethod(method *types.Func) *ServiceMethodInfo {
}
fqn := path + "." + obj.Name() + "." + method.Name()
id, _ := hash.Fnv(fqn)
id := hash.Fnv(fqn)
methodInfo := &ServiceMethodInfo{
MethodInfo: collector.Method(method).Collect(),
@ -265,6 +265,7 @@ func (info *ServiceInfo) collectMethod(method *types.Func) *ServiceMethodInfo {
idValue,
)
methodInfo.ID = strconv.FormatUint(idValue, 10)
methodIdFound = true
}
}
}

View File

@ -2,11 +2,8 @@ package hash
import "hash/fnv"
func Fnv(s string) (uint32, error) {
func Fnv(s string) uint32 {
h := fnv.New32a()
_, err := h.Write([]byte(s))
if err != nil {
return 0, err
}
return h.Sum32(), nil
_, _ = h.Write([]byte(s)) // Hash implementations never return errors (see https://pkg.go.dev/hash#Hash)
return h.Sum32()
}

View File

@ -12,10 +12,12 @@ tasks:
- npm install
test:
dir: desktop/@wailsio/runtime
cmds:
- npx vitest run
update:
dir: desktop/@wailsio/runtime
cmds:
- npx npm-check-updates -u

View File

@ -1,12 +1,12 @@
{
"name": "@wailsio/runtime",
"version": "3.0.0-alpha.39",
"version": "3.0.0-alpha.55",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@wailsio/runtime",
"version": "3.0.0-alpha.39",
"version": "3.0.0-alpha.55",
"license": "MIT",
"devDependencies": {
"rimraf": "^5.0.5",

View File

@ -48,7 +48,17 @@ function generateID() {
function resultHandler(id, data, isJSON) {
const promiseHandler = getAndDeleteResponse(id);
if (promiseHandler) {
promiseHandler.resolve(isJSON ? JSON.parse(data) : data);
if (!data) {
promiseHandler.resolve();
} else if (!isJSON) {
promiseHandler.resolve(data);
} else {
try {
promiseHandler.resolve(JSON.parse(data));
} catch (err) {
promiseHandler.reject(new TypeError("could not parse result: " + err.message, { cause: err }));
}
}
}
}
@ -56,14 +66,48 @@ function resultHandler(id, data, isJSON) {
* Handles the error from a call request.
*
* @param {string} id - The id of the promise handler.
* @param {string} message - The error message to reject the promise handler with.
* @param {string} data - The error data to reject the promise handler with.
* @param {boolean} isJSON - Indicates whether the data is JSON or not.
*
* @return {void}
*/
function errorHandler(id, message) {
function errorHandler(id, data, isJSON) {
const promiseHandler = getAndDeleteResponse(id);
if (promiseHandler) {
promiseHandler.reject(message);
if (!isJSON) {
promiseHandler.reject(new Error(data));
} else {
let error;
try {
error = JSON.parse(data);
} catch (err) {
promiseHandler.reject(new TypeError("could not parse error: " + err.message, { cause: err }));
return;
}
let options = {};
if (error.cause) {
options.cause = error.cause;
}
let exception;
switch (error.kind) {
case "ReferenceError":
exception = new ReferenceError(error.message, options);
break;
case "TypeError":
exception = new TypeError(error.message, options);
break;
case "RuntimeError":
exception = new RuntimeError(error.message, options);
break;
default:
exception = new Error(error.message, options);
break;
}
promiseHandler.reject(exception);
}
}
}
@ -81,30 +125,59 @@ function getAndDeleteResponse(id) {
}
/**
* Executes a call using the provided type and options.
* Collects all required information for a binding call.
*
* @param {string|number} type - The type of call to execute.
* @param {Object} [options={}] - Additional options for the call.
* @return {Promise} - A promise that will be resolved or rejected based on the result of the call. It also has a cancel method to cancel a long running request.
* @typedef {Object} CallOptions
* @property {number} [methodID] - The numeric ID of the bound method to call.
* @property {string} [methodName] - The fully qualified name of the bound method to call.
* @property {any[]} args - Arguments to be passed into the bound method.
*/
function callBinding(type, options = {}) {
/**
* Exception class that will be thrown in case the bound method returns an error.
* The value of the {@link RuntimeError#name} property is "RuntimeError".
*/
export class RuntimeError extends Error {
/**
* Constructs a new RuntimeError instance.
*
* @param {string} message - The error message.
* @param {any[]} args - Optional arguments for the Error constructor.
*/
constructor(message, ...args) {
super(message, ...args);
this.name = "RuntimeError";
}
}
/**
* Call a bound method according to the given call options.
*
* In case of failure, the returned promise will reject with an exception
* among ReferenceError (unknown method), TypeError (wrong argument count or type),
* {@link RuntimeError} (method returned an error), or other (network or internal errors).
* The exception might have a "cause" field with the value returned
* by the application- or service-level error marshaling functions.
*
* @param {CallOptions} options - A method call descriptor.
* @returns {Promise<any>} - The result of the call.
*/
export function Call(options) {
const id = generateID();
const doCancel = () => { return cancelCall(type, {"call-id": id}) };
let queuedCancel = false, callRunning = false;
let p = new Promise((resolve, reject) => {
options["call-id"] = id;
callResponses.set(id, { resolve, reject });
call(type, options).
then((_) => {
callRunning = true;
if (queuedCancel) {
return doCancel();
}
}).
catch((error) => {
reject(error);
callResponses.delete(id);
});
call(CallBinding, options).then((_) => {
callRunning = true;
if (queuedCancel) {
return doCancel();
}
}).catch((error) => {
reject(error);
callResponses.delete(id);
});
});
p.cancel = () => {
if (callRunning) {
@ -118,57 +191,31 @@ function callBinding(type, options = {}) {
}
/**
* Call method.
*
* @param {Object} options - The options for the method.
* @returns {Object} - The result of the call.
*/
export function Call(options) {
return callBinding(CallBinding, options);
}
/**
* Executes a method by name.
* Calls a bound method by name with the specified arguments.
* See {@link Call} for details.
*
* @param {string} methodName - The name of the method in the format 'package.struct.method'.
* @param {...*} args - The arguments to pass to the method.
* @throws {Error} If the name is not a string or is not in the correct format.
* @returns {*} The result of the method execution.
* @param {any[]} args - The arguments to pass to the method.
* @returns {Promise<any>} The result of the method call.
*/
export function ByName(methodName, ...args) {
return callBinding(CallBinding, {
return Call({
methodName,
args
});
}
/**
* Calls a method by its ID with the specified arguments.
* Calls a method by its numeric ID with the specified arguments.
* See {@link Call} for details.
*
* @param {number} methodID - The ID of the method to call.
* @param {...*} args - The arguments to pass to the method.
* @return {*} - The result of the method call.
* @param {any[]} args - The arguments to pass to the method.
* @return {Promise<any>} - The result of the method call.
*/
export function ByID(methodID, ...args) {
return callBinding(CallBinding, {
return Call({
methodID,
args
});
}
/**
* Calls a method on a plugin.
*
* @param {string} pluginName - The name of the plugin.
* @param {string} methodName - The name of the method to call.
* @param {...*} args - The arguments to pass to the method.
* @returns {*} - The result of the method call.
*/
export function Plugin(pluginName, methodName, ...args) {
return callBinding(CallBinding, {
packageName: "wails-plugins",
structName: pluginName,
methodName,
args
});
}

View File

@ -135,12 +135,12 @@ function dialog(type, options = {}) {
function dialogResultCallback(id, data, isJSON) {
let p = dialogResponses.get(id);
if (p) {
dialogResponses.delete(id);
if (isJSON) {
p.resolve(JSON.parse(data));
} else {
p.resolve(data);
}
dialogResponses.delete(id);
}
}
@ -155,8 +155,8 @@ function dialogResultCallback(id, data, isJSON) {
function dialogErrorCallback(id, message) {
let p = dialogResponses.get(id);
if (p) {
p.reject(message);
dialogResponses.delete(id);
p.reject(new Error(message));
}
}

View File

@ -45,7 +45,7 @@ export function newRuntimeCaller(object, windowName) {
/**
* Creates a new runtime caller with specified ID.
*
* @param {object} object - The object to invoke the method on.
* @param {number} object - The object to invoke the method on.
* @param {string} windowName - The name of the window.
* @return {Function} - The new runtime caller function.
*/
@ -57,8 +57,15 @@ export function newRuntimeCallerWithID(object, windowName) {
function runtimeCall(method, windowName, args) {
return runtimeCallWithID(null, method, windowName, args);
}
async function runtimeCallWithID(objectID, method, windowName, args) {
let url = new URL(runtimeURL);
if( method ) {
if (objectID != null) {
url.searchParams.append("object", objectID);
}
if (method != null) {
url.searchParams.append("method", method);
}
let fetchOptions = {
@ -72,52 +79,14 @@ function runtimeCall(method, windowName, args) {
}
fetchOptions.headers["x-wails-client-id"] = clientId;
return new Promise((resolve, reject) => {
fetch(url, fetchOptions)
.then(response => {
if (response.ok) {
// check content type
if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) {
return response.json();
} else {
return response.text();
}
}
reject(Error(response.statusText));
})
.then(data => resolve(data))
.catch(error => reject(error));
});
}
let response = await fetch(url, fetchOptions);
if (!response.ok) {
throw new Error(await response.text());
}
function runtimeCallWithID(objectID, method, windowName, args) {
let url = new URL(runtimeURL);
url.searchParams.append("object", objectID);
url.searchParams.append("method", method);
let fetchOptions = {
headers: {},
};
if (windowName) {
fetchOptions.headers["x-wails-window-name"] = windowName;
if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) {
return response.json();
} else {
return response.text();
}
if (args) {
url.searchParams.append("args", JSON.stringify(args));
}
fetchOptions.headers["x-wails-client-id"] = clientId;
return new Promise((resolve, reject) => {
fetch(url, fetchOptions)
.then(response => {
if (response.ok) {
// check content type
if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) {
return response.json();
} else {
return response.text();
}
}
reject(Error(response.statusText));
})
.then(data => resolve(data))
.catch(error => reject(error));
});
}

View File

@ -1,33 +1,69 @@
/**
* Call method.
* Call a bound method according to the given call options.
*
* @param {Object} options - The options for the method.
* @returns {Object} - The result of the call.
* In case of failure, the returned promise will reject with an exception
* among ReferenceError (unknown method), TypeError (wrong argument count or type),
* {@link RuntimeError} (method returned an error), or other (network or internal errors).
* The exception might have a "cause" field with the value returned
* by the application- or service-level error marshaling functions.
*
* @param {CallOptions} options - A method call descriptor.
* @returns {Promise<any>} - The result of the call.
*/
export function Call(options: any): any;
export function Call(options: CallOptions): Promise<any>;
/**
* Executes a method by name.
* Calls a bound method by name with the specified arguments.
* See {@link Call} for details.
*
* @param {string} methodName - The name of the method in the format 'package.struct.method'.
* @param {...*} args - The arguments to pass to the method.
* @throws {Error} If the name is not a string or is not in the correct format.
* @returns {*} The result of the method execution.
* @param {any[]} args - The arguments to pass to the method.
* @returns {Promise<any>} The result of the method call.
*/
export function ByName(methodName: string, ...args: any[]): any;
export function ByName(methodName: string, ...args: any[]): Promise<any>;
/**
* Calls a method by its ID with the specified arguments.
* Calls a method by its numeric ID with the specified arguments.
* See {@link Call} for details.
*
* @param {number} methodID - The ID of the method to call.
* @param {...*} args - The arguments to pass to the method.
* @return {*} - The result of the method call.
* @param {any[]} args - The arguments to pass to the method.
* @return {Promise<any>} - The result of the method call.
*/
export function ByID(methodID: number, ...args: any[]): any;
export function ByID(methodID: number, ...args: any[]): Promise<any>;
/**
* Calls a method on a plugin.
* Collects all required information for a binding call.
*
* @param {string} pluginName - The name of the plugin.
* @param {string} methodName - The name of the method to call.
* @param {...*} args - The arguments to pass to the method.
* @returns {*} - The result of the method call.
* @typedef {Object} CallOptions
* @property {number} [methodID] - The numeric ID of the bound method to call.
* @property {string} [methodName] - The fully qualified name of the bound method to call.
* @property {any[]} args - Arguments to be passed into the bound method.
*/
export function Plugin(pluginName: string, methodName: string, ...args: any[]): any;
/**
* Exception class that will be thrown in case the bound method returns an error.
* The value of the {@link RuntimeError#name} property is "RuntimeError".
*/
export class RuntimeError extends Error {
/**
* Constructs a new RuntimeError instance.
*
* @param {string} message - The error message.
* @param {any[]} args - Optional arguments for the Error constructor.
*/
constructor(message: string, ...args: any[]);
}
/**
* Collects all required information for a binding call.
*/
export type CallOptions = {
/**
* - The numeric ID of the bound method to call.
*/
methodID?: number;
/**
* - The fully qualified name of the bound method to call.
*/
methodName?: string;
/**
* - Arguments to be passed into the bound method.
*/
args: any[];
};

View File

@ -9,11 +9,11 @@ export function newRuntimeCaller(object: any, windowName: string): Function;
/**
* Creates a new runtime caller with specified ID.
*
* @param {object} object - The object to invoke the method on.
* @param {number} object - The object to invoke the method on.
* @param {string} windowName - The name of the window.
* @return {Function} - The new runtime caller function.
*/
export function newRuntimeCallerWithID(object: object, windowName: string): Function;
export function newRuntimeCallerWithID(object: number, windowName: string): Function;
export namespace objectNames {
let Call: number;
let Clipboard: number;

View File

@ -1,19 +1,51 @@
module changeme
go 1.21
go 1.24
require github.com/wailsapp/wails/v3 {{.WailsVersion}}
require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/leaanthony/slicer v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/samber/lo v1.37.0 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect
golang.org/x/net v0.7.0 // indirect
dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-git/go-git/v5 v5.13.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.2 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.49.1 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/skeema/knownhosts v1.3.1 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
{{if gt (len .LocalModulePath) 0}}
{{if .LocalModulePath}}
replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3
{{end}}

View File

@ -1,33 +1,146 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.1.5 h1:eoAQfK2dwL+tFSFpr7TbOaPNUbPiJj4fLYwwGE1FQO4=
github.com/ProtonMail/go-crypto v1.1.5/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.13.2 h1:7O7xvsK7K+rZPKW6AQR1YyNhfywkv7B8/FsP3ki6Zv0=
github.com/go-git/go-git/v5 v5.13.2/go.mod h1:hWdW5P4YZRjmpGHwRH2v3zkWcNl6HeXaXQEMGb3NJ9A=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/lmittmann/tint v1.0.7 h1:D/0OqWZ0YOGZ6AyC+5Y2kD8PBEzBk6rFHVSfOqCkF9Y=
github.com/lmittmann/tint v1.0.7/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -7,11 +7,11 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"os"
"runtime"
"slices"
"strconv"
"strings"
"sync"
@ -103,17 +103,17 @@ func New(appOptions Options) *App {
case "/wails/capabilities":
err := assetserver.ServeFile(rw, path, globalApplication.capabilities.AsBytes())
if err != nil {
result.handleFatalError(fmt.Errorf("unable to serve capabilities: %s", err.Error()))
result.fatal("unable to serve capabilities: %w", err)
}
case "/wails/flags":
updatedOptions := result.impl.GetFlags(appOptions)
flags, err := json.Marshal(updatedOptions)
if err != nil {
result.handleFatalError(fmt.Errorf("invalid flags provided to application: %s", err.Error()))
result.fatal("invalid flags provided to application: %w", err)
}
err = assetserver.ServeFile(rw, path, flags)
if err != nil {
result.handleFatalError(fmt.Errorf("unable to serve flags: %s", err.Error()))
result.fatal("unable to serve flags: %w", err)
}
default:
next.ServeHTTP(rw, req)
@ -130,40 +130,14 @@ func New(appOptions Options) *App {
srv, err := assetserver.NewAssetServer(opts)
if err != nil {
result.handleFatalError(fmt.Errorf("Fatal error in application initialisation: " + err.Error()))
result.fatal("application initialisation failed: %w", err)
}
result.assets = srv
result.assets.LogDetails()
result.bindings, err = NewBindings(appOptions.Services, appOptions.BindAliases)
if err != nil {
result.handleFatalError(fmt.Errorf("Fatal error in application initialisation: " + err.Error()))
}
for i, service := range appOptions.Services {
if thisService, ok := service.instance.(ServiceStartup); ok {
err := thisService.ServiceStartup(result.ctx, service.options)
if err != nil {
name := service.options.Name
if name == "" {
name = getServiceName(service.instance)
}
globalApplication.Logger.Error("ServiceStartup() failed shutting down application:", "service", name, "error", err.Error())
// Run shutdown on all services that have already started
for _, service := range appOptions.Services[:i] {
if thisService, ok := service.instance.(ServiceShutdown); ok {
err := thisService.ServiceShutdown()
if err != nil {
globalApplication.Logger.Error("Error shutting down service: " + err.Error())
}
}
}
// Shutdown the application
os.Exit(1)
}
}
}
result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
result.options.Services = slices.Clone(appOptions.Services)
// Process keybindings
if result.options.KeyBindings != nil {
@ -181,11 +155,11 @@ func New(appOptions Options) *App {
if errors.Is(err, alreadyRunningError) && manager != nil {
err = manager.notifyFirstInstance()
if err != nil {
globalApplication.error("Failed to notify first instance: " + err.Error())
globalApplication.error("failed to notify first instance: %w", err)
}
os.Exit(appOptions.SingleInstance.ExitCode)
}
result.handleFatalError(fmt.Errorf("failed to initialize single instance manager: %w", err))
result.fatal("failed to initialize single instance manager: %w", err)
} else {
result.singleInstanceManager = manager
}
@ -314,7 +288,8 @@ type App struct {
menuItems map[uint]*MenuItem
menuItemsLock sync.Mutex
// Running
// Starting and running
starting bool
running bool
runLock sync.Mutex
pendingRun []runnable
@ -350,7 +325,9 @@ type App struct {
keyBindingsLock sync.RWMutex
// Shutdown
performingShutdown bool
performingShutdown bool
shutdownLock sync.Mutex
serviceShutdownLock sync.Mutex
// Shutdown tasks are run when the application is shutting down.
// They are run in the order they are added and run on the main thread.
@ -375,6 +352,7 @@ func (a *App) handleWarning(msg string) {
a.Logger.Warn(msg)
}
}
func (a *App) handleError(err error) {
if a.options.ErrorHandler != nil {
a.options.ErrorHandler(err)
@ -383,6 +361,25 @@ func (a *App) handleError(err error) {
}
}
// RegisterService appends the given service to the list of bound services.
// Registered services will be bound and initialised
// in registration order upon calling [App.Run].
//
// RegisterService will log an error message
// and discard the given service
// if called after [App.Run].
func (a *App) RegisterService(service Service) {
a.runLock.Lock()
defer a.runLock.Unlock()
if a.starting || a.running {
a.error("services must be registered before running the application. Service '%s' will not be registered.", getServiceName(service))
return
}
a.options.Services = append(a.options.Services, service)
}
// EmitEvent will emit an event
func (a *App) EmitEvent(name string, data ...any) {
a.customEventProcessor.Emit(&CustomEvent{
@ -417,13 +414,7 @@ func (a *App) ResetEvents() {
}
func (a *App) handleFatalError(err error) {
var buffer strings.Builder
buffer.WriteString("\n\n************************ FATAL ******************************\n")
buffer.WriteString("* There has been a catastrophic failure in your application *\n")
buffer.WriteString("********************* Error Details *************************\n")
buffer.WriteString(err.Error())
buffer.WriteString("*************************************************************\n")
a.handleError(fmt.Errorf(buffer.String()))
a.handleError(&FatalError{err: err})
os.Exit(1)
}
@ -587,6 +578,18 @@ func (a *App) NewSystemTray() *SystemTray {
}
func (a *App) Run() error {
a.runLock.Lock()
// Prevent double invocations.
if a.starting || a.running {
a.runLock.Unlock()
return errors.New("application is running or a previous run has failed")
}
// Block further service registrations.
a.starting = true
a.runLock.Unlock()
// Ensure application context is canceled in case of failures.
defer a.cancel()
// Call post-create hooks
err := a.preRun()
@ -595,6 +598,24 @@ func (a *App) Run() error {
}
a.impl = newPlatformApp(a)
// Ensure services are shut down in case of failures.
defer a.shutdownServices()
// Ensure application context is canceled before service shutdown (duplicate calls don't hurt).
defer a.cancel()
// Startup services before dispatching any events.
// No need to hold the lock here because a.options.Services may only change when a.running is false.
services := a.options.Services
a.options.Services = nil
for i, service := range services {
if err := a.startupService(service); err != nil {
return fmt.Errorf("error starting service '%s': %w", getServiceName(service), err)
}
// Schedule started services for shutdown.
a.options.Services = services[:i+1]
}
go func() {
for {
event := <-applicationEvents
@ -641,17 +662,19 @@ func (a *App) Run() error {
a.runLock.Lock()
a.running = true
a.runLock.Unlock()
for _, systray := range a.pendingRun {
// No need to hold the lock here because
// - a.pendingRun may only change while a.running is false.
// - runnables are scheduled asynchronously anyway.
for _, pending := range a.pendingRun {
go func() {
defer handlePanic()
systray.Run()
pending.Run()
}()
}
a.pendingRun = nil
a.runLock.Unlock()
// set the application menu
if runtime.GOOS == "darwin" {
a.impl.setApplicationMenu(a.ApplicationMenu)
@ -660,27 +683,59 @@ func (a *App) Run() error {
a.impl.setIcon(a.options.Icon)
}
err = a.impl.run()
return a.impl.run()
}
func (a *App) startupService(service Service) error {
err := a.bindings.Add(service)
if err != nil {
return err
return fmt.Errorf("cannot bind service methods: %w", err)
}
// Cancel the context
a.cancel()
for _, service := range a.options.Services {
// If it conforms to the ServiceShutdown interface, call the Shutdown method
if thisService, ok := service.instance.(ServiceShutdown); ok {
err := thisService.ServiceShutdown()
if err != nil {
a.error("Error shutting down service: " + err.Error())
}
if service.options.Route != "" {
handler, ok := service.Instance().(http.Handler)
if !ok {
handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
http.Error(
rw,
fmt.Sprintf("Service '%s' does not handle HTTP requests", getServiceName(service)),
http.StatusServiceUnavailable,
)
})
}
a.assets.AttachServiceHandler(service.options.Route, handler)
}
if s, ok := service.instance.(ServiceStartup); ok {
a.debug("Starting up service:", "name", getServiceName(service))
return s.ServiceStartup(a.ctx, service.options)
}
return nil
}
func (a *App) shutdownServices() {
// Acquire lock to prevent double calls (defer in Run() + OnShutdown)
a.serviceShutdownLock.Lock()
defer a.serviceShutdownLock.Unlock()
// Ensure app context is canceled first (duplicate calls don't hurt).
a.cancel()
for len(a.options.Services) > 0 {
last := len(a.options.Services) - 1
service := a.options.Services[last]
a.options.Services = a.options.Services[:last] // Prevent double shutdowns
if s, ok := service.instance.(ServiceShutdown); ok {
a.debug("Shutting down service:", "name", getServiceName(service))
if err := s.ServiceShutdown(); err != nil {
a.error("error shutting down service '%s': %w", getServiceName(service), err)
}
}
}
}
func (a *App) handleApplicationEvent(event *ApplicationEvent) {
defer handlePanic()
a.applicationEventListenersLock.RLock()
@ -721,7 +776,7 @@ func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
window, ok := a.windows[event.windowId]
a.windowsLock.Unlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId)
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window
@ -735,7 +790,7 @@ func (a *App) handleWindowMessage(event *windowMessage) {
window, ok := a.windows[event.windowId]
a.windowsLock.RUnlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId)
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Check if the message starts with "wails:"
@ -760,7 +815,7 @@ func (a *App) handleWindowEvent(event *windowEvent) {
window, ok := a.windows[event.WindowID]
a.windowsLock.RUnlock()
if !ok {
log.Printf("Window #%d not found", event.WindowID)
a.warning("Window #%d not found", event.WindowID)
return
}
window.HandleWindowEvent(event.EventID)
@ -771,7 +826,7 @@ func (a *App) handleMenuItemClicked(menuItemID uint) {
menuItem := getMenuItemByID(menuItemID)
if menuItem == nil {
log.Printf("MenuItem #%d not found", menuItemID)
a.warning("MenuItem #%d not found", menuItemID)
return
}
menuItem.handleClick()
@ -796,7 +851,17 @@ func (a *App) OnShutdown(f func()) {
if f == nil {
return
}
a.shutdownTasks = append(a.shutdownTasks, f)
a.shutdownLock.Lock()
if !a.performingShutdown {
defer a.shutdownLock.Unlock()
a.shutdownTasks = append(a.shutdownTasks, f)
return
}
a.shutdownLock.Unlock()
InvokeAsync(f)
}
func (a *App) destroySystemTray(tray *SystemTray) {
@ -808,14 +873,22 @@ func (a *App) destroySystemTray(tray *SystemTray) {
}
func (a *App) cleanup() {
a.shutdownLock.Lock()
if a.performingShutdown {
a.shutdownLock.Unlock()
return
}
a.cancel() // Cancel app context before running shutdown hooks.
a.performingShutdown = true
a.shutdownLock.Unlock()
// No need to hold the lock here because a.shutdownTasks
// may only change while a.performingShutdown is false.
for _, shutdownTask := range a.shutdownTasks {
InvokeSync(shutdownTask)
}
InvokeSync(func() {
a.shutdownServices()
a.windowsLock.RLock()
for _, window := range a.windows {
window.Close()
@ -828,17 +901,23 @@ func (a *App) cleanup() {
}
a.systemTrays = nil
a.systemTraysLock.Unlock()
// Cleanup single instance manager
if a.singleInstanceManager != nil {
a.singleInstanceManager.cleanup()
}
a.postQuit()
if a.options.PostShutdown != nil {
a.options.PostShutdown()
}
})
// Cleanup single instance manager
if a.singleInstanceManager != nil {
a.singleInstanceManager.cleanup()
}
}
func (a *App) Quit() {
if a.impl != nil {
InvokeSync(a.impl.destroy)
a.postQuit()
}
}
@ -1006,15 +1085,17 @@ func (a *App) GetWindowByName(name string) Window {
func (a *App) runOrDeferToAppRun(r runnable) {
a.runLock.Lock()
running := a.running
if !running {
a.pendingRun = append(a.pendingRun, r)
}
a.runLock.Unlock()
if running {
r.Run()
if !a.running {
defer a.runLock.Unlock() // Defer unlocking for panic tolerance.
a.pendingRun = append(a.pendingRun, r)
return
}
// Unlock immediately to prevent deadlocks.
// No TOC/TOU risk here because a.running can never switch back to false.
a.runLock.Unlock()
r.Run()
}
func (a *App) processKeyBinding(acceleratorString string, window *WebviewWindow) bool {
@ -1056,7 +1137,7 @@ func (a *App) handleWindowKeyEvent(event *windowKeyEvent) {
window, ok := a.windows[event.windowId]
a.windowsLock.RUnlock()
if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId)
a.warning("WebviewWindow #%d not found", event.windowId)
return
}
// Get callback from window

View File

@ -362,7 +362,7 @@ func cleanup() {
func (a *App) logPlatformInfo() {
info, err := operatingsystem.Info()
if err != nil {
a.error("Error getting OS info: %s", err.Error())
a.error("error getting OS info: %w", err)
return
}

View File

@ -3,9 +3,10 @@
package application
import (
"github.com/wailsapp/wails/v3/internal/assetserver"
"net/http"
"time"
"github.com/wailsapp/wails/v3/internal/assetserver"
)
var devMode = false

View File

@ -208,7 +208,7 @@ func newPlatformApp(parent *App) *linuxApp {
func (a *App) logPlatformInfo() {
info, err := operatingsystem.Info()
if err != nil {
a.error("Error getting OS info: %s", err.Error())
a.error("error getting OS info: %w", err)
return
}

View File

@ -31,11 +31,21 @@ type Options struct {
// Services allows you to bind Go methods to the frontend.
Services []Service
// MarshalError will be called if non-nil
// to marshal to JSON the error values returned by service methods.
//
// MarshalError is not allowed to fail,
// but it may return a nil slice to fall back
// to the default error handling mechanism.
//
// If the returned slice is not nil, it must contain valid JSON.
MarshalError func(error) []byte
// BindAliases allows you to specify alias IDs for your bound methods.
// Example: `BindAliases: map[uint32]uint32{1: 1411160069}` states that alias ID 1 maps to the Go method with ID 1411160069.
BindAliases map[uint32]uint32
// Logger i a slog.Logger instance used for logging Wails system messages (not application messages).
// Logger is a slog.Logger instance used for logging Wails system messages (not application messages).
// If not defined, a default logger is used.
Logger *slog.Logger
@ -60,9 +70,17 @@ type Options struct {
// OnShutdown is called when the application is about to terminate.
// This is useful for cleanup tasks.
// The shutdown process blocks until this function returns
// The shutdown process blocks until this function returns.
OnShutdown func()
// PostShutdown is called after the application
// has finished shutting down, just before process termination.
// This is useful for testing and logging purposes
// on platforms where the Run() method does not return.
// When PostShutdown is called, the application instance is not usable anymore.
// The shutdown process blocks until this function returns.
PostShutdown func()
// ShouldQuit is a function that is called when the user tries to quit the application.
// If the function returns true, the application will quit.
// If the function returns false, the application will not quit.

View File

@ -3,7 +3,7 @@
package application
import (
"fmt"
"errors"
"os"
"path/filepath"
"slices"
@ -215,7 +215,7 @@ func (m *windowsApp) wndProc(hwnd w32.HWND, msg uint32, wParam, lParam uintptr)
if msg == w32.WM_DISPLAYCHANGE || (msg == w32.WM_SETTINGCHANGE && wParam == w32.SPI_SETWORKAREA) {
err := m.processAndCacheScreens()
if err != nil {
m.parent.error(err.Error())
m.parent.handleError(err)
}
}
}
@ -318,14 +318,14 @@ func setupDPIAwareness() error {
return w32.SetProcessDPIAware()
}
return fmt.Errorf("no DPI awareness method supported")
return errors.New("no DPI awareness method supported")
}
func newPlatformApp(app *App) *windowsApp {
err := setupDPIAwareness()
if err != nil {
app.error(err.Error())
app.handleError(err)
}
result := &windowsApp{
@ -337,7 +337,7 @@ func newPlatformApp(app *App) *windowsApp {
err = result.processAndCacheScreens()
if err != nil {
app.fatal(err.Error())
app.handleFatalError(err)
}
result.init()

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"runtime"
"strings"
@ -21,16 +20,22 @@ type CallOptions struct {
Args []json.RawMessage `json:"args"`
}
type PluginCallOptions struct {
Name string `json:"name"`
Args []json.RawMessage `json:"args"`
type ErrorKind string
const (
ReferenceError ErrorKind = "ReferenceError"
TypeError ErrorKind = "TypeError"
RuntimeError ErrorKind = "RuntimeError"
)
type CallError struct {
Kind ErrorKind `json:"kind"`
Message string `json:"message"`
Cause any `json:"cause,omitempty"`
}
var reservedPluginMethods = []string{
"Name",
"Init",
"Shutdown",
"Exported",
func (e *CallError) Error() string {
return e.Message
}
// Parameter defines a Go method parameter
@ -61,66 +66,75 @@ func (p *Parameter) IsError() bool {
// BoundMethod defines all the data related to a Go method that is
// bound to the Wails application
type BoundMethod struct {
ID uint32 `json:"id"`
Name string `json:"name"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
TypeName string
PackagePath string
ID uint32 `json:"id"`
Name string `json:"name"`
Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"`
FQN string
marshalError func(error) []byte
needsContext bool
}
type Bindings struct {
marshalError func(error) []byte
boundMethods map[string]*BoundMethod
boundByID map[uint32]*BoundMethod
methodAliases map[uint32]uint32
}
func NewBindings(instances []Service, aliases map[uint32]uint32) (*Bindings, error) {
app := Get()
b := &Bindings{
func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings {
return &Bindings{
marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError),
boundMethods: make(map[string]*BoundMethod),
boundByID: make(map[uint32]*BoundMethod),
methodAliases: aliases,
}
for _, binding := range instances {
handler, ok := binding.Instance().(http.Handler)
if ok && binding.options.Route != "" {
app.assets.AttachServiceHandler(binding.options.Route, handler)
}
err := b.Add(binding.Instance())
if err != nil {
return nil, err
}
}
return b, nil
}
// Add the given named type pointer methods to the Bindings
func (b *Bindings) Add(namedPtr interface{}) error {
methods, err := b.getMethods(namedPtr)
// Add adds the given service to the bindings.
func (b *Bindings) Add(service Service) error {
methods, err := getMethods(service.Instance())
if err != nil {
return fmt.Errorf("cannot bind value to app: %s", err.Error())
return err
}
marshalError := wrapErrorMarshaler(service.options.MarshalError, defaultMarshalError)
// Validate and log methods.
for _, method := range methods {
if _, ok := b.boundMethods[method.FQN]; ok {
return fmt.Errorf("bound method '%s' is already registered. Please note that you can register at most one service of each type; additional instances must be wrapped in dedicated structs", method.FQN)
}
if boundMethod, ok := b.boundByID[method.ID]; ok {
return fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To use this method, please rename it. Sorry :(", method.FQN, boundMethod.FQN)
}
// Log
attrs := []any{"fqn", method.FQN, "id", method.ID}
if alias, ok := lo.FindKey(b.methodAliases, method.ID); ok {
attrs = append(attrs, "alias", alias)
}
globalApplication.debug("Registering bound method:", attrs...)
}
for _, method := range methods {
// Add it as a regular method
b.boundMethods[method.String()] = method
// Store composite error marshaler
method.marshalError = marshalError
// Register method
b.boundMethods[method.FQN] = method
b.boundByID[method.ID] = method
}
return nil
}
// Get returns the bound method with the given name
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
method, ok := b.boundMethods[options.MethodName]
if !ok {
return nil
}
return method
return b.boundMethods[options.MethodName]
}
// GetByID returns the bound method with the given ID
@ -131,29 +145,27 @@ func (b *Bindings) GetByID(id uint32) *BoundMethod {
id = alias
}
}
result := b.boundByID[id]
return result
return b.boundByID[id]
}
// GenerateID generates a unique ID for a binding
func (b *Bindings) GenerateID(name string) (uint32, error) {
id, err := hash.Fnv(name)
if err != nil {
return 0, err
}
// Check if we already have it
boundMethod, ok := b.boundByID[id]
if ok {
return 0, fmt.Errorf("oh wow, we're sorry about this! Amazingly, a hash collision was detected for method '%s' (it generates the same hash as '%s'). To continue, please rename it. Sorry :(", name, boundMethod.String())
}
return id, nil
// internalServiceMethod is a set of methods
// that are handled specially by the binding engine
// and must not be exposed to the frontend.
//
// For simplicity we exclude these by name
// without checking their signatures,
// and so does the binding generator.
var internalServiceMethods = map[string]bool{
"ServiceName": true,
"ServiceStartup": true,
"ServiceShutdown": true,
"ServeHTTP": true,
}
func (b *BoundMethod) String() string {
return fmt.Sprintf("%s.%s.%s", b.PackagePath, b.TypeName, b.Name)
}
var ctxType = reflect.TypeFor[context.Context]()
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
func getMethods(value any) ([]*BoundMethod, error) {
// Create result placeholder
var result []*BoundMethod
@ -180,42 +192,27 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
return nil, fmt.Errorf("%s.%s is a generic type. Generic bound types are not supported", packagePath, namedType.String())
}
ctxType := reflect.TypeFor[context.Context]()
// Process Methods
for i := 0; i < ptrType.NumMethod(); i++ {
methodDef := ptrType.Method(i)
methodName := methodDef.Name
method := namedValue.MethodByName(methodName)
for i := range ptrType.NumMethod() {
methodName := ptrType.Method(i).Name
method := namedValue.Method(i)
if b.internalMethod(methodDef) {
if internalServiceMethods[methodName] {
continue
}
fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName)
// Create new method
boundMethod := &BoundMethod{
Name: methodName,
PackagePath: packagePath,
TypeName: typeName,
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
ID: hash.Fnv(fqn),
FQN: fqn,
Name: methodName,
Inputs: nil,
Outputs: nil,
Comments: "",
Method: method,
}
var err error
boundMethod.ID, err = b.GenerateID(boundMethod.String())
if err != nil {
return nil, err
}
args := []any{"name", boundMethod, "id", boundMethod.ID}
if b.methodAliases != nil {
alias, found := lo.FindKey(b.methodAliases, boundMethod.ID)
if found {
args = append(args, "alias", alias)
}
}
globalApplication.debug("Adding method:", args...)
// Iterate inputs
methodType := method.Type()
@ -245,40 +242,23 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
result = append(result, boundMethod)
}
return result, nil
}
func (b *Bindings) internalMethod(def reflect.Method) bool {
// Get the receiver type
receiverType := def.Type.In(0)
// Create a new instance of the receiver type
instance := reflect.New(receiverType.Elem()).Interface()
// Check if the instance implements any of our service interfaces
// and if the method matches the interface method
switch def.Name {
case "ServiceName":
if _, ok := instance.(ServiceName); ok {
return true
}
case "ServiceStartup":
if _, ok := instance.(ServiceStartup); ok {
return true
}
case "ServiceShutdown":
if _, ok := instance.(ServiceShutdown); ok {
return true
}
}
return false
func (b *BoundMethod) String() string {
return b.FQN
}
var errorType = reflect.TypeFor[error]()
// Call will attempt to call this bound method with the given args
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnValue interface{}, err error) {
// Call will attempt to call this bound method with the given args.
// If the call succeeds, result will be either a non-error return value (if there is only one)
// or a slice of non-error return values (if there are more than one).
//
// If the arguments are mistyped or the call returns one or more non-nil error values,
// result is nil and err is an instance of *[CallError].
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (result any, err error) {
// Use a defer statement to capture panics
defer handlePanic(handlePanicOptions{skipEnd: 5})
argCount := len(args)
@ -287,7 +267,10 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
}
if argCount != len(b.Inputs) {
err = fmt.Errorf("%s expects %d arguments, received %d", b.Name, len(b.Inputs), argCount)
err = &CallError{
Kind: TypeError,
Message: fmt.Sprintf("%s expects %d arguments, got %d", b.FQN, len(b.Inputs), argCount),
}
return
}
@ -305,7 +288,11 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
value := reflect.New(b.Inputs[base+index].ReflectType)
err = json.Unmarshal(arg, value.Interface())
if err != nil {
err = fmt.Errorf("could not parse argument #%d: %w", index, err)
err = &CallError{
Kind: TypeError,
Message: fmt.Sprintf("could not parse argument #%d: %s", index, err),
Cause: json.RawMessage(b.marshalError(err)),
}
return
}
callArgs[base+index] = value.Elem()
@ -322,32 +309,73 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
var nonErrorOutputs = make([]any, 0, len(callResults))
var errorOutputs []error
for _, result := range callResults {
if result.Type() == errorType {
if result.IsNil() {
for _, field := range callResults {
if field.Type() == errorType {
if field.IsNil() {
continue
}
if errorOutputs == nil {
errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs))
nonErrorOutputs = nil
}
errorOutputs = append(errorOutputs, result.Interface().(error))
errorOutputs = append(errorOutputs, field.Interface().(error))
} else if nonErrorOutputs != nil {
nonErrorOutputs = append(nonErrorOutputs, result.Interface())
nonErrorOutputs = append(nonErrorOutputs, field.Interface())
}
}
if errorOutputs != nil {
err = errors.Join(errorOutputs...)
if len(errorOutputs) > 0 {
info := make([]json.RawMessage, len(errorOutputs))
for i, err := range errorOutputs {
info[i] = b.marshalError(err)
}
cerr := &CallError{
Kind: RuntimeError,
Message: errors.Join(errorOutputs...).Error(),
Cause: info,
}
if len(info) == 1 {
cerr.Cause = info[0]
}
err = cerr
} else if len(nonErrorOutputs) == 1 {
returnValue = nonErrorOutputs[0]
result = nonErrorOutputs[0]
} else if len(nonErrorOutputs) > 1 {
returnValue = nonErrorOutputs
result = nonErrorOutputs
}
return
}
// wrapErrorMarshaler returns an error marshaling functions
// that calls the primary marshaler first,
// then falls back to the secondary one.
func wrapErrorMarshaler(primary func(error) []byte, secondary func(error) []byte) func(error) []byte {
if primary == nil {
return secondary
}
return func(err error) []byte {
result := primary(err)
if result == nil {
result = secondary(err)
}
return result
}
}
// defaultMarshalError implements the default error marshaling mechanism.
func defaultMarshalError(err error) []byte {
result, jsonErr := json.Marshal(&err)
if jsonErr != nil {
return nil
}
return result
}
// isPtr returns true if the value given is a pointer.
func isPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr

View File

@ -44,7 +44,7 @@ func (t *TestService) Variadic(s ...string) []string {
return s
}
func (t *TestService) PositionalAndVariadic(a int, b ...string) int {
func (t *TestService) PositionalAndVariadic(a int, _ ...string) int {
return a
}
@ -52,106 +52,103 @@ func (t *TestService) Slice(a []int) []int {
return a
}
func newArgs(jsonArgs ...string) []json.RawMessage {
args := []json.RawMessage{}
func newArgs(jsonArgs ...string) (args []json.RawMessage) {
for _, j := range jsonArgs {
args = append(args, json.RawMessage(j))
}
return args
return
}
func TestBoundMethodCall(t *testing.T) {
tests := []struct {
name string
method string
args []json.RawMessage
err error
err string
expected interface{}
}{
{
name: "nil",
method: "Nil",
args: []json.RawMessage{},
err: nil,
err: "",
expected: nil,
},
{
name: "string",
method: "String",
args: newArgs(`"foo"`),
err: nil,
err: "",
expected: "foo",
},
{
name: "multiple",
method: "Multiple",
args: newArgs(`"foo"`, "0", "false"),
err: nil,
err: "",
expected: []interface{}{"foo", 0, false},
},
{
name: "struct",
method: "Struct",
args: newArgs(`{ "name": "alice" }`),
err: nil,
err: "",
expected: Person{Name: "alice"},
},
{
name: "struct, nil error",
method: "StructNil",
args: newArgs(`{ "name": "alice" }`),
err: nil,
err: "",
expected: Person{Name: "alice"},
},
{
name: "struct, error",
method: "StructError",
args: newArgs(`{ "name": "alice" }`),
err: errors.New("error"),
err: "error",
expected: nil,
},
{
name: "invalid argument count",
method: "Multiple",
args: newArgs(`"foo"`),
err: errors.New("expects 3 arguments, received 1"),
err: "expects 3 arguments, got 1",
expected: nil,
},
{
name: "invalid argument type",
method: "String",
args: newArgs("1"),
err: errors.New("could not parse"),
err: "could not parse",
expected: nil,
},
{
name: "variadic, no arguments",
method: "Variadic",
args: newArgs(`[]`), // variadic parameters are passed as arrays
err: nil,
err: "",
expected: []string{},
},
{
name: "variadic",
method: "Variadic",
args: newArgs(`["foo", "bar"]`),
err: nil,
err: "",
expected: []string{"foo", "bar"},
},
{
name: "positional and variadic",
method: "PositionalAndVariadic",
args: newArgs("42", `[]`),
err: nil,
err: "",
expected: 42,
},
{
name: "slice",
method: "Slice",
args: newArgs(`[1,2,3]`),
err: nil,
err: "",
expected: []int{1, 2, 3},
},
}
@ -159,13 +156,11 @@ func TestBoundMethodCall(t *testing.T) {
// init globalApplication
_ = application.New(application.Options{})
bindings, err := application.NewBindings(
[]application.Service{
application.NewService(&TestService{}),
}, make(map[uint32]uint32),
)
bindings := application.NewBindings(nil, nil)
err := bindings.Add(application.NewService(&TestService{}))
if err != nil {
t.Fatalf("application.NewBindings() error = %v\n", err)
t.Fatalf("bindings.Add() error = %v\n", err)
}
for _, tt := range tests {
@ -180,13 +175,16 @@ func TestBoundMethodCall(t *testing.T) {
}
result, err := method.Call(context.TODO(), tt.args)
if tt.err != err && (tt.err == nil || err == nil || !strings.Contains(err.Error(), tt.err.Error())) {
t.Fatalf("error: %v, expected error: %v", err, tt.err)
if (tt.err == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), tt.err)) {
expected := tt.err
if expected == "" {
expected = "nil"
}
t.Fatalf("error: %#v, expected error: %v", err, expected)
}
if !reflect.DeepEqual(result, tt.expected) {
t.Fatalf("result: %v, expected result: %v", result, tt.expected)
}
})
}

View File

@ -42,7 +42,7 @@ func (m *windowsDialog) show() {
if m.dialog.window != nil {
parentWindow, err = m.dialog.window.NativeWindowHandle()
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
@ -50,12 +50,12 @@ func (m *windowsDialog) show() {
// 3 is the application icon
button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
} else {
button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
// This maps MessageBox return values to strings
@ -114,7 +114,7 @@ func (m *windowOpenFileDialog) show() (chan string, error) {
if m.dialog.window != nil {
config.ParentWindowHandle, err = m.dialog.window.NativeWindowHandle()
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
@ -242,7 +242,7 @@ func showCfdDialog(newDlg func() (cfd.Dialog, error), isMultiSelect bool) (any,
defer func() {
err := dlg.Release()
if err != nil {
globalApplication.error("Unable to release dialog: " + err.Error())
globalApplication.error("unable to release dialog: %w", err)
}
}()

View File

@ -3,14 +3,53 @@ package application
import (
"fmt"
"os"
"strings"
)
func Fatal(message string, args ...interface{}) {
println("*********************** FATAL ***********************")
println("There has been a catastrophic failure in your application.")
println("Please report this error at https://github.com/wailsapp/wails/issues")
println("******************** Error Details ******************")
println(fmt.Sprintf(message, args...))
println("*********************** FATAL ***********************")
// FatalError instances are passed to the registered error handler
// in case of catastrophic, unrecoverable failures that require immediate termination.
// FatalError wraps the original error value in an informative message.
// The underlying error may be retrieved through the [FatalError.Unwrap] method.
type FatalError struct {
err error
internal bool
}
// Internal returns true when the error was triggered from wails' internal code.
func (e *FatalError) Internal() bool {
return e.internal
}
// Unwrap returns the original cause of the fatal error,
// for easy inspection using the [errors.As] API.
func (e *FatalError) Unwrap() error {
return e.err
}
func (e *FatalError) Error() string {
var buffer strings.Builder
buffer.WriteString("\n\n******************************** FATAL *********************************\n")
buffer.WriteString("* There has been a catastrophic failure in your application. *\n")
if e.internal {
buffer.WriteString("* Please report this error at https://github.com/wailsapp/wails/issues *\n")
}
buffer.WriteString("**************************** Error Details *****************************\n")
buffer.WriteString(e.err.Error())
buffer.WriteString("************************************************************************\n")
return buffer.String()
}
func Fatal(message string, args ...any) {
err := &FatalError{
err: fmt.Errorf(message, args...),
internal: true,
}
if globalApplication != nil {
globalApplication.handleError(err)
} else {
fmt.Println(err)
}
os.Exit(1)
}

View File

@ -0,0 +1,168 @@
package services
import (
"context"
"fmt"
"sync/atomic"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/wailsapp/wails/v3/pkg/application"
)
type Config struct {
Id int
T *testing.T
Seq *atomic.Int64
Options application.ServiceOptions
StartupErr bool
ShutdownErr bool
}
func Configure[T any, P interface {
*T
Configure(Config)
}](srv P, c Config) application.Service {
srv.Configure(c)
return application.NewServiceWithOptions(srv, c.Options)
}
type Error struct {
Id int
}
func (e *Error) Error() string {
return fmt.Sprintf("service #%d mock failure", e.Id)
}
type Startupper struct {
Config
startup int64
}
func (s *Startupper) Configure(c Config) {
s.Config = c
}
func (s *Startupper) Id() int {
return s.Config.Id
}
func (s *Startupper) StartupSeq() int64 {
return s.startup
}
func (s *Startupper) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if s.startup != 0 {
s.T.Errorf("Double startup for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.startup, s.Seq.Load())
return nil
}
s.startup = s.Seq.Add(1)
if diff := cmp.Diff(s.Options, options); diff != "" {
s.T.Errorf("Options mismatch for service #%d (-want +got):\n%s", s.Id(), diff)
}
if s.StartupErr {
return &Error{Id: s.Id()}
} else {
return nil
}
}
type Shutdowner struct {
Config
shutdown int64
}
func (s *Shutdowner) Configure(c Config) {
s.Config = c
}
func (s *Shutdowner) Id() int {
return s.Config.Id
}
func (s *Shutdowner) ShutdownSeq() int64 {
return s.shutdown
}
func (s *Shutdowner) ServiceShutdown() error {
if s.shutdown != 0 {
s.T.Errorf("Double shutdown for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.shutdown, s.Seq.Load())
return nil
}
s.shutdown = s.Seq.Add(1)
if s.ShutdownErr {
return &Error{Id: s.Id()}
} else {
return nil
}
}
type StartupShutdowner struct {
Config
startup int64
shutdown int64
ctx context.Context
}
func (s *StartupShutdowner) Configure(c Config) {
s.Config = c
}
func (s *StartupShutdowner) Id() int {
return s.Config.Id
}
func (s *StartupShutdowner) StartupSeq() int64 {
return s.startup
}
func (s *StartupShutdowner) ShutdownSeq() int64 {
return s.shutdown
}
func (s *StartupShutdowner) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if s.startup != 0 {
s.T.Errorf("Double startup for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.startup, s.Seq.Load())
return nil
}
s.startup = s.Seq.Add(1)
s.ctx = ctx
if diff := cmp.Diff(s.Options, options); diff != "" {
s.T.Errorf("Options mismatch for service #%d (-want +got):\n%s", s.Id(), diff)
}
if s.StartupErr {
return &Error{Id: s.Id()}
} else {
return nil
}
}
func (s *StartupShutdowner) ServiceShutdown() error {
if s.shutdown != 0 {
s.T.Errorf("Double shutdown for service #%d: first at seq=%d, then at seq=%d", s.Id(), s.shutdown, s.Seq.Load())
return nil
}
s.shutdown = s.Seq.Add(1)
select {
case <-s.ctx.Done():
default:
s.T.Errorf("Service #%d shut down before context cancellation", s.Id())
}
if s.ShutdownErr {
return &Error{Id: s.Id()}
} else {
return nil
}
}

View File

@ -0,0 +1,92 @@
package shutdown
import (
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.Shutdowner }
Service2 struct{ svctest.Shutdowner }
Service3 struct{ svctest.Shutdowner }
Service4 struct{ svctest.Shutdowner }
Service5 struct{ svctest.Shutdowner }
Service6 struct{ svctest.Shutdowner }
)
func TestServiceShutdown(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq}),
}
app := apptest.New(t, application.Options{
Services: services[:3],
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
app.Quit()
})
err := apptest.Run(t, app)
if err != nil {
t.Fatal(err)
}
if count := seq.Load(); count != int64(len(services)) {
t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count)
}
validate(t, services[0], 5)
validate(t, services[1], 4)
validate(t, services[2], 2, 3)
validate(t, services[3], 1)
validate(t, services[4], 1)
validate(t, services[5], 0)
}
func validate(t *testing.T, svc application.Service, prev ...int64) {
id := svc.Instance().(interface{ Id() int }).Id()
seq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq()
if seq == 0 {
t.Errorf("Service #%d did not shut down", id)
return
}
for _, p := range prev {
if seq <= p {
t.Errorf("Wrong shutdown sequence number for service #%d: wanted >%d, got %d", id, p, seq)
}
}
}

View File

@ -0,0 +1,123 @@
package shutdownerror
import (
"errors"
"slices"
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.Shutdowner }
Service2 struct{ svctest.Shutdowner }
Service3 struct{ svctest.Shutdowner }
Service4 struct{ svctest.Shutdowner }
Service5 struct{ svctest.Shutdowner }
Service6 struct{ svctest.Shutdowner }
)
func TestServiceShutdownError(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, ShutdownErr: true}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, ShutdownErr: true}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, ShutdownErr: true}),
}
expectedShutdownErrors := []int{5, 4, 1}
var errCount atomic.Int64
var app *application.App
app = apptest.New(t, application.Options{
Services: services[:3],
ErrorHandler: func(err error) {
var mock *svctest.Error
if !errors.As(err, &mock) {
app.Logger.Error(err.Error())
return
}
i := int(errCount.Add(1) - 1)
if i < len(expectedShutdownErrors) && mock.Id == expectedShutdownErrors[i] {
return
}
cut := min(i, len(expectedShutdownErrors))
if slices.Contains(expectedShutdownErrors[:cut], mock.Id) {
t.Errorf("Late or duplicate shutdown error for service #%d", mock.Id)
} else if slices.Contains(expectedShutdownErrors[cut:], mock.Id) {
t.Errorf("Early shutdown error for service #%d", mock.Id)
} else {
t.Errorf("Unexpected shutdown error for service #%d", mock.Id)
}
},
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
app.Quit()
})
err := apptest.Run(t, app)
if err != nil {
t.Fatal(err)
}
if ec := errCount.Load(); ec != int64(len(expectedShutdownErrors)) {
t.Errorf("Wrong shutdown error count: wanted %d, got %d", len(expectedShutdownErrors), ec)
}
if count := seq.Load(); count != int64(len(services)) {
t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count)
}
validate(t, services[0], 5)
validate(t, services[1], 4)
validate(t, services[2], 2, 3)
validate(t, services[3], 1)
validate(t, services[4], 1)
validate(t, services[5], 0)
}
func validate(t *testing.T, svc application.Service, prev ...int64) {
id := svc.Instance().(interface{ Id() int }).Id()
seq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq()
if seq == 0 {
t.Errorf("Service #%d did not shut down", id)
return
}
for _, p := range prev {
if seq <= p {
t.Errorf("Wrong shutdown sequence number for service #%d: wanted >%d, got %d", id, p, seq)
}
}
}

View File

@ -0,0 +1,102 @@
package startup
import (
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.Startupper }
Service2 struct{ svctest.Startupper }
Service3 struct{ svctest.Startupper }
Service4 struct{ svctest.Startupper }
Service5 struct{ svctest.Startupper }
Service6 struct{ svctest.Startupper }
)
func TestServiceStartup(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, Options: application.ServiceOptions{
Name: "I am service 2",
}}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq, Options: application.ServiceOptions{
Route: "/mounted/here",
}}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, Seq: &seq, Options: application.ServiceOptions{
Name: "I am service 5",
Route: "/mounted/there",
}}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, Options: application.ServiceOptions{
Name: "I am service 6",
Route: "/mounted/elsewhere",
}}),
}
app := apptest.New(t, application.Options{
Services: services[:3],
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
app.Quit()
})
err := apptest.Run(t, app)
if err != nil {
t.Fatal(err)
}
if count := seq.Load(); count != int64(len(services)) {
t.Errorf("Wrong startup call count: wanted %d, got %d", len(services), count)
}
validate(t, services[0], 0)
validate(t, services[1], 1)
validate(t, services[2], 2)
validate(t, services[3], 3)
validate(t, services[4], 3)
validate(t, services[5], 4, 5)
}
func validate(t *testing.T, svc application.Service, prev ...int64) {
id := svc.Instance().(interface{ Id() int }).Id()
seq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq()
if seq == 0 {
t.Errorf("Service #%d did not start up", id)
return
}
for _, p := range prev {
if seq <= p {
t.Errorf("Wrong startup sequence number for service #%d: wanted >%d, got %d", id, p, seq)
}
}
}

View File

@ -0,0 +1,114 @@
package startuperror
import (
"errors"
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.Startupper }
Service2 struct{ svctest.Startupper }
Service3 struct{ svctest.Startupper }
Service4 struct{ svctest.Startupper }
Service5 struct{ svctest.Startupper }
Service6 struct{ svctest.Startupper }
)
func TestServiceStartupError(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq, StartupErr: true}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, StartupErr: true}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, StartupErr: true}),
}
app := apptest.New(t, application.Options{
Services: services[:3],
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
t.Errorf("Application started")
app.Quit()
})
var mock *svctest.Error
err := apptest.Run(t, app)
if err != nil {
if !errors.As(err, &mock) {
t.Fatal(err)
}
}
if mock == nil {
t.Fatal("Wanted startup error for service #3 or #4, got none")
} else if mock.Id != 3 && mock.Id != 4 {
t.Errorf("Wanted startup error for service #3 or #4, got #%d", mock.Id)
}
if count := seq.Load(); count != 4 {
t.Errorf("Wrong startup call count: wanted %d, got %d", 4, count)
}
validate(t, services[0], 0)
validate(t, services[1], 1)
validate(t, services[2], 2)
validate(t, services[mock.Id], 3)
notStarted := 3
if mock.Id == 3 {
notStarted = 4
}
if seq := services[notStarted].Instance().(interface{ StartupSeq() int64 }).StartupSeq(); seq != 0 {
t.Errorf("Service #%d started up unexpectedly at seq=%d", notStarted, seq)
}
if seq := services[5].Instance().(interface{ StartupSeq() int64 }).StartupSeq(); seq != 0 {
t.Errorf("Service #5 started up unexpectedly at seq=%d", seq)
}
}
func validate(t *testing.T, svc application.Service, prev ...int64) {
id := svc.Instance().(interface{ Id() int }).Id()
seq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq()
if seq == 0 {
t.Errorf("Service #%d did not start up", id)
return
}
for _, p := range prev {
if seq <= p {
t.Errorf("Wrong startup sequence number for service #%d: wanted >%d, got %d", id, p, seq)
}
}
}

View File

@ -0,0 +1,102 @@
package startupshutdown
import (
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.StartupShutdowner }
Service2 struct{ svctest.StartupShutdowner }
Service3 struct{ svctest.StartupShutdowner }
Service4 struct{ svctest.StartupShutdowner }
Service5 struct{ svctest.StartupShutdowner }
Service6 struct{ svctest.StartupShutdowner }
)
func TestServiceStartupShutdown(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq}),
}
app := apptest.New(t, application.Options{
Services: services[:3],
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
if count := seq.Load(); count != int64(len(services)) {
t.Errorf("Wrong startup call count: wanted %d, got %d", len(services), count)
}
seq.Store(0)
app.Quit()
})
err := apptest.Run(t, app)
if err != nil {
t.Fatal(err)
}
if count := seq.Load(); count != int64(len(services)) {
t.Errorf("Wrong shutdown call count: wanted %d, got %d", len(services), count)
}
bound := int64(len(services)) + 1
validate(t, services[0], bound)
validate(t, services[1], bound)
validate(t, services[2], bound)
validate(t, services[3], bound)
validate(t, services[4], bound)
validate(t, services[5], bound)
}
func validate(t *testing.T, svc application.Service, bound int64) {
id := svc.Instance().(interface{ Id() int }).Id()
startup := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq()
shutdown := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq()
if startup == 0 && shutdown == 0 {
t.Errorf("Service #%d did not start nor shut down", id)
return
} else if startup == 0 {
t.Errorf("Service #%d started, but did not shut down", id)
return
} else if shutdown == 0 {
t.Errorf("Service #%d shut down, but did not start", id)
return
}
if shutdown != bound-startup {
t.Errorf("Wrong sequence numbers for service #%d: wanted either %d..%d or %d..%d, got %d..%d", id, startup, bound-startup, bound-shutdown, shutdown, startup, shutdown)
}
}

View File

@ -0,0 +1,140 @@
package startupshutdownerror
import (
"errors"
"slices"
"sync"
"sync/atomic"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
apptest "github.com/wailsapp/wails/v3/pkg/application/internal/tests"
svctest "github.com/wailsapp/wails/v3/pkg/application/internal/tests/services"
"github.com/wailsapp/wails/v3/pkg/events"
)
func TestMain(m *testing.M) {
apptest.Main(m)
}
type (
Service1 struct{ svctest.StartupShutdowner }
Service2 struct{ svctest.StartupShutdowner }
Service3 struct{ svctest.StartupShutdowner }
Service4 struct{ svctest.StartupShutdowner }
Service5 struct{ svctest.StartupShutdowner }
Service6 struct{ svctest.StartupShutdowner }
)
func TestServiceStartupShutdownError(t *testing.T) {
var seq atomic.Int64
services := []application.Service{
svctest.Configure(&Service1{}, svctest.Config{Id: 0, T: t, Seq: &seq}),
svctest.Configure(&Service2{}, svctest.Config{Id: 1, T: t, Seq: &seq, ShutdownErr: true}),
svctest.Configure(&Service3{}, svctest.Config{Id: 2, T: t, Seq: &seq}),
svctest.Configure(&Service4{}, svctest.Config{Id: 3, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}),
svctest.Configure(&Service5{}, svctest.Config{Id: 4, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}),
svctest.Configure(&Service6{}, svctest.Config{Id: 5, T: t, Seq: &seq, StartupErr: true, ShutdownErr: true}),
}
expectedShutdownErrors := []int{1}
var errCount atomic.Int64
var app *application.App
app = apptest.New(t, application.Options{
Services: services[:3],
ErrorHandler: func(err error) {
var mock *svctest.Error
if !errors.As(err, &mock) {
app.Logger.Error(err.Error())
return
}
i := int(errCount.Add(1) - 1)
if i < len(expectedShutdownErrors) && mock.Id == expectedShutdownErrors[i] {
return
}
cut := min(i, len(expectedShutdownErrors))
if slices.Contains(expectedShutdownErrors[:cut], mock.Id) {
t.Errorf("Late or duplicate shutdown error for service #%d", mock.Id)
} else if slices.Contains(expectedShutdownErrors[cut:], mock.Id) {
t.Errorf("Early shutdown error for service #%d", mock.Id)
} else {
t.Errorf("Unexpected shutdown error for service #%d", mock.Id)
}
},
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
app.RegisterService(services[3])
wg.Done()
}()
go func() {
app.RegisterService(services[4])
wg.Done()
}()
wg.Wait()
app.RegisterService(services[5])
app.OnApplicationEvent(events.Common.ApplicationStarted, func(*application.ApplicationEvent) {
t.Errorf("Application started")
app.Quit()
})
var mock *svctest.Error
err := apptest.Run(t, app)
if err != nil {
if !errors.As(err, &mock) {
t.Fatal(err)
}
}
if mock == nil {
t.Fatal("Wanted error for service #3 or #4, got none")
} else if mock.Id != 3 && mock.Id != 4 {
t.Errorf("Wanted error for service #3 or #4, got #%d", mock.Id)
}
if ec := errCount.Load(); ec != int64(len(expectedShutdownErrors)) {
t.Errorf("Wrong shutdown error count: wanted %d, got %d", len(expectedShutdownErrors), ec)
}
if count := seq.Load(); count != 4+3 {
t.Errorf("Wrong startup+shutdown call count: wanted %d+%d, got %d", 4, 3, count)
}
validate(t, services[0], true, true)
validate(t, services[1], true, true)
validate(t, services[2], true, true)
validate(t, services[3], mock.Id == 3, false)
validate(t, services[4], mock.Id == 4, false)
validate(t, services[5], false, false)
}
func validate(t *testing.T, svc application.Service, startup bool, shutdown bool) {
id := svc.Instance().(interface{ Id() int }).Id()
startupSeq := svc.Instance().(interface{ StartupSeq() int64 }).StartupSeq()
shutdownSeq := svc.Instance().(interface{ ShutdownSeq() int64 }).ShutdownSeq()
if startup != (startupSeq != 0) {
if startupSeq == 0 {
t.Errorf("Service #%d did not start up", id)
} else {
t.Errorf("Unexpected startup for service #%d at seq=%d", id, startupSeq)
}
}
if shutdown != (shutdownSeq != 0) {
if shutdownSeq == 0 {
t.Errorf("Service #%d did not shut down", id)
} else {
t.Errorf("Unexpected shutdown for service #%d at seq=%d", id, shutdownSeq)
}
}
}

View File

@ -0,0 +1,74 @@
package tests
import (
"errors"
"os"
"runtime"
"testing"
"github.com/wailsapp/wails/v3/pkg/application"
)
var appChan chan *application.App = make(chan *application.App, 1)
var errChan chan error = make(chan error, 1)
var endChan chan error = make(chan error, 1)
func init() { runtime.LockOSThread() }
func New(t *testing.T, options application.Options) *application.App {
var app *application.App
app = application.Get()
if app != nil {
return app
}
if options.Name == "" {
options.Name = t.Name()
}
errorHandler := options.ErrorHandler
options.ErrorHandler = func(err error) {
if fatal := (*application.FatalError)(nil); errors.As(err, &fatal) {
endChan <- err
select {} // Block forever
} else if errorHandler != nil {
errorHandler(err)
} else {
app.Logger.Error(err.Error())
}
}
postShutdown := options.PostShutdown
options.PostShutdown = func() {
if postShutdown != nil {
postShutdown()
}
endChan <- nil
select {} // Block forever
}
return application.New(options)
}
func Run(t *testing.T, app *application.App) error {
appChan <- app
select {
case err := <-errChan:
return err
case fatal := <-endChan:
if fatal != nil {
t.Fatal(fatal)
}
return fatal
}
}
func Main(m *testing.M) {
go func() {
os.Exit(m.Run())
}()
errChan <- (<-appChan).Run()
select {} // Block forever
}

View File

@ -465,7 +465,7 @@ func (a *linuxApp) setIcon(icon []byte) {
var gerror *C.GError
pixbuf := C.gdk_pixbuf_new_from_stream(stream, nil, &gerror)
if gerror != nil {
a.parent.error("Failed to load application icon: " + C.GoString(gerror.message))
a.parent.error("failed to load application icon: %s", C.GoString(gerror.message))
C.g_error_free(gerror)
return
}

View File

@ -1,8 +1,6 @@
package application
import (
"fmt"
"os"
"sync"
"sync/atomic"
)
@ -226,15 +224,13 @@ func NewRole(role Role) *MenuItem {
result = NewHelpMenuItem()
default:
globalApplication.error(fmt.Sprintf("No support for role: %v", role))
os.Exit(1)
globalApplication.error("no support for role: %v", role)
}
if result == nil {
return nil
if result != nil {
result.role = role
}
result.role = role
return result
}
@ -279,7 +275,7 @@ func (m *MenuItem) handleClick() {
func (m *MenuItem) SetAccelerator(shortcut string) *MenuItem {
accelerator, err := parseAccelerator(shortcut)
if err != nil {
globalApplication.error("invalid accelerator. %v", err.Error())
globalApplication.error("invalid accelerator: %w", err)
return m
}
m.accelerator = accelerator

View File

@ -3,8 +3,9 @@
package application
import (
"github.com/wailsapp/wails/v3/pkg/w32"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
)
type windowsMenuItem struct {
@ -121,7 +122,7 @@ func (m *windowsMenuItem) setBitmap(bitmap []byte) {
// Set the icon
err := w32.SetMenuIcons(m.hMenu, m.id, bitmap, nil)
if err != nil {
globalApplication.error("Unable to set bitmap on menu item: %s", err.Error())
globalApplication.error("unable to set bitmap on menu item: %w", err)
return
}
m.update()

View File

@ -3,6 +3,7 @@ package application
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
@ -41,12 +42,12 @@ func NewMessageProcessor(logger *slog.Logger) *MessageProcessor {
}
}
func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, args ...any) {
m.Error(message, args...)
rw.WriteHeader(http.StatusBadRequest)
_, err := rw.Write([]byte(fmt.Sprintf(message, args...)))
func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, err error) {
m.Error(message, "error", err)
rw.WriteHeader(http.StatusUnprocessableEntity)
_, err = rw.Write([]byte(err.Error()))
if err != nil {
m.Error("Unable to write error message: %s", err)
m.Error("Unable to write error response:", "error", err)
}
}
@ -61,12 +62,12 @@ func (m *MessageProcessor) getTargetWindow(r *http.Request) (Window, string) {
}
wID, err := strconv.ParseUint(windowID, 10, 64)
if err != nil {
m.Error("Window ID '%s' not parsable: %s", windowID, err)
m.Error("Window ID not parsable:", "id", windowID, "error", err)
return nil, windowID
}
targetWindow := globalApplication.getWindowForID(uint(wID))
if targetWindow == nil {
m.Error("Window ID %d not found", wID)
m.Error("Window ID not found:", "id", wID)
return nil, windowID
}
return targetWindow, windowID
@ -75,7 +76,7 @@ func (m *MessageProcessor) getTargetWindow(r *http.Request) (Window, string) {
func (m *MessageProcessor) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
object := r.URL.Query().Get("object")
if object == "" {
m.httpError(rw, "Invalid runtime call")
m.httpError(rw, "Invalid runtime call:", errors.New("missing object value"))
return
}
@ -90,19 +91,19 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h
}()
object, err := strconv.Atoi(r.URL.Query().Get("object"))
if err != nil {
m.httpError(rw, "Error decoding object value: "+err.Error())
m.httpError(rw, "Invalid runtime call:", fmt.Errorf("error decoding object value: %w", err))
return
}
method, err := strconv.Atoi(r.URL.Query().Get("method"))
if err != nil {
m.httpError(rw, "Error decoding method value: "+err.Error())
m.httpError(rw, "Invalid runtime call:", fmt.Errorf("error decoding method value: %w", err))
return
}
params := QueryParams(r.URL.Query())
targetWindow, nameOrID := m.getTargetWindow(r)
if targetWindow == nil {
m.httpError(rw, fmt.Sprintf("Window '%s' not found", nameOrID))
m.httpError(rw, "Invalid runtime call:", fmt.Errorf("window '%s' not found", nameOrID))
return
}
@ -130,7 +131,7 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h
case cancelCallRequesst:
m.processCallCancelMethod(method, rw, r, targetWindow, params)
default:
m.httpError(rw, "Unknown runtime call: %d", object)
m.httpError(rw, "Invalid runtime call:", fmt.Errorf("unknown object %d", object))
}
}
@ -150,13 +151,13 @@ func (m *MessageProcessor) json(rw http.ResponseWriter, data any) {
if data != nil {
jsonPayload, err = json.Marshal(data)
if err != nil {
m.Error("Unable to convert data to JSON. Please report this to the Wails team! Error: %s", err)
m.Error("Unable to convert data to JSON. Please report this to the Wails team!", "error", err)
return
}
}
_, err = rw.Write(jsonPayload)
if err != nil {
m.Error("Unable to write json payload. Please report this to the Wails team! Error: %s", err)
m.Error("Unable to write json payload. Please report this to the Wails team!", "error", err)
return
}
m.ok(rw)
@ -165,7 +166,7 @@ func (m *MessageProcessor) json(rw http.ResponseWriter, data any) {
func (m *MessageProcessor) text(rw http.ResponseWriter, data string) {
_, err := rw.Write([]byte(data))
if err != nil {
m.Error("Unable to write json payload. Please report this to the Wails team! Error: %s", err)
m.Error("Unable to write json payload. Please report this to the Wails team!", "error", err)
return
}
rw.Header().Set("Content-Type", "text/plain")

View File

@ -1,6 +1,7 @@
package application
import (
"fmt"
"net/http"
)
@ -28,9 +29,9 @@ func (m *MessageProcessor) processApplicationMethod(method int, rw http.Response
globalApplication.Show()
m.ok(rw)
default:
m.httpError(rw, "Unknown application method: %d", method)
m.httpError(rw, "Invalid application call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime Call:", "method", "Application."+applicationMethodNames[method])
m.Info("Runtime call:", "method", "Application."+applicationMethodNames[method])
}

View File

@ -1,8 +1,11 @@
package application
import (
"github.com/pkg/browser"
"errors"
"fmt"
"net/http"
"github.com/pkg/browser"
)
const (
@ -14,10 +17,9 @@ var browserMethods = map[int]string{
}
func (m *MessageProcessor) processBrowserMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
m.httpError(rw, "Invalid browser call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
@ -25,19 +27,20 @@ func (m *MessageProcessor) processBrowserMethod(method int, rw http.ResponseWrit
case BrowserOpenURL:
url := args.String("url")
if url == nil {
m.Error("OpenURL: url is required")
m.httpError(rw, "Invalid browser call:", errors.New("missing argument 'url'"))
return
}
err := browser.OpenURL(*url)
if err != nil {
m.Error("OpenURL: %s", err.Error())
m.httpError(rw, "OpenURL failed:", err)
return
}
m.ok(rw)
m.Info("Runtime Call:", "method", "Browser."+browserMethods[method], "url", *url)
m.Info("Runtime call:", "method", "Browser."+browserMethods[method], "url", *url)
default:
m.httpError(rw, "Unknown browser method: %d", method)
m.httpError(rw, "Invalid browser call:", fmt.Errorf("unknown method: %d", method))
return
}
}

View File

@ -3,6 +3,7 @@ package application
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
)
@ -15,33 +16,46 @@ const (
)
func (m *MessageProcessor) callErrorCallback(window Window, message string, callID *string, err error) {
errorMsg := fmt.Sprintf(message, err)
m.Error(errorMsg)
window.CallError(*callID, errorMsg)
m.Error(message, "id", *callID, "error", err)
if cerr := (*CallError)(nil); errors.As(err, &cerr) {
if data, jsonErr := json.Marshal(cerr); jsonErr == nil {
window.CallError(*callID, string(data), true)
return
} else {
m.Error("Unable to convert data to JSON. Please report this to the Wails team!", "id", *callID, "error", jsonErr)
}
}
window.CallError(*callID, err.Error(), false)
}
func (m *MessageProcessor) callCallback(window Window, callID *string, result string, isJSON bool) {
func (m *MessageProcessor) callCallback(window Window, callID *string, result string) {
window.CallResponse(*callID, result)
}
func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
return
}
callID := args.String("call-id")
if callID == nil || *callID == "" {
m.Error("call-id is required")
m.httpError(rw, "Invalid binding call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
m.l.Lock()
cancel := m.runningCalls[*callID]
m.l.Unlock()
callID := args.String("call-id")
if callID == nil || *callID == "" {
m.httpError(rw, "Invalid binding call:", errors.New("missing argument 'call-id'"))
return
}
var cancel func()
func() {
m.l.Lock()
defer m.l.Unlock()
cancel = m.runningCalls[*callID]
}()
if cancel != nil {
cancel()
m.Info("Binding call canceled:", "id", *callID)
}
m.ok(rw)
}
@ -49,12 +63,13 @@ func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseW
func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
m.httpError(rw, "Invalid binding call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
callID := args.String("call-id")
if callID == nil || *callID == "" {
m.Error("call-id is required")
m.httpError(rw, "Invalid binding call:", errors.New("missing argument 'call-id'"))
return
}
@ -63,86 +78,116 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter,
var options CallOptions
err := params.ToStruct(&options)
if err != nil {
m.callErrorCallback(window, "Error parsing call options: %s", callID, err)
return
}
var boundMethod *BoundMethod
if options.MethodName != "" {
boundMethod = globalApplication.bindings.Get(&options)
if boundMethod == nil {
m.callErrorCallback(window, "Error getting binding for method: %s", callID, fmt.Errorf("method '%s' not found", options.MethodName))
return
}
} else {
boundMethod = globalApplication.bindings.GetByID(options.MethodID)
}
if boundMethod == nil {
m.callErrorCallback(window, "Error getting binding for method: %s", callID, fmt.Errorf("method ID %d not found", options.MethodID))
m.httpError(rw, "Invalid binding call:", fmt.Errorf("error parsing call options: %w", err))
return
}
ctx, cancel := context.WithCancel(context.WithoutCancel(r.Context()))
// Schedule cancel in case panics happen before starting the call.
cancelRequired := true
defer func() {
if cancelRequired {
cancel()
}
}()
ambiguousID := false
m.l.Lock()
if m.runningCalls[*callID] != nil {
ambiguousID = true
} else {
m.runningCalls[*callID] = cancel
}
m.l.Unlock()
func() {
m.l.Lock()
defer m.l.Unlock()
if m.runningCalls[*callID] != nil {
ambiguousID = true
} else {
m.runningCalls[*callID] = cancel
}
}()
if ambiguousID {
cancel()
m.callErrorCallback(window, "Error calling method: %s, a method call with the same id is already running", callID, err)
m.httpError(rw, "Invalid binding call:", fmt.Errorf("ambiguous call id: %s", *callID))
return
}
// Set the context values for the window
if window != nil {
ctx = context.WithValue(ctx, WindowKey, window)
m.ok(rw) // From now on, failures are reported through the error callback.
// Log call
var methodRef any = options.MethodName
if options.MethodName == "" {
methodRef = options.MethodID
}
m.Info("Binding call started:", "id", *callID, "method", methodRef)
go func() {
defer handlePanic()
defer func() {
cancel()
m.l.Lock()
defer m.l.Unlock()
delete(m.runningCalls, *callID)
m.l.Unlock()
}()
defer cancel()
var boundMethod *BoundMethod
if options.MethodName != "" {
boundMethod = globalApplication.bindings.Get(&options)
if boundMethod == nil {
m.callErrorCallback(window, "Binding call failed:", callID, &CallError{
Kind: ReferenceError,
Message: fmt.Sprintf("unknown bound method name '%s'", options.MethodName),
})
return
}
} else {
boundMethod = globalApplication.bindings.GetByID(options.MethodID)
if boundMethod == nil {
m.callErrorCallback(window, "Binding call failed:", callID, &CallError{
Kind: ReferenceError,
Message: fmt.Sprintf("unknown bound method id %d", options.MethodID),
})
return
}
}
// Prepare args for logging. This should never fail since json.Unmarshal succeeded before.
jsonArgs, _ := json.Marshal(options.Args)
var jsonResult []byte
defer m.Info("Binding call complete:", "id", *callID, "method", boundMethod, "args", string(jsonArgs), "result", string(jsonResult))
// Set the context values for the window
if window != nil {
ctx = context.WithValue(ctx, WindowKey, window)
}
result, err := boundMethod.Call(ctx, options.Args)
if err != nil {
msg := fmt.Sprintf("Error calling method '%v'", boundMethod.Name)
m.callErrorCallback(window, msg+": %s", callID, err)
if cerr := (*CallError)(nil); errors.As(err, &cerr) {
switch cerr.Kind {
case ReferenceError, TypeError:
m.callErrorCallback(window, "Binding call failed:", callID, cerr)
case RuntimeError:
m.callErrorCallback(window, "Bound method returned an error:", callID, cerr)
}
return
}
var jsonResult = []byte("{}")
if result != nil {
// convert result to json
jsonResult, err = json.Marshal(result)
if err != nil {
m.callErrorCallback(window, "Error converting result to json: %s", callID, err)
m.callErrorCallback(window, "Binding call failed:", callID, &CallError{
Kind: TypeError,
Message: fmt.Sprintf("error marshaling result: %s", err),
})
return
}
}
m.callCallback(window, callID, string(jsonResult), true)
var jsonArgs struct {
Args json.RawMessage `json:"args"`
}
err = params.ToStruct(&jsonArgs)
if err != nil {
m.callErrorCallback(window, "Error parsing arguments: %s", callID, err)
return
}
m.Info("Call Binding:", "method", boundMethod, "args", string(jsonArgs.Args), "result", result)
m.callCallback(window, callID, string(jsonResult))
}()
m.ok(rw)
default:
m.httpError(rw, "Unknown call method: %d", method)
}
cancelRequired = false
default:
m.httpError(rw, "Invalid binding call:", fmt.Errorf("unknown method: %d", method))
return
}
}

View File

@ -1,6 +1,8 @@
package application
import (
"errors"
"fmt"
"net/http"
)
@ -15,30 +17,31 @@ var clipboardMethods = map[int]string{
}
func (m *MessageProcessor) processClipboardMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
m.httpError(rw, "Invalid clipboard call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
var text string
switch method {
case ClipboardSetText:
text := args.String("text")
if text == nil {
m.Error("SetText: text is required")
textp := args.String("text")
if textp == nil {
m.httpError(rw, "Invalid clipboard call:", errors.New("missing argument 'text'"))
return
}
globalApplication.Clipboard().SetText(*text)
text = *textp
globalApplication.Clipboard().SetText(text)
m.ok(rw)
m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", *text)
case ClipboardText:
text, _ := globalApplication.Clipboard().Text()
text, _ = globalApplication.Clipboard().Text()
m.text(rw, text)
m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", text)
default:
m.httpError(rw, "Unknown clipboard method: %d", method)
m.httpError(rw, "Invalid clipboard call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime call:", "method", "Clipboard."+clipboardMethods[method], "text", text)
}

View File

@ -1,6 +1,7 @@
package application
import (
"fmt"
"net/http"
)
@ -29,21 +30,21 @@ var contextmenuMethodNames = map[int]string{
}
func (m *MessageProcessor) processContextMenuMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
switch method {
case ContextMenuOpen:
var data ContextMenuData
err := params.ToStruct(&data)
if err != nil {
m.httpError(rw, "error parsing contextmenu message: %s", err.Error())
m.httpError(rw, "Invalid contextmenu call:", fmt.Errorf("error parsing parameters: %w", err))
return
}
window.OpenContextMenu(&data)
m.ok(rw)
m.Info("Runtime call:", "method", "ContextMenu."+contextmenuMethodNames[method], "id", data.Id, "x", data.X, "y", data.Y, "data", data.Data)
default:
m.httpError(rw, "Unknown contextmenu method: %d", method)
m.httpError(rw, "Invalid contextmenu call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime Call:", "method", "ContextMenu."+contextmenuMethodNames[method])
}

View File

@ -2,6 +2,7 @@ package application
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime"
@ -26,9 +27,8 @@ var dialogMethodNames = map[int]string{
}
func (m *MessageProcessor) dialogErrorCallback(window Window, message string, dialogID *string, err error) {
errorMsg := fmt.Sprintf(message, err)
m.Error(errorMsg)
window.DialogError(*dialogID, errorMsg)
m.Error(message, "error", err)
window.DialogError(*dialogID, err.Error())
}
func (m *MessageProcessor) dialogCallback(window Window, dialogID *string, result string, isJSON bool) {
@ -36,15 +36,15 @@ func (m *MessageProcessor) dialogCallback(window Window, dialogID *string, resul
}
func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
m.httpError(rw, "Invalid dialog call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
dialogID := args.String("dialog-id")
if dialogID == nil {
m.Error("dialog-id is required")
m.httpError(rw, "Invalid window call:", errors.New("missing argument 'dialog-id'"))
return
}
@ -55,7 +55,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
var options MessageDialogOptions
err := params.ToStruct(&options)
if err != nil {
m.dialogErrorCallback(window, "Error parsing dialog options: %s", dialogID, err)
m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err))
return
}
if len(options.Buttons) == 0 {
@ -91,13 +91,13 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
dialog.AddButtons(options.Buttons)
dialog.Show()
m.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options)
m.Info("Runtime call:", "method", methodName, "options", options)
case DialogOpenFile:
var options OpenFileDialogOptions
err := params.ToStruct(&options)
if err != nil {
m.httpError(rw, "Error parsing dialog options: %s", err.Error())
m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err))
return
}
var detached = args.Bool("Detached")
@ -111,35 +111,35 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
if options.AllowsMultipleSelection {
files, err := dialog.PromptForMultipleSelection()
if err != nil {
m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err)
m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error getting selection: %w", err))
return
} else {
result, err := json.Marshal(files)
if err != nil {
m.dialogErrorCallback(window, "Error marshalling files: %s", dialogID, err)
m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error marshaling files: %w", err))
return
}
m.dialogCallback(window, dialogID, string(result), true)
m.Info("Runtime Call:", "method", methodName, "result", result)
m.Info("Runtime call:", "method", methodName, "result", result)
}
} else {
file, err := dialog.PromptForSingleSelection()
if err != nil {
m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err)
m.dialogErrorCallback(window, "Dialog.OpenFile failed", dialogID, fmt.Errorf("error getting selection: %w", err))
return
}
m.dialogCallback(window, dialogID, file, false)
m.Info("Runtime Call:", "method", methodName, "result", file)
m.Info("Runtime call:", "method", methodName, "result", file)
}
}()
m.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options)
m.Info("Runtime call:", "method", methodName, "options", options)
case DialogSaveFile:
var options SaveFileDialogOptions
err := params.ToStruct(&options)
if err != nil {
m.httpError(rw, "Error parsing dialog options: %s", err.Error())
m.httpError(rw, "Invalid dialog call:", fmt.Errorf("error parsing dialog options: %w", err))
return
}
var detached = args.Bool("Detached")
@ -152,17 +152,17 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
defer handlePanic()
file, err := dialog.PromptForSingleSelection()
if err != nil {
m.dialogErrorCallback(window, "Error getting selection: %s", dialogID, err)
m.dialogErrorCallback(window, "Dialog.SaveFile failed", dialogID, fmt.Errorf("error getting selection: %w", err))
return
}
m.dialogCallback(window, dialogID, file, false)
m.Info("Runtime Call:", "method", methodName, "result", file)
m.Info("Runtime call:", "method", methodName, "result", file)
}()
m.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options)
m.Info("Runtime call:", "method", methodName, "options", options)
default:
m.httpError(rw, "Unknown dialog method: %d", method)
m.httpError(rw, "Invalid dialog call:", fmt.Errorf("unknown method: %d", method))
return
}
}

View File

@ -1,7 +1,10 @@
package application
import (
"fmt"
"net/http"
"github.com/pkg/errors"
)
const (
@ -13,28 +16,26 @@ var eventsMethodNames = map[int]string{
}
func (m *MessageProcessor) processEventsMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
var event CustomEvent
switch method {
case EventsEmit:
var event CustomEvent
err := params.ToStruct(&event)
if err != nil {
m.httpError(rw, "Error parsing event: %s", err.Error())
m.httpError(rw, "Invalid events call:", fmt.Errorf("error parsing event: %w", err))
return
}
if event.Name == "" {
m.httpError(rw, "Event name must be specified")
m.httpError(rw, "Invalid events call:", errors.New("missing event name"))
return
}
event.Sender = window.Name()
globalApplication.customEventProcessor.Emit(&event)
m.ok(rw)
m.Info("Runtime call:", "method", "Events."+eventsMethodNames[method], "name", event.Name, "sender", event.Sender, "data", event.Data, "cancelled", event.IsCancelled())
default:
m.httpError(rw, "Unknown event method: %d", method)
m.httpError(rw, "Invalid events call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime Call:", "method", "Events."+eventsMethodNames[method], "name", event.Name, "sender", event.Sender, "data", event.Data, "cancelled", event.IsCancelled())
}

View File

@ -141,6 +141,8 @@ func convertNumber[T int | int8 | int16 | int32 | int64 | uint | uint8 | uint16
result = v
case float64:
result = T(v)
default:
return nil
}
return &result
}
@ -154,6 +156,7 @@ func (a *Args) UInt8(s string) *uint8 {
}
return nil
}
func (a *Args) UInt(s string) *uint {
if a == nil {
return nil
@ -169,8 +172,9 @@ func (a *Args) Float64(s string) *float64 {
return nil
}
if val := a.data[s]; val != nil {
result := val.(float64)
return &result
if result, ok := val.(float64); ok {
return &result
}
}
return nil
}
@ -180,8 +184,9 @@ func (a *Args) Bool(s string) *bool {
return nil
}
if val := a.data[s]; val != nil {
result := val.(bool)
return &result
if result, ok := val.(bool); ok {
return &result
}
}
return nil
}

View File

@ -1,6 +1,7 @@
package application
import (
"fmt"
"net/http"
)
@ -17,33 +18,33 @@ var screensMethodNames = map[int]string{
}
func (m *MessageProcessor) processScreensMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, _ QueryParams) {
switch method {
case ScreensGetAll:
screens, err := globalApplication.GetScreens()
if err != nil {
m.Error("GetAll: %s", err.Error())
m.httpError(rw, "GetScreens failed:", err)
return
}
m.json(rw, screens)
case ScreensGetPrimary:
screen, err := globalApplication.GetPrimaryScreen()
if err != nil {
m.Error("GetPrimary: %s", err.Error())
m.httpError(rw, "GetPrimary failed:", err)
return
}
m.json(rw, screen)
case ScreensGetCurrent:
screen, err := globalApplication.CurrentWindow().GetScreen()
if err != nil {
m.Error("GetCurrent: %s", err.Error())
m.httpError(rw, "Window.GetScreen failed:", err)
return
}
m.json(rw, screen)
default:
m.httpError(rw, "Unknown screens method: %d", method)
m.httpError(rw, "Invalid screens call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime Call:", "method", "Screens."+screensMethodNames[method])
m.Info("Runtime call:", "method", "Screens."+screensMethodNames[method])
}

View File

@ -1,6 +1,7 @@
package application
import (
"fmt"
"net/http"
)
@ -15,16 +16,15 @@ var systemMethodNames = map[int]string{
}
func (m *MessageProcessor) processSystemMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
switch method {
case SystemIsDarkMode:
m.json(rw, globalApplication.IsDarkMode())
case Environment:
m.json(rw, globalApplication.Environment())
default:
m.httpError(rw, "Unknown system method: %d", method)
m.httpError(rw, "Invalid system call:", fmt.Errorf("unknown method: %d", method))
return
}
m.Info("Runtime Call:", "method", "System."+systemMethodNames[method])
m.Info("Runtime call:", "method", "System."+systemMethodNames[method])
}

View File

@ -1,6 +1,8 @@
package application
import (
"errors"
"fmt"
"net/http"
)
@ -107,10 +109,9 @@ var windowMethodNames = map[int]string{
}
func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
args, err := params.Args()
if err != nil {
m.httpError(rw, "Unable to parse arguments: %s", err.Error())
m.httpError(rw, "Invalid window call:", fmt.Errorf("unable to parse arguments: %w", err))
return
}
@ -145,7 +146,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowGetScreen:
screen, err := window.GetScreen()
if err != nil {
m.httpError(rw, err.Error())
m.httpError(rw, "Window.GetScreen failed:", err)
return
}
m.json(rw, screen)
@ -197,18 +198,20 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetPosition:
x := args.Int("x")
if x == nil {
m.Error("Invalid SetPosition Message: 'x' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'x'"))
return
}
y := args.Int("y")
if y == nil {
m.Error("Invalid SetPosition Message: 'y' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'y'"))
return
}
window.SetPosition(*x, *y)
m.ok(rw)
case WindowSetAlwaysOnTop:
alwaysOnTop := args.Bool("alwaysOnTop")
if alwaysOnTop == nil {
m.Error("Invalid SetAlwaysOnTop Message: 'alwaysOnTop' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'alwaysOnTop'"))
return
}
window.SetAlwaysOnTop(*alwaysOnTop)
@ -216,22 +219,22 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetBackgroundColour:
r := args.UInt8("r")
if r == nil {
m.Error("Invalid SetBackgroundColour Message: 'r' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'r'"))
return
}
g := args.UInt8("g")
if g == nil {
m.Error("Invalid SetBackgroundColour Message: 'g' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'g'"))
return
}
b := args.UInt8("b")
if b == nil {
m.Error("Invalid SetBackgroundColour Message: 'b' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'b'"))
return
}
a := args.UInt8("a")
if a == nil {
m.Error("Invalid SetBackgroundColour Message: 'a' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'a'"))
return
}
window.SetBackgroundColour(RGBA{
@ -244,7 +247,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetFrameless:
frameless := args.Bool("frameless")
if frameless == nil {
m.Error("Invalid SetFrameless Message: 'frameless' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'frameless'"))
return
}
window.SetFrameless(*frameless)
@ -252,40 +255,46 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetMaxSize:
width := args.Int("width")
if width == nil {
m.Error("Invalid SetMaxSize Message: 'width' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'width'"))
return
}
height := args.Int("height")
if height == nil {
m.Error("Invalid SetMaxSize Message: 'height' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'height'"))
return
}
window.SetMaxSize(*width, *height)
m.ok(rw)
case WindowSetMinSize:
width := args.Int("width")
if width == nil {
m.Error("Invalid SetMinSize Message: 'width' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'width'"))
return
}
height := args.Int("height")
if height == nil {
m.Error("Invalid SetMinSize Message: 'height' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'height'"))
return
}
window.SetMinSize(*width, *height)
m.ok(rw)
case WindowSetRelativePosition:
x := args.Int("x")
if x == nil {
m.Error("Invalid SetRelativePosition Message: 'x' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'x'"))
return
}
y := args.Int("y")
if y == nil {
m.Error("Invalid SetRelativePosition Message: 'y' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'y'"))
return
}
window.SetRelativePosition(*x, *y)
m.ok(rw)
case WindowSetResizable:
resizable := args.Bool("resizable")
if resizable == nil {
m.Error("Invalid SetResizable Message: 'resizable' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'resizable'"))
return
}
window.SetResizable(*resizable)
@ -293,18 +302,20 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetSize:
width := args.Int("width")
if width == nil {
m.Error("Invalid SetSize Message: 'width' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'width'"))
return
}
height := args.Int("height")
if height == nil {
m.Error("Invalid SetSize Message: 'height' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'height'"))
return
}
window.SetSize(*width, *height)
m.ok(rw)
case WindowSetTitle:
title := args.String("title")
if title == nil {
m.Error("Invalid SetTitle Message: 'title' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing argument 'title'"))
return
}
window.SetTitle(*title)
@ -312,7 +323,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetZoom:
zoom := args.Float64("zoom")
if zoom == nil {
m.Error("Invalid SetZoom Message: 'zoom' value required")
m.httpError(rw, "Invalid window call:", errors.New("missing or invalid argument 'zoom'"))
return
}
window.SetZoom(*zoom)
@ -360,8 +371,9 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
window.ZoomReset()
m.ok(rw)
default:
m.httpError(rw, "Unknown window method id: %d", method)
m.httpError(rw, "Invalid window call:", fmt.Errorf("unknown method %d", method))
return
}
m.Info("Runtime Call:", "method", "Window."+windowMethodNames[method])
m.Info("Runtime call:", "method", "Window."+windowMethodNames[method])
}

View File

@ -73,10 +73,8 @@ func handlePanic(options ...handlePanicOptions) bool {
}
// Get the error
var err error
if errPanic, ok := e.(error); ok {
err = errPanic
} else {
err, ok := e.(error)
if !ok {
err = fmt.Errorf("%v", e)
}
@ -102,6 +100,5 @@ func processPanic(panicDetails *PanicDetails) {
}
func defaultPanicHandler(panicDetails *PanicDetails) {
errorMessage := fmt.Sprintf("panic error: %s\n%s", panicDetails.Error.Error(), panicDetails.StackTrace)
globalApplication.fatal(errorMessage)
globalApplication.fatal("panic error: %w\n%s", panicDetails.Error, panicDetails.StackTrace)
}

View File

@ -1,7 +1,6 @@
package application
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32"
)
@ -133,12 +132,12 @@ func (p *Win32Menu) buildMenu(parentMenu w32.HMENU, inputMenu *Menu) {
}
ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText))
if !ok {
globalApplication.fatal(fmt.Sprintf("Error adding menu item: %s", menuText))
globalApplication.fatal("error adding menu item '%s'", menuText)
}
if item.bitmap != nil {
err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil)
if err != nil {
globalApplication.fatal(fmt.Sprintf("Error setting menu icons: %s", err.Error()))
globalApplication.fatal("error setting menu icons: %w", err)
}
}

View File

@ -3,7 +3,7 @@
package application
import (
"fmt"
"errors"
"strconv"
"github.com/wailsapp/wails/v3/pkg/w32"
@ -80,7 +80,7 @@ func getScreenForWindowHwnd(hwnd w32.HWND) (*Screen, error) {
return screen, nil
}
}
return nil, fmt.Errorf("screen not found for window")
return nil, errors.New("screen not found for window")
}
func hMonitorToScreenID(hMonitor uintptr) string {

View File

@ -1,7 +1,7 @@
package application
import (
"fmt"
"errors"
"math"
"sort"
)
@ -363,7 +363,7 @@ func (s *Screen) physicalToDipRect(physicalRect Rect) Rect {
// for future coordinate transformation between the physical and logical (DIP) space
func (m *ScreenManager) LayoutScreens(screens []*Screen) error {
if screens == nil || len(screens) == 0 {
return fmt.Errorf("screens parameter is nil or empty")
return errors.New("screens parameter is nil or empty")
}
m.screens = screens
@ -397,9 +397,9 @@ func (m *ScreenManager) calculateScreensDipCoordinates() error {
}
}
if m.primaryScreen == nil {
return fmt.Errorf("no primary screen found")
return errors.New("no primary screen found")
} else if len(remainingScreens) != len(m.screens)-1 {
return fmt.Errorf("invalid primary screen found")
return errors.New("invalid primary screen found")
}
// Build screens tree using the primary screen as root

View File

@ -27,6 +27,16 @@ type ServiceOptions struct {
// it will be mounted on the internal asset server
// at the prefix specified by Route.
Route string
// MarshalError will be called if non-nil
// to marshal to JSON the error values returned by this service's methods.
//
// MarshalError is not allowed to fail,
// but it may return a nil slice to fall back
// to the globally configured error handler.
//
// If the returned slice is not nil, it must contain valid JSON.
MarshalError func(error) []byte
}
// DefaultServiceOptions specifies the default values of service options,
@ -72,8 +82,17 @@ type ServiceName interface {
// The context will be valid as long as the application is running,
// and will be canceled right before shutdown.
//
// If the return value is non-nil, it is logged along with the service name,
// the startup process aborts and the application quits.
// Services are guaranteed to receive the startup notification
// in the exact order in which they were either
// listed in [Options.Services] or registered with [App.RegisterService],
// with those from [Options.Services] coming first.
//
// If the return value is non-nil, the startup process aborts
// and [App.Run] returns the error wrapped with [fmt.Errorf]
// in a user-friendly message comprising the service name.
// The original error can be retrieved either by calling the Unwrap method
// or through the [errors.As] API.
//
// When that happens, service instances that have been already initialised
// receive a shutdown notification.
type ServiceStartup interface {
@ -83,17 +102,33 @@ type ServiceStartup interface {
// ServiceShutdown is an *optional* method that may be implemented by service instances.
//
// This method will be called during application shutdown. It can be used for cleaning up resources.
// If a service has received a startup notification,
// then it is guaranteed to receive a shutdown notification too,
// except in case of unhandled panics during shutdown.
//
// If the return value is non-nil, it is logged along with the service name.
// Services receive shutdown notifications in reverse registration order,
// after all user-provided shutdown hooks have run (see [App.OnShutdown]).
//
// If the return value is non-nil, it is passed to the application's
// configured error handler at [Options.ErrorHandler],
// wrapped with [fmt.Errorf] in a user-friendly message comprising the service name.
// The default behaviour is to log the error along with the service name.
// The original error can be retrieved either by calling the Unwrap method
// or through the [errors.As] API.
type ServiceShutdown interface {
ServiceShutdown() error
}
func getServiceName(service any) string {
// First check it conforms to ServiceName interface
if serviceName, ok := service.(ServiceName); ok {
return serviceName.ServiceName()
func getServiceName(service Service) string {
if service.options.Name != "" {
return service.options.Name
}
// Next, get the name from the type
return reflect.TypeOf(service).String()
// Check if the service implements the ServiceName interface
if s, ok := service.Instance().(ServiceName); ok {
return s.ServiceName()
}
// Finally, get the name from the type.
return reflect.TypeOf(service.Instance()).Elem().String()
}

View File

@ -3,12 +3,13 @@
package application
import (
"fmt"
"github.com/godbus/dbus/v5"
"errors"
"os"
"strings"
"sync"
"syscall"
"github.com/godbus/dbus/v5"
)
type dbusHandler func(string)
@ -36,7 +37,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
func (l *linuxLock) acquire(uniqueID string) error {
if uniqueID == "" {
return fmt.Errorf("UniqueID is required for single instance lock")
return errors.New("UniqueID is required for single instance lock")
}
id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
@ -56,11 +57,11 @@ func (l *linuxLock) acquire(uniqueID string) error {
secondInstanceBuffer <- message
})
err := conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName)
if err != nil {
globalApplication.error(err.Error())
}
err = conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName)
})
if err != nil {
return err
}
reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue)
if err != nil {

View File

@ -4,11 +4,11 @@ package application
import (
"errors"
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32"
"golang.org/x/sys/windows"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
"golang.org/x/sys/windows"
)
var (
@ -33,7 +33,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
func (l *windowsLock) acquire(uniqueID string) error {
if uniqueID == "" {
return fmt.Errorf("UniqueID is required for single instance lock")
return errors.New("UniqueID is required for single instance lock")
}
l.uniqueID = uniqueID

View File

@ -1,7 +1,7 @@
package application
import (
"fmt"
"errors"
"runtime"
"sync"
"time"
@ -123,7 +123,7 @@ func (s *SystemTray) Run() {
func (s *SystemTray) PositionWindow(window *WebviewWindow, offset int) error {
if s.impl == nil {
return fmt.Errorf("system tray not running")
return errors.New("system tray not running")
}
return InvokeSyncWithError(func() error {
return s.impl.positionWindow(window, offset)

View File

@ -30,9 +30,9 @@ static void systemTrayHide(void* nsStatusItem) {
*/
import "C"
import (
"errors"
"unsafe"
"fmt"
"github.com/leaanthony/go-ansi-parser"
)
@ -125,7 +125,7 @@ func (s *macosSystemTray) getScreen() (*Screen, error) {
}
return result, nil
}
return nil, fmt.Errorf("no screen available")
return nil, errors.New("no screen available")
}
func (s *macosSystemTray) bounds() (*Rect, error) {

View File

@ -9,13 +9,14 @@ package application
import "C"
import (
"fmt"
"os"
"github.com/godbus/dbus/v5"
"github.com/godbus/dbus/v5/introspect"
"github.com/godbus/dbus/v5/prop"
"github.com/wailsapp/wails/v3/internal/dbus/menu"
"github.com/wailsapp/wails/v3/internal/dbus/notifier"
"github.com/wailsapp/wails/v3/pkg/icons"
"os"
)
const (
@ -178,7 +179,7 @@ func (s *linuxSystemTray) refresh() {
s.menuVersion++
if err := s.menuProps.Set("com.canonical.dbusmenu", "Version",
dbus.MakeVariant(s.menuVersion)); err != nil {
globalApplication.error("systray error: failed to update menu version: %v", err)
globalApplication.error("systray error: failed to update menu version: %w", err)
return
}
if err := menu.Emit(s.conn, &menu.Dbusmenu_LayoutUpdatedSignal{
@ -187,7 +188,7 @@ func (s *linuxSystemTray) refresh() {
Revision: s.menuVersion,
},
}); err != nil {
globalApplication.error("systray error: failed to emit layout updated signal: %v", err)
globalApplication.error("systray error: failed to emit layout updated signal: %w", err)
}
}
@ -270,34 +271,34 @@ func (s *linuxSystemTray) bounds() (*Rect, error) {
func (s *linuxSystemTray) run() {
conn, err := dbus.SessionBus()
if err != nil {
globalApplication.error("systray error: failed to connect to DBus: %v\n", err)
globalApplication.error("systray error: failed to connect to DBus: %w\n", err)
return
}
err = notifier.ExportStatusNotifierItem(conn, itemPath, s)
if err != nil {
globalApplication.error("systray error: failed to export status notifier item: %v\n", err)
globalApplication.error("systray error: failed to export status notifier item: %w\n", err)
}
err = menu.ExportDbusmenu(conn, menuPath, s)
if err != nil {
globalApplication.error("systray error: failed to export status notifier menu: %v", err)
globalApplication.error("systray error: failed to export status notifier menu: %w", err)
return
}
name := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid()) // register id 1 for this process
_, err = conn.RequestName(name, dbus.NameFlagDoNotQueue)
if err != nil {
globalApplication.error("systray error: failed to request name: %s\n", err)
globalApplication.error("systray error: failed to request name: %w", err)
// it's not critical error: continue
}
props, err := prop.Export(conn, itemPath, s.createPropSpec())
if err != nil {
globalApplication.error("systray error: failed to export notifier item properties to bus: %s\n", err)
globalApplication.error("systray error: failed to export notifier item properties to bus: %w", err)
return
}
menuProps, err := prop.Export(conn, menuPath, s.createMenuPropSpec())
if err != nil {
globalApplication.error("systray error: failed to export notifier menu properties to bus: %s\n", err)
globalApplication.error("systray error: failed to export notifier menu properties to bus: %w", err)
return
}
@ -315,7 +316,7 @@ func (s *linuxSystemTray) run() {
}
err = conn.Export(introspect.NewIntrospectable(&node), itemPath, "org.freedesktop.DBus.Introspectable")
if err != nil {
globalApplication.error("systray error: failed to export node introspection: %s\n", err)
globalApplication.error("systray error: failed to export node introspection: %w", err)
return
}
menuNode := introspect.Node{
@ -329,7 +330,7 @@ func (s *linuxSystemTray) run() {
err = conn.Export(introspect.NewIntrospectable(&menuNode), menuPath,
"org.freedesktop.DBus.Introspectable")
if err != nil {
globalApplication.error("systray error: failed to export menu node introspection: %s\n", err)
globalApplication.error("systray error: failed to export menu node introspection: %w", err)
return
}
s.setLabel(s.label)
@ -344,7 +345,7 @@ func (s *linuxSystemTray) run() {
dbus.WithMatchMember("NameOwnerChanged"),
dbus.WithMatchArg(0, "org.kde.StatusNotifierWatcher"),
); err != nil {
globalApplication.error("systray error: failed to register signal matching: %v\n", err)
globalApplication.error("systray error: failed to register signal matching: %w", err)
return
}
@ -388,7 +389,7 @@ func (s *linuxSystemTray) setIcon(icon []byte) {
iconPx, err := iconToPX(icon)
if err != nil {
globalApplication.error("systray error: failed to convert icon to PX: %s\n", err)
globalApplication.error("systray error: failed to convert icon to PX: %w", err)
return
}
s.props.SetMust("org.kde.StatusNotifierItem", "IconPixmap", []PX{iconPx})
@ -402,7 +403,7 @@ func (s *linuxSystemTray) setIcon(icon []byte) {
Body: &notifier.StatusNotifierItem_NewIconSignalBody{},
})
if err != nil {
globalApplication.error("systray error: failed to emit new icon signal: %s\n", err)
globalApplication.error("systray error: failed to emit new icon signal: %w", err)
return
}
}
@ -445,7 +446,7 @@ func (s *linuxSystemTray) setLabel(label string) {
s.label = label
if err := s.props.Set("org.kde.StatusNotifierItem", "Title", dbus.MakeVariant(label)); err != nil {
globalApplication.error("systray error: failed to set Title prop: %s\n", err)
globalApplication.error("systray error: failed to set Title prop: %w", err)
return
}
@ -457,7 +458,7 @@ func (s *linuxSystemTray) setLabel(label string) {
Path: itemPath,
Body: &notifier.StatusNotifierItem_NewTitleSignalBody{},
}); err != nil {
globalApplication.error("systray error: failed to emit new title signal: %s", err)
globalApplication.error("systray error: failed to emit new title signal: %w", err)
return
}
@ -591,7 +592,7 @@ func (s *linuxSystemTray) register() bool {
obj := s.conn.Object("org.kde.StatusNotifierWatcher", "/StatusNotifierWatcher")
call := obj.Call("org.kde.StatusNotifierWatcher.RegisterStatusNotifierItem", 0, itemPath)
if call.Err != nil {
globalApplication.error("systray error: failed to register: %v\n", call.Err)
globalApplication.error("systray error: failed to register: %w", call.Err)
return false
}

View File

@ -3,12 +3,13 @@
package application
import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/icons"
"errors"
"syscall"
"time"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/icons"
"github.com/samber/lo"
"github.com/wailsapp/wails/v3/pkg/events"
@ -120,7 +121,7 @@ func (s *windowsSystemTray) bounds() (*Rect, error) {
monitor := w32.MonitorFromWindow(s.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 {
return nil, fmt.Errorf("failed to get monitor")
return nil, errors.New("failed to get monitor")
}
return &Rect{
@ -186,7 +187,7 @@ func (s *windowsSystemTray) run() {
for retries := range 6 {
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
if retries == 5 {
globalApplication.fatal("Failed to register system tray icon: %v", syscall.GetLastError())
globalApplication.fatal("failed to register system tray icon: %w", syscall.GetLastError())
}
time.Sleep(500 * time.Millisecond)

View File

@ -1,13 +1,13 @@
package application
import (
"encoding/json"
"errors"
"fmt"
"runtime"
"slices"
"strings"
"sync"
"text/template"
"github.com/leaanthony/u"
@ -276,7 +276,7 @@ func processKeyBindingOptions(keyBindings map[string]func(window *WebviewWindow)
// Parse the key to an accelerator
acc, err := parseAccelerator(key)
if err != nil {
globalApplication.error("Invalid keybinding: %s", err.Error())
globalApplication.error("invalid keybinding: %w", err)
continue
}
result[acc.String()] = callback
@ -291,40 +291,27 @@ func (w *WebviewWindow) addCancellationFunction(canceller func()) {
w.cancellers = append(w.cancellers, canceller)
}
// formatJS ensures the 'data' provided marshals to valid json or panics
func (w *WebviewWindow) formatJS(f string, callID string, data string) string {
j, err := json.Marshal(data)
if err != nil {
panic(err)
}
return fmt.Sprintf(f, callID, j)
}
func (w *WebviewWindow) CallError(callID string, result string) {
func (w *WebviewWindow) CallError(callID string, result string, isJSON bool) {
if w.impl != nil {
w.impl.execJS(w.formatJS("_wails.callErrorHandler('%s', %s);", callID, result))
w.impl.execJS(fmt.Sprintf("_wails.callErrorHandler('%s', '%s', %t);", callID, template.JSEscapeString(result), isJSON))
}
}
func (w *WebviewWindow) CallResponse(callID string, result string) {
if w.impl != nil {
w.impl.execJS(w.formatJS("_wails.callResultHandler('%s', %s, true);", callID, result))
w.impl.execJS(fmt.Sprintf("_wails.callResultHandler('%s', '%s', true);", callID, template.JSEscapeString(result)))
}
}
func (w *WebviewWindow) DialogError(dialogID string, result string) {
if w.impl != nil {
w.impl.execJS(w.formatJS("_wails.dialogErrorCallback('%s', %s);", dialogID, result))
w.impl.execJS(fmt.Sprintf("_wails.dialogErrorCallback('%s', '%s');", dialogID, template.JSEscapeString(result)))
}
}
func (w *WebviewWindow) DialogResponse(dialogID string, result string, isJSON bool) {
if w.impl != nil {
if isJSON {
w.impl.execJS(w.formatJS("_wails.dialogResultCallback('%s', %s, true);", dialogID, result))
} else {
w.impl.execJS(fmt.Sprintf("_wails.dialogResultCallback('%s', '%s', false);", dialogID, result))
}
w.impl.execJS(fmt.Sprintf("_wails.dialogResultCallback('%s', '%s', %t);", dialogID, template.JSEscapeString(result), isJSON))
}
}
@ -690,7 +677,7 @@ func (w *WebviewWindow) HandleMessage(message string) {
InvokeSync(func() {
err := w.startDrag()
if err != nil {
w.Error("Failed to start drag: %s", err)
w.Error("failed to start drag: %w", err)
}
})
}
@ -698,12 +685,12 @@ func (w *WebviewWindow) HandleMessage(message string) {
if !w.IsFullscreen() {
sl := strings.Split(message, ":")
if len(sl) != 3 {
w.Error("Unknown message returned from dispatcher", "message", message)
w.Error("unknown message returned from dispatcher: %s", message)
return
}
err := w.startResize(sl[2])
if err != nil {
w.Error(err.Error())
w.Error("%w", err)
}
}
case message == "wails:runtime:ready":
@ -714,7 +701,7 @@ func (w *WebviewWindow) HandleMessage(message string) {
w.ExecJS(js)
}
default:
w.Error("Unknown message sent via 'invoke' on frontend: %v", message)
w.Error("unknown message sent via 'invoke' on frontend: %v", message)
}
}
@ -1162,10 +1149,8 @@ func (w *WebviewWindow) Info(message string, args ...any) {
}
func (w *WebviewWindow) Error(message string, args ...any) {
var messageArgs []interface{}
messageArgs = append(messageArgs, args...)
messageArgs = append(messageArgs, "sender", w.Name())
globalApplication.error(message, messageArgs...)
args = append([]any{w.Name()}, args...)
globalApplication.error("in window '%s': "+message, args...)
}
func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) {
@ -1182,7 +1167,7 @@ func (w *WebviewWindow) OpenContextMenu(data *ContextMenuData) {
// try application level context menu
menu, ok := globalApplication.getContextMenu(data.Id)
if !ok {
w.Error("No context menu found for id: %s", data.Id)
w.Error("no context menu found for id: %s", data.Id)
return
}
menu.setContextData(data)

View File

@ -832,7 +832,7 @@ func (w *macosWebviewWindow) handleKeyEvent(acceleratorString string) {
// Parse acceleratorString
accelerator, err := parseAccelerator(acceleratorString)
if err != nil {
globalApplication.error("unable to parse accelerator: %s", err.Error())
globalApplication.error("unable to parse accelerator: %w", err)
return
}
w.parent.processKeyBinding(accelerator.String())
@ -1038,7 +1038,11 @@ func (w *macosWebviewWindow) setEnabled(enabled bool) {
func (w *macosWebviewWindow) execJS(js string) {
InvokeAsync(func() {
if globalApplication.performingShutdown {
globalApplication.shutdownLock.Lock()
performingShutdown := globalApplication.performingShutdown
globalApplication.shutdownLock.Unlock()
if performingShutdown {
return
}
if w.nsWindow == nil {
@ -1264,7 +1268,7 @@ func (w *macosWebviewWindow) run() {
startURL, err := assetserver.GetStartURL(options.URL)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
w.setURL(startURL)

View File

@ -324,7 +324,7 @@ func (w *linuxWebviewWindow) run() {
startURL, err := assetserver.GetStartURL(w.parent.options.URL)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
w.setURL(startURL)
@ -380,7 +380,7 @@ func (w *linuxWebviewWindow) handleKeyEvent(acceleratorString string) {
// Parse acceleratorString
// accelerator, err := parseAccelerator(acceleratorString)
// if err != nil {
// globalApplication.error("unable to parse accelerator: %s", err.Error())
// globalApplication.error("unable to parse accelerator: %w", err)
// return
// }
w.parent.processKeyBinding(acceleratorString)

View File

@ -183,7 +183,7 @@ func (w *windowsWebviewWindow) print() error {
func (w *windowsWebviewWindow) startResize(border string) error {
if !w32.ReleaseCapture() {
return fmt.Errorf("unable to release mouse capture")
return errors.New("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until resizing has been finished.
w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, edgeMap[border], 0)
@ -192,7 +192,7 @@ func (w *windowsWebviewWindow) startResize(border string) error {
func (w *windowsWebviewWindow) startDrag() error {
if !w32.ReleaseCapture() {
return fmt.Errorf("unable to release mouse capture")
return errors.New("unable to release mouse capture")
}
// Use PostMessage because we don't want to block the caller until dragging has been finished.
w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0)
@ -334,7 +334,7 @@ func (w *windowsWebviewWindow) run() {
nil)
if w.hwnd == 0 {
globalApplication.fatal("Unable to create window")
globalApplication.fatal("unable to create window")
}
// Ensure correct window size in case the scale factor of current screen is different from the initial one.
@ -1465,7 +1465,7 @@ func (w *windowsWebviewWindow) setWindowMask(imageData []byte) {
data, err := pngToImage(imageData)
if err != nil {
globalApplication.fatal("Fatal error in callback setWindowMask: " + err.Error())
globalApplication.fatal("fatal error in callback setWindowMask: %w", err)
}
bitmap, err := w32.CreateHBITMAPFromImage(data)
@ -1513,15 +1513,15 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ")
err = reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent)
if err != nil {
globalApplication.fatal("Error setting UserAgent header: " + err.Error())
globalApplication.fatal("error setting UserAgent header: %w", err)
}
err = reqHeaders.SetHeader(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(w.parent.id), 10))
if err != nil {
globalApplication.fatal("Error setting WindowId header: " + err.Error())
globalApplication.fatal("error setting WindowId header: %w", err)
}
err = reqHeaders.Release()
if err != nil {
globalApplication.fatal("Error releasing headers: " + err.Error())
globalApplication.fatal("error releasing headers: %w", err)
}
}
@ -1534,7 +1534,7 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
uri, _ := req.GetUri()
reqUri, err := url.ParseRequestURI(uri)
if err != nil {
globalApplication.error("Unable to parse request uri: uri='%s' error='%s'", uri, err)
globalApplication.error("unable to parse request uri: uri='%s' error='%w'", uri, err)
return
}
@ -1553,7 +1553,7 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
InvokeSync(fn)
})
if err != nil {
globalApplication.error("%s: NewRequest failed: %s", uri, err)
globalApplication.error("%s: NewRequest failed: %w", uri, err)
return
}
@ -1572,7 +1572,7 @@ func (w *windowsWebviewWindow) setupChromium() {
webview2version, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(globalApplication.options.Windows.WebviewBrowserPath)
if err != nil {
globalApplication.error("Error getting WebView2 version: " + err.Error())
globalApplication.error("error getting WebView2 version: %w", err)
return
}
globalApplication.capabilities = capabilities.NewCapabilities(webview2version)
@ -1614,14 +1614,14 @@ func (w *windowsWebviewWindow) setupChromium() {
if chromium.HasCapability(edge.SwipeNavigation) {
err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
if chromium.HasCapability(edge.AllowExternalDrop) {
err := chromium.AllowExternalDrag(false)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
if w.parent.options.EnableDragAndDrop {
@ -1657,7 +1657,7 @@ func (w *windowsWebviewWindow) setupChromium() {
//if windowName == "Chrome_RenderWidgetHostHWND" {
err := w32.RegisterDragDrop(hwnd, w.dropTarget)
if err != nil && !errors.Is(err, syscall.Errno(w32.DRAGDROP_E_ALREADYREGISTERED)) {
globalApplication.error("Error registering drag and drop: " + err.Error())
globalApplication.error("error registering drag and drop: %w", err)
}
//}
return 1
@ -1672,7 +1672,7 @@ func (w *windowsWebviewWindow) setupChromium() {
// warning
globalApplication.warning("unsupported capability: GeneralAutofillEnabled")
} else {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
}
@ -1683,7 +1683,7 @@ func (w *windowsWebviewWindow) setupChromium() {
if errors.Is(edge.UnsupportedCapabilityError, err) {
globalApplication.warning("unsupported capability: PasswordAutosaveEnabled")
} else {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}
}
@ -1692,7 +1692,7 @@ func (w *windowsWebviewWindow) setupChromium() {
//if chromium.HasCapability(edge.AllowExternalDrop) {
// err := chromium.AllowExternalDrag(w.parent.options.EnableDragAndDrop)
// if err != nil {
// globalApplication.fatal(err.Error())
// globalApplication.handleFatalError(err)
// }
// if w.parent.options.EnableDragAndDrop {
// chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects
@ -1702,14 +1702,14 @@ func (w *windowsWebviewWindow) setupChromium() {
chromium.Resize()
settings, err := chromium.GetSettings()
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
if settings == nil {
globalApplication.fatal("Error getting settings")
globalApplication.fatal("error getting settings")
}
err = settings.PutAreDefaultContextMenusEnabled(debugMode || !w.parent.options.DefaultContextMenuDisabled)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
w.enableDevTools(settings)
@ -1719,20 +1719,20 @@ func (w *windowsWebviewWindow) setupChromium() {
}
err = settings.PutIsZoomControlEnabled(w.parent.options.ZoomControlEnabled)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
err = settings.PutIsStatusBarEnabled(false)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
err = settings.PutAreBrowserAcceleratorKeysEnabled(false)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
err = settings.PutIsSwipeNavigationEnabled(false)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
if debugMode && w.parent.options.OpenInspectorOnStartup {
@ -1761,7 +1761,7 @@ func (w *windowsWebviewWindow) setupChromium() {
} else {
startURL, err := assetserver.GetStartURL(w.parent.options.URL)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
w.webviewNavigationCompleted = false
chromium.Navigate(startURL)
@ -1772,7 +1772,7 @@ func (w *windowsWebviewWindow) setupChromium() {
func (w *windowsWebviewWindow) fullscreenChanged(sender *edge.ICoreWebView2, _ *edge.ICoreWebView2ContainsFullScreenElementChangedEventArgs) {
isFullscreen, err := sender.GetContainsFullScreenElement()
if err != nil {
globalApplication.fatal("Fatal error in callback fullscreenChanged: " + err.Error())
globalApplication.fatal("fatal error in callback fullscreenChanged: %w", err)
}
if isFullscreen {
w.fullscreen()
@ -1817,11 +1817,11 @@ func (w *windowsWebviewWindow) navigationCompleted(sender *edge.ICoreWebView2, a
// Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026
err := w.chromium.Hide()
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
err = w.chromium.Show()
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
if wasFocused {
w.focus()
@ -1839,7 +1839,7 @@ func (w *windowsWebviewWindow) processKeyBinding(vkey uint) bool {
// Get the keyboard state and convert to an accelerator
var keyState [256]byte
if !w32.GetKeyboardState(keyState[:]) {
globalApplication.error("Error getting keyboard state")
globalApplication.error("error getting keyboard state")
return false
}
@ -1890,20 +1890,20 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
if strings.HasPrefix(message, "FilesDropped") {
objs, err := args.GetAdditionalObjects()
if err != nil {
globalApplication.error(err.Error())
globalApplication.handleError(err)
return
}
defer func() {
err = objs.Release()
if err != nil {
globalApplication.error("Error releasing objects: " + err.Error())
globalApplication.error("error releasing objects: %w", err)
}
}()
count, err := objs.GetCount()
if err != nil {
globalApplication.error("cannot get count: %s", err.Error())
globalApplication.error("cannot get count: %w", err)
return
}
@ -1911,7 +1911,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
for i := uint32(0); i < count; i++ {
_file, err := objs.GetValueAtIndex(i)
if err != nil {
globalApplication.error("cannot get value at %d : %s", i, err.Error())
globalApplication.error("cannot get value at %d: %w", i, err)
return
}
@ -1922,7 +1922,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
filepath, err := file.GetPath()
if err != nil {
globalApplication.error("cannot get path for object at %d : %s", i, err.Error())
globalApplication.error("cannot get path for object at %d: %w", i, err)
return
}
@ -1983,7 +1983,7 @@ func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error
var err error
var result w32.HICON
if result = w32.LoadIconWithResourceID(instance, resId); result == 0 {
err = errors.New(fmt.Sprintf("Cannot load icon from resource with id %v", resId))
err = fmt.Errorf("cannot load icon from resource with id %v", resId)
}
return result, err
}

View File

@ -11,6 +11,6 @@ func (w *windowsWebviewWindow) openDevTools() {
func (w *windowsWebviewWindow) enableDevTools(settings *edge.ICoreWebViewSettings) {
err := settings.PutAreDevToolsEnabled(true)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}

View File

@ -9,6 +9,6 @@ func (w *windowsWebviewWindow) openDevTools() {}
func (w *windowsWebviewWindow) enableDevTools(settings *edge.ICoreWebViewSettings) {
err := settings.PutAreDevToolsEnabled(false)
if err != nil {
globalApplication.fatal(err.Error())
globalApplication.handleFatalError(err)
}
}

View File

@ -5,7 +5,7 @@ import (
)
type Callback interface {
CallError(callID string, result string)
CallError(callID string, result string, isJSON bool)
CallResponse(callID string, result string)
DialogError(dialogID string, result string)
DialogResponse(dialogID string, result string, isJSON bool)