5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-04 16:59:31 +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 fail-fast: false
matrix: matrix:
os: [windows-latest, macos-latest, ubuntu-latest] os: [windows-latest, macos-latest, ubuntu-latest]
go-version: [1.23] go-version: [1.24]
steps: steps:
- name: Checkout code - name: Checkout code
@ -40,7 +40,7 @@ jobs:
uses: awalsh128/cache-apt-pkgs-action@latest uses: awalsh128/cache-apt-pkgs-action@latest
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
with: 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 version: 1.0
- name: Setup Go - name: Setup Go
@ -66,11 +66,25 @@ jobs:
working-directory: ./v3 working-directory: ./v3
run: go test -v ./... run: go test -v ./...
- name: Run tests (!mac) - name: Run tests (windows)
if: matrix.os != 'macos-latest' if: matrix.os == 'windows-latest'
working-directory: ./v3 working-directory: ./v3
run: go test -v ./... 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: test_js:
name: Run JS Tests name: Run JS Tests
needs: check_approval needs: check_approval
@ -105,41 +119,23 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macos-latest] os: [ubuntu-latest, windows-latest, macos-latest]
template: template:
[ - svelte
svelte, - svelte-ts
svelte-ts, - vue
vue, - vue-ts
vue-ts, - react
react, - react-ts
react-ts, - preact
preact, - preact-ts
preact-ts, - lit
lit, - lit-ts
lit-ts, - vanilla
vanilla, - vanilla-ts
vanilla-ts, go-version: [1.24]
]
go-version: [1.23]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 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 - name: Install linux dependencies
uses: awalsh128/cache-apt-pkgs-action@latest uses: awalsh128/cache-apt-pkgs-action@latest
if: matrix.os == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
@ -147,15 +143,26 @@ jobs:
packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config packages: libgtk-3-dev libwebkit2gtk-4.1-dev build-essential pkg-config
version: 1.0 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 - name: Build Wails3 CLI
working-directory: ./v3
run: | run: |
cd ./v3/cmd/wails3 task install
go install wails3 doctor
wails3 -help
- name: Generate template '${{ matrix.template }}' - name: Generate template '${{ matrix.template }}'
run: | run: |
go install github.com/go-task/task/v3/cmd/task@latest
mkdir -p ./test-${{ matrix.template }} mkdir -p ./test-${{ matrix.template }}
cd ./test-${{ matrix.template }} cd ./test-${{ matrix.template }}
wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }} wails3 init -n ${{ matrix.template }} -t ${{ matrix.template }}

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 `//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 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 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 ### 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) - 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 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 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 ### 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) - 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) - 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) - 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 ## 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 // This will cause the context to be cancelled in the Go method
promise.cancel(); 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 ## Registering a Service
To register a service with the application, you need to provide an instance of 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 the service to the `Services` field of the `application.Options` struct.
services need to be wrapped by an `application.NewService` call. Here's an All services need to be wrapped by an `application.NewService` call.
example: Here's an example:
```go ```go
app := application.New(application.Options{ app := application.New(application.Options{
@ -70,6 +70,25 @@ ServiceOptions has the following fields:
- Name - Specify a custom name for the Service - Name - Specify a custom name for the Service
- Route - A route to bind the Service to the frontend (more on this below) - 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 ## Optional Methods
Services can implement optional methods to hook into the application lifecycle. 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 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. initialize resources, set up connections, or perform any necessary setup tasks.
The context is the application context, and the `options` parameter provides The context is the application context that will be canceled upon shutdown,
additional information about the service. 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 ### 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 This method is called when the application is shutting down. Use it to clean up
resources, close connections, or perform any necessary cleanup tasks. 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 ### ServeHTTP
```go ```go

View File

@ -1,17 +1,17 @@
module changeme module changeme
go 1.23.4 go 1.24.0
require github.com/wailsapp/wails/v3 v3.0.0-alpha.0 require github.com/wailsapp/wails/v3 v3.0.0-alpha.0
require ( require (
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/bep/debounce v1.2.1 // 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/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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-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/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.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/kevinburke/ssh_config v1.2.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.4 // indirect github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // 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/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
github.com/modern-go/reflect2 v1.0.2 // 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/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.12.0 // 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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/stretchr/testify v1.10.0 // indirect github.com/stretchr/testify v1.10.0 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // 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/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 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 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 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 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.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.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.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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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= 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/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 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= 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 h1:fzEj70K1L58uyoePQxKe+ezDZJ5pybiWGdA0JeFvvyw=
github.com/lmittmann/tint v1.0.0/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 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.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lmittmann/tint v1.0.4/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 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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/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 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-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/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.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 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/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 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 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.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 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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.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.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 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 h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 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-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-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-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 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.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-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-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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.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.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.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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=

View File

@ -1,21 +1,21 @@
module changeme module changeme
go 1.23.4 go 1.24.0
require github.com/wailsapp/wails/v3 v3.0.0-alpha.7 require github.com/wailsapp/wails/v3 v3.0.0-alpha.7
require ( require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // 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/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.5.0 // indirect github.com/cloudflare/circl v1.6.0 // indirect
github.com/cyphar/filepath-securejoin v0.4.0 // indirect github.com/cyphar/filepath-securejoin v0.4.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/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // 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-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/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.0 // indirect github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.0.4 // indirect github.com/lmittmann/tint v1.0.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.3.1 // indirect github.com/pjbgf/sha1cd v0.3.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/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/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/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.10.0 // indirect golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.29.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/tools v0.29.0 // indirect golang.org/x/tools v0.30.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // 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 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 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.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 h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 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 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 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 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.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 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 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.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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes=
github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 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 h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 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 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.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.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 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 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= 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/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 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI=
github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= 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 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc=
github.com/lmittmann/tint v1.0.4/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 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 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 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 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.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 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-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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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 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/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 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 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 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 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= 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 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 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.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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 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 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 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.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 h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= 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-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.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.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 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 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.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-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-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 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.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-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.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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 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.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.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-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-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.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-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.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 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 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.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.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= 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 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-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -19,12 +19,15 @@ type RuntimeHandler interface {
HandleRuntimeCall(w http.ResponseWriter, r *http.Request) HandleRuntimeCall(w http.ResponseWriter, r *http.Request)
} }
type service struct {
Route string
Handler http.Handler
}
type AssetServer struct { type AssetServer struct {
options *Options options *Options
handler http.Handler
handler http.Handler services []service
services map[string]http.Handler
assetServerWebView assetServerWebView
} }
@ -112,11 +115,11 @@ func (a *AssetServer) serveHTTP(rw http.ResponseWriter, req *http.Request, userH
userHandler.ServeHTTP(wrapped, req) userHandler.ServeHTTP(wrapped, req)
default: default:
// Check if the path matches the keys in the services map // Check if the path matches a service route
for route, handler := range a.services { for _, svc := range a.services {
if strings.HasPrefix(reqPath, route) { if strings.HasPrefix(reqPath, svc.Route) {
req.URL.Path = strings.TrimPrefix(reqPath, route) req.URL.Path = strings.TrimPrefix(reqPath, svc.Route)
handler.ServeHTTP(rw, req) svc.Handler.ServeHTTP(rw, req)
return 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) { func (a *AssetServer) AttachServiceHandler(route string, handler http.Handler) {
if a.services == nil { a.services = append(a.services, service{route, handler})
a.services = make(map[string]http.Handler)
}
a.services[prefix] = handler
} }
func (a *AssetServer) writeBlob(rw http.ResponseWriter, filename string, blob []byte) { 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 ( import (
"fmt" "fmt"
"github.com/samber/lo"
"runtime/debug" "runtime/debug"
"slices"
"github.com/samber/lo"
) )
type Info struct { type Info struct {
@ -29,7 +31,9 @@ func Get() (*Info, error) {
return setting.Key, setting.Value return setting.Key, setting.Value
}) })
result.Version = BuildInfo.Main.Version 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 return &result, nil

View File

@ -2,14 +2,15 @@ package commands
import ( import (
"fmt" "fmt"
"os"
"os/exec"
"path/filepath"
"github.com/pterm/pterm" "github.com/pterm/pterm"
"github.com/wailsapp/wails/v3/internal/debug" "github.com/wailsapp/wails/v3/internal/debug"
"github.com/wailsapp/wails/v3/internal/github" "github.com/wailsapp/wails/v3/internal/github"
"github.com/wailsapp/wails/v3/internal/term" "github.com/wailsapp/wails/v3/internal/term"
"github.com/wailsapp/wails/v3/internal/version" "github.com/wailsapp/wails/v3/internal/version"
"os"
"os/exec"
"path/filepath"
) )
type UpdateCLIOptions struct { type UpdateCLIOptions struct {
@ -31,9 +32,9 @@ func UpdateCLI(options *UpdateCLIOptions) error {
v3Path := filepath.ToSlash(debug.LocalModulePath + "/v3") 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("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(" - git pull")
term.Println(" - go install") term.Println(" - wails3 task install")
term.Println("") 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 return nil
} }
@ -69,7 +70,7 @@ func UpdateCLI(options *UpdateCLIOptions) error {
if err != nil { if err != nil {
pterm.Println("") pterm.Println("")
pterm.Println("No stable release found for this major version. To update to the latest pre-release (eg beta), run:") 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 return nil
} }
} }
@ -142,7 +143,7 @@ func updateToVersion(targetVersion *github.SemanticVersion, force bool, currentV
// Compare // Compare
if !success { if !success {
pterm.Println("Error: The requested version is lower than the current version.") 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 return nil
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -48,7 +48,17 @@ function generateID() {
function resultHandler(id, data, isJSON) { function resultHandler(id, data, isJSON) {
const promiseHandler = getAndDeleteResponse(id); const promiseHandler = getAndDeleteResponse(id);
if (promiseHandler) { 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. * Handles the error from a call request.
* *
* @param {string} id - The id of the promise handler. * @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} * @return {void}
*/ */
function errorHandler(id, message) { function errorHandler(id, data, isJSON) {
const promiseHandler = getAndDeleteResponse(id); const promiseHandler = getAndDeleteResponse(id);
if (promiseHandler) { 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. * @typedef {Object} CallOptions
* @param {Object} [options={}] - Additional options for the call. * @property {number} [methodID] - The numeric ID of the bound method to 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. * @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 id = generateID();
const doCancel = () => { return cancelCall(type, {"call-id": id}) }; const doCancel = () => { return cancelCall(type, {"call-id": id}) };
let queuedCancel = false, callRunning = false; let queuedCancel = false, callRunning = false;
let p = new Promise((resolve, reject) => { let p = new Promise((resolve, reject) => {
options["call-id"] = id; options["call-id"] = id;
callResponses.set(id, { resolve, reject }); callResponses.set(id, { resolve, reject });
call(type, options). call(CallBinding, options).then((_) => {
then((_) => { callRunning = true;
callRunning = true; if (queuedCancel) {
if (queuedCancel) { return doCancel();
return doCancel(); }
} }).catch((error) => {
}). reject(error);
catch((error) => { callResponses.delete(id);
reject(error); });
callResponses.delete(id);
});
}); });
p.cancel = () => { p.cancel = () => {
if (callRunning) { if (callRunning) {
@ -118,57 +191,31 @@ function callBinding(type, options = {}) {
} }
/** /**
* Call method. * Calls a bound method by name with the specified arguments.
* * See {@link Call} for details.
* @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.
* *
* @param {string} methodName - The name of the method in the format 'package.struct.method'. * @param {string} methodName - The name of the method in the format 'package.struct.method'.
* @param {...*} args - The arguments to pass to the method. * @param {any[]} 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 {Promise<any>} The result of the method call.
* @returns {*} The result of the method execution.
*/ */
export function ByName(methodName, ...args) { export function ByName(methodName, ...args) {
return callBinding(CallBinding, { return Call({
methodName, methodName,
args 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 {number} methodID - The ID of the method to call.
* @param {...*} args - The arguments to pass to the method. * @param {any[]} args - The arguments to pass to the method.
* @return {*} - The result of the method call. * @return {Promise<any>} - The result of the method call.
*/ */
export function ByID(methodID, ...args) { export function ByID(methodID, ...args) {
return callBinding(CallBinding, { return Call({
methodID, methodID,
args 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) { function dialogResultCallback(id, data, isJSON) {
let p = dialogResponses.get(id); let p = dialogResponses.get(id);
if (p) { if (p) {
dialogResponses.delete(id);
if (isJSON) { if (isJSON) {
p.resolve(JSON.parse(data)); p.resolve(JSON.parse(data));
} else { } else {
p.resolve(data); p.resolve(data);
} }
dialogResponses.delete(id);
} }
} }
@ -155,8 +155,8 @@ function dialogResultCallback(id, data, isJSON) {
function dialogErrorCallback(id, message) { function dialogErrorCallback(id, message) {
let p = dialogResponses.get(id); let p = dialogResponses.get(id);
if (p) { if (p) {
p.reject(message);
dialogResponses.delete(id); 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. * 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. * @param {string} windowName - The name of the window.
* @return {Function} - The new runtime caller function. * @return {Function} - The new runtime caller function.
*/ */
@ -57,8 +57,15 @@ export function newRuntimeCallerWithID(object, windowName) {
function runtimeCall(method, windowName, args) { function runtimeCall(method, windowName, args) {
return runtimeCallWithID(null, method, windowName, args);
}
async function runtimeCallWithID(objectID, method, windowName, args) {
let url = new URL(runtimeURL); let url = new URL(runtimeURL);
if( method ) { if (objectID != null) {
url.searchParams.append("object", objectID);
}
if (method != null) {
url.searchParams.append("method", method); url.searchParams.append("method", method);
} }
let fetchOptions = { let fetchOptions = {
@ -72,52 +79,14 @@ function runtimeCall(method, windowName, args) {
} }
fetchOptions.headers["x-wails-client-id"] = clientId; fetchOptions.headers["x-wails-client-id"] = clientId;
return new Promise((resolve, reject) => { let response = await fetch(url, fetchOptions);
fetch(url, fetchOptions) if (!response.ok) {
.then(response => { throw new Error(await response.text());
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));
});
}
function runtimeCallWithID(objectID, method, windowName, args) { if (response.headers.get("Content-Type") && response.headers.get("Content-Type").indexOf("application/json") !== -1) {
let url = new URL(runtimeURL); return response.json();
url.searchParams.append("object", objectID); } else {
url.searchParams.append("method", method); return response.text();
let fetchOptions = {
headers: {},
};
if (windowName) {
fetchOptions.headers["x-wails-window-name"] = windowName;
} }
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. * In case of failure, the returned promise will reject with an exception
* @returns {Object} - The result of the call. * 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 {string} methodName - The name of the method in the format 'package.struct.method'.
* @param {...*} args - The arguments to pass to the method. * @param {any[]} 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 {Promise<any>} The result of the method call.
* @returns {*} The result of the method execution.
*/ */
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 {number} methodID - The ID of the method to call.
* @param {...*} args - The arguments to pass to the method. * @param {any[]} args - The arguments to pass to the method.
* @return {*} - The result of the method call. * @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. * @typedef {Object} CallOptions
* @param {string} methodName - The name of the method to call. * @property {number} [methodID] - The numeric ID of the bound method to call.
* @param {...*} args - The arguments to pass to the method. * @property {string} [methodName] - The fully qualified name of the bound method to call.
* @returns {*} - The result of the method 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. * 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. * @param {string} windowName - The name of the window.
* @return {Function} - The new runtime caller function. * @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 { export namespace objectNames {
let Call: number; let Call: number;
let Clipboard: number; let Clipboard: number;

View File

@ -1,19 +1,51 @@
module changeme module changeme
go 1.21 go 1.24
require github.com/wailsapp/wails/v3 {{.WailsVersion}} require github.com/wailsapp/wails/v3 {{.WailsVersion}}
require ( require (
github.com/json-iterator/go v1.1.12 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/leaanthony/slicer v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/ProtonMail/go-crypto v1.1.5 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/adrg/xdg v0.5.3 // indirect
github.com/samber/lo v1.37.0 // indirect github.com/bep/debounce v1.2.1 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/cloudflare/circl v1.6.0 // indirect
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect
golang.org/x/net v0.7.0 // 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 replace github.com/wailsapp/wails/v3 => {{.LocalModulePath}}v3
{{end}} {{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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/elazarl/goproxy v1.4.0 h1:4GyuSbFa+s26+3rmYNSuUVsx+HgPrV1bk1jXI0l9wjM=
github.com/leaanthony/slicer v1.5.0 h1:aHYTN8xbCCLxJmkNKiLB6tgcMARl4eWmH9/F+S/0HtY= github.com/elazarl/goproxy v1.4.0/go.mod h1:X/5W/t+gzDyLfHW4DrMdpjqYjpXsURlBt9lpBDxZZZQ=
github.com/leaanthony/slicer v1.5.0/go.mod h1:FwrApmf8gOrpzEWM2J/9Lh79tyq8KTX5AzRtwV7m4AY= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/samber/lo v1.37.0 h1:XjVcB8g6tgUp8rsPsJ2CvhClfImrpL04YpQHXeHPhRw= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/samber/lo v1.37.0/go.mod h1:9vaz2O4o8oOnK23pd2TrXufcbdbJIa3b6cstBWKpopA= 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/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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 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 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9 h1:RjggHMcaTVp0LOVZcW0bo8alwHrOaCrGUDgfWUHhnN4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
golang.org/x/exp v0.0.0-20220930202632-ec3f01382ef9/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 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.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 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-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-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.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.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= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -7,11 +7,11 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"log/slog" "log/slog"
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -103,17 +103,17 @@ func New(appOptions Options) *App {
case "/wails/capabilities": case "/wails/capabilities":
err := assetserver.ServeFile(rw, path, globalApplication.capabilities.AsBytes()) err := assetserver.ServeFile(rw, path, globalApplication.capabilities.AsBytes())
if err != nil { 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": case "/wails/flags":
updatedOptions := result.impl.GetFlags(appOptions) updatedOptions := result.impl.GetFlags(appOptions)
flags, err := json.Marshal(updatedOptions) flags, err := json.Marshal(updatedOptions)
if err != nil { 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) err = assetserver.ServeFile(rw, path, flags)
if err != nil { if err != nil {
result.handleFatalError(fmt.Errorf("unable to serve flags: %s", err.Error())) result.fatal("unable to serve flags: %w", err)
} }
default: default:
next.ServeHTTP(rw, req) next.ServeHTTP(rw, req)
@ -130,40 +130,14 @@ func New(appOptions Options) *App {
srv, err := assetserver.NewAssetServer(opts) srv, err := assetserver.NewAssetServer(opts)
if err != nil { 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 = srv
result.assets.LogDetails() result.assets.LogDetails()
result.bindings, err = NewBindings(appOptions.Services, appOptions.BindAliases) result.bindings = NewBindings(appOptions.MarshalError, appOptions.BindAliases)
if err != nil { result.options.Services = slices.Clone(appOptions.Services)
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)
}
}
}
// Process keybindings // Process keybindings
if result.options.KeyBindings != nil { if result.options.KeyBindings != nil {
@ -181,11 +155,11 @@ func New(appOptions Options) *App {
if errors.Is(err, alreadyRunningError) && manager != nil { if errors.Is(err, alreadyRunningError) && manager != nil {
err = manager.notifyFirstInstance() err = manager.notifyFirstInstance()
if err != nil { 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) 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 { } else {
result.singleInstanceManager = manager result.singleInstanceManager = manager
} }
@ -314,7 +288,8 @@ type App struct {
menuItems map[uint]*MenuItem menuItems map[uint]*MenuItem
menuItemsLock sync.Mutex menuItemsLock sync.Mutex
// Running // Starting and running
starting bool
running bool running bool
runLock sync.Mutex runLock sync.Mutex
pendingRun []runnable pendingRun []runnable
@ -350,7 +325,9 @@ type App struct {
keyBindingsLock sync.RWMutex keyBindingsLock sync.RWMutex
// Shutdown // Shutdown
performingShutdown bool performingShutdown bool
shutdownLock sync.Mutex
serviceShutdownLock sync.Mutex
// Shutdown tasks are run when the application is shutting down. // 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. // 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) a.Logger.Warn(msg)
} }
} }
func (a *App) handleError(err error) { func (a *App) handleError(err error) {
if a.options.ErrorHandler != nil { if a.options.ErrorHandler != nil {
a.options.ErrorHandler(err) 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 // EmitEvent will emit an event
func (a *App) EmitEvent(name string, data ...any) { func (a *App) EmitEvent(name string, data ...any) {
a.customEventProcessor.Emit(&CustomEvent{ a.customEventProcessor.Emit(&CustomEvent{
@ -417,13 +414,7 @@ func (a *App) ResetEvents() {
} }
func (a *App) handleFatalError(err error) { func (a *App) handleFatalError(err error) {
var buffer strings.Builder a.handleError(&FatalError{err: err})
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()))
os.Exit(1) os.Exit(1)
} }
@ -587,6 +578,18 @@ func (a *App) NewSystemTray() *SystemTray {
} }
func (a *App) Run() error { 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 // Call post-create hooks
err := a.preRun() err := a.preRun()
@ -595,6 +598,24 @@ func (a *App) Run() error {
} }
a.impl = newPlatformApp(a) 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() { go func() {
for { for {
event := <-applicationEvents event := <-applicationEvents
@ -641,17 +662,19 @@ func (a *App) Run() error {
a.runLock.Lock() a.runLock.Lock()
a.running = true 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() { go func() {
defer handlePanic() defer handlePanic()
systray.Run() pending.Run()
}() }()
} }
a.pendingRun = nil a.pendingRun = nil
a.runLock.Unlock()
// set the application menu // set the application menu
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
a.impl.setApplicationMenu(a.ApplicationMenu) a.impl.setApplicationMenu(a.ApplicationMenu)
@ -660,27 +683,59 @@ func (a *App) Run() error {
a.impl.setIcon(a.options.Icon) 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 { if err != nil {
return err return fmt.Errorf("cannot bind service methods: %w", err)
} }
// Cancel the context if service.options.Route != "" {
a.cancel() handler, ok := service.Instance().(http.Handler)
if !ok {
for _, service := range a.options.Services { handler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
// If it conforms to the ServiceShutdown interface, call the Shutdown method http.Error(
if thisService, ok := service.instance.(ServiceShutdown); ok { rw,
err := thisService.ServiceShutdown() fmt.Sprintf("Service '%s' does not handle HTTP requests", getServiceName(service)),
if err != nil { http.StatusServiceUnavailable,
a.error("Error shutting down service: " + err.Error()) )
} })
} }
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 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) { func (a *App) handleApplicationEvent(event *ApplicationEvent) {
defer handlePanic() defer handlePanic()
a.applicationEventListenersLock.RLock() a.applicationEventListenersLock.RLock()
@ -721,7 +776,7 @@ func (a *App) handleDragAndDropMessage(event *dragAndDropMessage) {
window, ok := a.windows[event.windowId] window, ok := a.windows[event.windowId]
a.windowsLock.Unlock() a.windowsLock.Unlock()
if !ok { if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId) a.warning("WebviewWindow #%d not found", event.windowId)
return return
} }
// Get callback from window // Get callback from window
@ -735,7 +790,7 @@ func (a *App) handleWindowMessage(event *windowMessage) {
window, ok := a.windows[event.windowId] window, ok := a.windows[event.windowId]
a.windowsLock.RUnlock() a.windowsLock.RUnlock()
if !ok { if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId) a.warning("WebviewWindow #%d not found", event.windowId)
return return
} }
// Check if the message starts with "wails:" // Check if the message starts with "wails:"
@ -760,7 +815,7 @@ func (a *App) handleWindowEvent(event *windowEvent) {
window, ok := a.windows[event.WindowID] window, ok := a.windows[event.WindowID]
a.windowsLock.RUnlock() a.windowsLock.RUnlock()
if !ok { if !ok {
log.Printf("Window #%d not found", event.WindowID) a.warning("Window #%d not found", event.WindowID)
return return
} }
window.HandleWindowEvent(event.EventID) window.HandleWindowEvent(event.EventID)
@ -771,7 +826,7 @@ func (a *App) handleMenuItemClicked(menuItemID uint) {
menuItem := getMenuItemByID(menuItemID) menuItem := getMenuItemByID(menuItemID)
if menuItem == nil { if menuItem == nil {
log.Printf("MenuItem #%d not found", menuItemID) a.warning("MenuItem #%d not found", menuItemID)
return return
} }
menuItem.handleClick() menuItem.handleClick()
@ -796,7 +851,17 @@ func (a *App) OnShutdown(f func()) {
if f == nil { if f == nil {
return 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) { func (a *App) destroySystemTray(tray *SystemTray) {
@ -808,14 +873,22 @@ func (a *App) destroySystemTray(tray *SystemTray) {
} }
func (a *App) cleanup() { func (a *App) cleanup() {
a.shutdownLock.Lock()
if a.performingShutdown { if a.performingShutdown {
a.shutdownLock.Unlock()
return return
} }
a.cancel() // Cancel app context before running shutdown hooks.
a.performingShutdown = true 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 { for _, shutdownTask := range a.shutdownTasks {
InvokeSync(shutdownTask) InvokeSync(shutdownTask)
} }
InvokeSync(func() { InvokeSync(func() {
a.shutdownServices()
a.windowsLock.RLock() a.windowsLock.RLock()
for _, window := range a.windows { for _, window := range a.windows {
window.Close() window.Close()
@ -828,17 +901,23 @@ func (a *App) cleanup() {
} }
a.systemTrays = nil a.systemTrays = nil
a.systemTraysLock.Unlock() a.systemTraysLock.Unlock()
// Cleanup single instance manager
if a.singleInstanceManager != nil {
a.singleInstanceManager.cleanup()
}
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() { func (a *App) Quit() {
if a.impl != nil { if a.impl != nil {
InvokeSync(a.impl.destroy) InvokeSync(a.impl.destroy)
a.postQuit()
} }
} }
@ -1006,15 +1085,17 @@ func (a *App) GetWindowByName(name string) Window {
func (a *App) runOrDeferToAppRun(r runnable) { func (a *App) runOrDeferToAppRun(r runnable) {
a.runLock.Lock() a.runLock.Lock()
running := a.running
if !running {
a.pendingRun = append(a.pendingRun, r)
}
a.runLock.Unlock()
if running { if !a.running {
r.Run() 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 { 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] window, ok := a.windows[event.windowId]
a.windowsLock.RUnlock() a.windowsLock.RUnlock()
if !ok { if !ok {
log.Printf("WebviewWindow #%d not found", event.windowId) a.warning("WebviewWindow #%d not found", event.windowId)
return return
} }
// Get callback from window // Get callback from window

View File

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

View File

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

View File

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

View File

@ -31,11 +31,21 @@ type Options struct {
// Services allows you to bind Go methods to the frontend. // Services allows you to bind Go methods to the frontend.
Services []Service 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. // 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. // Example: `BindAliases: map[uint32]uint32{1: 1411160069}` states that alias ID 1 maps to the Go method with ID 1411160069.
BindAliases map[uint32]uint32 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. // If not defined, a default logger is used.
Logger *slog.Logger Logger *slog.Logger
@ -60,9 +70,17 @@ type Options struct {
// OnShutdown is called when the application is about to terminate. // OnShutdown is called when the application is about to terminate.
// This is useful for cleanup tasks. // This is useful for cleanup tasks.
// The shutdown process blocks until this function returns // The shutdown process blocks until this function returns.
OnShutdown func() 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. // 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 true, the application will quit.
// If the function returns false, the application will not quit. // If the function returns false, the application will not quit.

View File

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

View File

@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http"
"reflect" "reflect"
"runtime" "runtime"
"strings" "strings"
@ -21,16 +20,22 @@ type CallOptions struct {
Args []json.RawMessage `json:"args"` Args []json.RawMessage `json:"args"`
} }
type PluginCallOptions struct { type ErrorKind string
Name string `json:"name"`
Args []json.RawMessage `json:"args"` 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{ func (e *CallError) Error() string {
"Name", return e.Message
"Init",
"Shutdown",
"Exported",
} }
// Parameter defines a Go method parameter // 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 // BoundMethod defines all the data related to a Go method that is
// bound to the Wails application // bound to the Wails application
type BoundMethod struct { type BoundMethod struct {
ID uint32 `json:"id"` ID uint32 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Inputs []*Parameter `json:"inputs,omitempty"` Inputs []*Parameter `json:"inputs,omitempty"`
Outputs []*Parameter `json:"outputs,omitempty"` Outputs []*Parameter `json:"outputs,omitempty"`
Comments string `json:"comments,omitempty"` Comments string `json:"comments,omitempty"`
Method reflect.Value `json:"-"` Method reflect.Value `json:"-"`
TypeName string FQN string
PackagePath string
marshalError func(error) []byte
needsContext bool needsContext bool
} }
type Bindings struct { type Bindings struct {
marshalError func(error) []byte
boundMethods map[string]*BoundMethod boundMethods map[string]*BoundMethod
boundByID map[uint32]*BoundMethod boundByID map[uint32]*BoundMethod
methodAliases map[uint32]uint32 methodAliases map[uint32]uint32
} }
func NewBindings(instances []Service, aliases map[uint32]uint32) (*Bindings, error) { func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings {
app := Get() return &Bindings{
b := &Bindings{ marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError),
boundMethods: make(map[string]*BoundMethod), boundMethods: make(map[string]*BoundMethod),
boundByID: make(map[uint32]*BoundMethod), boundByID: make(map[uint32]*BoundMethod),
methodAliases: aliases, 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 // Add adds the given service to the bindings.
func (b *Bindings) Add(namedPtr interface{}) error { func (b *Bindings) Add(service Service) error {
methods, err := b.getMethods(namedPtr) methods, err := getMethods(service.Instance())
if err != nil { 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 { for _, method := range methods {
// Add it as a regular method // Store composite error marshaler
b.boundMethods[method.String()] = method method.marshalError = marshalError
// Register method
b.boundMethods[method.FQN] = method
b.boundByID[method.ID] = method b.boundByID[method.ID] = method
} }
return nil return nil
} }
// Get returns the bound method with the given name // Get returns the bound method with the given name
func (b *Bindings) Get(options *CallOptions) *BoundMethod { func (b *Bindings) Get(options *CallOptions) *BoundMethod {
method, ok := b.boundMethods[options.MethodName] return b.boundMethods[options.MethodName]
if !ok {
return nil
}
return method
} }
// GetByID returns the bound method with the given ID // GetByID returns the bound method with the given ID
@ -131,29 +145,27 @@ func (b *Bindings) GetByID(id uint32) *BoundMethod {
id = alias id = alias
} }
} }
result := b.boundByID[id]
return result return b.boundByID[id]
} }
// GenerateID generates a unique ID for a binding // internalServiceMethod is a set of methods
func (b *Bindings) GenerateID(name string) (uint32, error) { // that are handled specially by the binding engine
id, err := hash.Fnv(name) // and must not be exposed to the frontend.
if err != nil { //
return 0, err // For simplicity we exclude these by name
} // without checking their signatures,
// Check if we already have it // and so does the binding generator.
boundMethod, ok := b.boundByID[id] var internalServiceMethods = map[string]bool{
if ok { "ServiceName": true,
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()) "ServiceStartup": true,
} "ServiceShutdown": true,
return id, nil "ServeHTTP": true,
} }
func (b *BoundMethod) String() string { var ctxType = reflect.TypeFor[context.Context]()
return fmt.Sprintf("%s.%s.%s", b.PackagePath, b.TypeName, b.Name)
}
func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { func getMethods(value any) ([]*BoundMethod, error) {
// Create result placeholder // Create result placeholder
var result []*BoundMethod 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()) 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 // Process Methods
for i := 0; i < ptrType.NumMethod(); i++ { for i := range ptrType.NumMethod() {
methodDef := ptrType.Method(i) methodName := ptrType.Method(i).Name
methodName := methodDef.Name method := namedValue.Method(i)
method := namedValue.MethodByName(methodName)
if b.internalMethod(methodDef) { if internalServiceMethods[methodName] {
continue continue
} }
fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName)
// Create new method // Create new method
boundMethod := &BoundMethod{ boundMethod := &BoundMethod{
Name: methodName, ID: hash.Fnv(fqn),
PackagePath: packagePath, FQN: fqn,
TypeName: typeName, Name: methodName,
Inputs: nil, Inputs: nil,
Outputs: nil, Outputs: nil,
Comments: "", Comments: "",
Method: method, 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 // Iterate inputs
methodType := method.Type() methodType := method.Type()
@ -245,40 +242,23 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) {
result = append(result, boundMethod) result = append(result, boundMethod)
} }
return result, nil return result, nil
} }
func (b *Bindings) internalMethod(def reflect.Method) bool { func (b *BoundMethod) String() string {
// Get the receiver type return b.FQN
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
} }
var errorType = reflect.TypeFor[error]() var errorType = reflect.TypeFor[error]()
// Call will attempt to call this bound method with the given args // 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) { // 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 // Use a defer statement to capture panics
defer handlePanic(handlePanicOptions{skipEnd: 5}) defer handlePanic(handlePanicOptions{skipEnd: 5})
argCount := len(args) argCount := len(args)
@ -287,7 +267,10 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
} }
if argCount != len(b.Inputs) { 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 return
} }
@ -305,7 +288,11 @@ func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (returnV
value := reflect.New(b.Inputs[base+index].ReflectType) value := reflect.New(b.Inputs[base+index].ReflectType)
err = json.Unmarshal(arg, value.Interface()) err = json.Unmarshal(arg, value.Interface())
if err != nil { 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 return
} }
callArgs[base+index] = value.Elem() 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 nonErrorOutputs = make([]any, 0, len(callResults))
var errorOutputs []error var errorOutputs []error
for _, result := range callResults { for _, field := range callResults {
if result.Type() == errorType { if field.Type() == errorType {
if result.IsNil() { if field.IsNil() {
continue continue
} }
if errorOutputs == nil { if errorOutputs == nil {
errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs)) errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs))
nonErrorOutputs = nil nonErrorOutputs = nil
} }
errorOutputs = append(errorOutputs, result.Interface().(error)) errorOutputs = append(errorOutputs, field.Interface().(error))
} else if nonErrorOutputs != nil { } else if nonErrorOutputs != nil {
nonErrorOutputs = append(nonErrorOutputs, result.Interface()) nonErrorOutputs = append(nonErrorOutputs, field.Interface())
} }
} }
if errorOutputs != nil { if len(errorOutputs) > 0 {
err = errors.Join(errorOutputs...) 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 { } else if len(nonErrorOutputs) == 1 {
returnValue = nonErrorOutputs[0] result = nonErrorOutputs[0]
} else if len(nonErrorOutputs) > 1 { } else if len(nonErrorOutputs) > 1 {
returnValue = nonErrorOutputs result = nonErrorOutputs
} }
return 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. // isPtr returns true if the value given is a pointer.
func isPtr(value interface{}) bool { func isPtr(value interface{}) bool {
return reflect.ValueOf(value).Kind() == reflect.Ptr return reflect.ValueOf(value).Kind() == reflect.Ptr

View File

@ -44,7 +44,7 @@ func (t *TestService) Variadic(s ...string) []string {
return s return s
} }
func (t *TestService) PositionalAndVariadic(a int, b ...string) int { func (t *TestService) PositionalAndVariadic(a int, _ ...string) int {
return a return a
} }
@ -52,106 +52,103 @@ func (t *TestService) Slice(a []int) []int {
return a return a
} }
func newArgs(jsonArgs ...string) []json.RawMessage { func newArgs(jsonArgs ...string) (args []json.RawMessage) {
args := []json.RawMessage{}
for _, j := range jsonArgs { for _, j := range jsonArgs {
args = append(args, json.RawMessage(j)) args = append(args, json.RawMessage(j))
} }
return args return
} }
func TestBoundMethodCall(t *testing.T) { func TestBoundMethodCall(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
method string method string
args []json.RawMessage args []json.RawMessage
err error err string
expected interface{} expected interface{}
}{ }{
{ {
name: "nil", name: "nil",
method: "Nil", method: "Nil",
args: []json.RawMessage{}, args: []json.RawMessage{},
err: nil, err: "",
expected: nil, expected: nil,
}, },
{ {
name: "string", name: "string",
method: "String", method: "String",
args: newArgs(`"foo"`), args: newArgs(`"foo"`),
err: nil, err: "",
expected: "foo", expected: "foo",
}, },
{ {
name: "multiple", name: "multiple",
method: "Multiple", method: "Multiple",
args: newArgs(`"foo"`, "0", "false"), args: newArgs(`"foo"`, "0", "false"),
err: nil, err: "",
expected: []interface{}{"foo", 0, false}, expected: []interface{}{"foo", 0, false},
}, },
{ {
name: "struct", name: "struct",
method: "Struct", method: "Struct",
args: newArgs(`{ "name": "alice" }`), args: newArgs(`{ "name": "alice" }`),
err: nil, err: "",
expected: Person{Name: "alice"}, expected: Person{Name: "alice"},
}, },
{ {
name: "struct, nil error", name: "struct, nil error",
method: "StructNil", method: "StructNil",
args: newArgs(`{ "name": "alice" }`), args: newArgs(`{ "name": "alice" }`),
err: nil, err: "",
expected: Person{Name: "alice"}, expected: Person{Name: "alice"},
}, },
{ {
name: "struct, error", name: "struct, error",
method: "StructError", method: "StructError",
args: newArgs(`{ "name": "alice" }`), args: newArgs(`{ "name": "alice" }`),
err: errors.New("error"), err: "error",
expected: nil, expected: nil,
}, },
{ {
name: "invalid argument count", name: "invalid argument count",
method: "Multiple", method: "Multiple",
args: newArgs(`"foo"`), args: newArgs(`"foo"`),
err: errors.New("expects 3 arguments, received 1"), err: "expects 3 arguments, got 1",
expected: nil, expected: nil,
}, },
{ {
name: "invalid argument type", name: "invalid argument type",
method: "String", method: "String",
args: newArgs("1"), args: newArgs("1"),
err: errors.New("could not parse"), err: "could not parse",
expected: nil, expected: nil,
}, },
{ {
name: "variadic, no arguments", name: "variadic, no arguments",
method: "Variadic", method: "Variadic",
args: newArgs(`[]`), // variadic parameters are passed as arrays args: newArgs(`[]`), // variadic parameters are passed as arrays
err: nil, err: "",
expected: []string{}, expected: []string{},
}, },
{ {
name: "variadic", name: "variadic",
method: "Variadic", method: "Variadic",
args: newArgs(`["foo", "bar"]`), args: newArgs(`["foo", "bar"]`),
err: nil, err: "",
expected: []string{"foo", "bar"}, expected: []string{"foo", "bar"},
}, },
{ {
name: "positional and variadic", name: "positional and variadic",
method: "PositionalAndVariadic", method: "PositionalAndVariadic",
args: newArgs("42", `[]`), args: newArgs("42", `[]`),
err: nil, err: "",
expected: 42, expected: 42,
}, },
{ {
name: "slice", name: "slice",
method: "Slice", method: "Slice",
args: newArgs(`[1,2,3]`), args: newArgs(`[1,2,3]`),
err: nil, err: "",
expected: []int{1, 2, 3}, expected: []int{1, 2, 3},
}, },
} }
@ -159,13 +156,11 @@ func TestBoundMethodCall(t *testing.T) {
// init globalApplication // init globalApplication
_ = application.New(application.Options{}) _ = application.New(application.Options{})
bindings, err := application.NewBindings( bindings := application.NewBindings(nil, nil)
[]application.Service{
application.NewService(&TestService{}), err := bindings.Add(application.NewService(&TestService{}))
}, make(map[uint32]uint32),
)
if err != nil { if err != nil {
t.Fatalf("application.NewBindings() error = %v\n", err) t.Fatalf("bindings.Add() error = %v\n", err)
} }
for _, tt := range tests { for _, tt := range tests {
@ -180,13 +175,16 @@ func TestBoundMethodCall(t *testing.T) {
} }
result, err := method.Call(context.TODO(), tt.args) result, err := method.Call(context.TODO(), tt.args)
if tt.err != err && (tt.err == nil || err == nil || !strings.Contains(err.Error(), tt.err.Error())) { if (tt.err == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), tt.err)) {
t.Fatalf("error: %v, expected error: %v", err, tt.err) expected := tt.err
if expected == "" {
expected = "nil"
}
t.Fatalf("error: %#v, expected error: %v", err, expected)
} }
if !reflect.DeepEqual(result, tt.expected) { if !reflect.DeepEqual(result, tt.expected) {
t.Fatalf("result: %v, expected result: %v", 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 { if m.dialog.window != nil {
parentWindow, err = m.dialog.window.NativeWindowHandle() parentWindow, err = m.dialog.window.NativeWindowHandle()
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
@ -50,12 +50,12 @@ func (m *windowsDialog) show() {
// 3 is the application icon // 3 is the application icon
button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON) button, err = w32.MessageBoxWithIcon(parentWindow, message, title, 3, windows.MB_OK|windows.MB_USERICON)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} else { } else {
button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL) button, err = windows.MessageBox(windows.HWND(parentWindow), message, title, flags|windows.MB_SYSTEMMODAL)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
// This maps MessageBox return values to strings // This maps MessageBox return values to strings
@ -114,7 +114,7 @@ func (m *windowOpenFileDialog) show() (chan string, error) {
if m.dialog.window != nil { if m.dialog.window != nil {
config.ParentWindowHandle, err = m.dialog.window.NativeWindowHandle() config.ParentWindowHandle, err = m.dialog.window.NativeWindowHandle()
if err != nil { 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() { defer func() {
err := dlg.Release() err := dlg.Release()
if err != nil { 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 ( import (
"fmt" "fmt"
"os" "os"
"strings"
) )
func Fatal(message string, args ...interface{}) { // FatalError instances are passed to the registered error handler
println("*********************** FATAL ***********************") // in case of catastrophic, unrecoverable failures that require immediate termination.
println("There has been a catastrophic failure in your application.") // FatalError wraps the original error value in an informative message.
println("Please report this error at https://github.com/wailsapp/wails/issues") // The underlying error may be retrieved through the [FatalError.Unwrap] method.
println("******************** Error Details ******************") type FatalError struct {
println(fmt.Sprintf(message, args...)) err error
println("*********************** FATAL ***********************") 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) 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 var gerror *C.GError
pixbuf := C.gdk_pixbuf_new_from_stream(stream, nil, &gerror) pixbuf := C.gdk_pixbuf_new_from_stream(stream, nil, &gerror)
if gerror != nil { 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) C.g_error_free(gerror)
return return
} }

View File

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

View File

@ -3,8 +3,9 @@
package application package application
import ( import (
"github.com/wailsapp/wails/v3/pkg/w32"
"unsafe" "unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
) )
type windowsMenuItem struct { type windowsMenuItem struct {
@ -121,7 +122,7 @@ func (m *windowsMenuItem) setBitmap(bitmap []byte) {
// Set the icon // Set the icon
err := w32.SetMenuIcons(m.hMenu, m.id, bitmap, nil) err := w32.SetMenuIcons(m.hMenu, m.id, bitmap, nil)
if err != 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 return
} }
m.update() m.update()

View File

@ -3,6 +3,7 @@ package application
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
@ -41,12 +42,12 @@ func NewMessageProcessor(logger *slog.Logger) *MessageProcessor {
} }
} }
func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, args ...any) { func (m *MessageProcessor) httpError(rw http.ResponseWriter, message string, err error) {
m.Error(message, args...) m.Error(message, "error", err)
rw.WriteHeader(http.StatusBadRequest) rw.WriteHeader(http.StatusUnprocessableEntity)
_, err := rw.Write([]byte(fmt.Sprintf(message, args...))) _, err = rw.Write([]byte(err.Error()))
if err != nil { 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) wID, err := strconv.ParseUint(windowID, 10, 64)
if err != nil { 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 return nil, windowID
} }
targetWindow := globalApplication.getWindowForID(uint(wID)) targetWindow := globalApplication.getWindowForID(uint(wID))
if targetWindow == nil { if targetWindow == nil {
m.Error("Window ID %d not found", wID) m.Error("Window ID not found:", "id", wID)
return nil, windowID return nil, windowID
} }
return targetWindow, 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) { func (m *MessageProcessor) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
object := r.URL.Query().Get("object") object := r.URL.Query().Get("object")
if object == "" { if object == "" {
m.httpError(rw, "Invalid runtime call") m.httpError(rw, "Invalid runtime call:", errors.New("missing object value"))
return return
} }
@ -90,19 +91,19 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h
}() }()
object, err := strconv.Atoi(r.URL.Query().Get("object")) object, err := strconv.Atoi(r.URL.Query().Get("object"))
if err != nil { 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 return
} }
method, err := strconv.Atoi(r.URL.Query().Get("method")) method, err := strconv.Atoi(r.URL.Query().Get("method"))
if err != nil { 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 return
} }
params := QueryParams(r.URL.Query()) params := QueryParams(r.URL.Query())
targetWindow, nameOrID := m.getTargetWindow(r) targetWindow, nameOrID := m.getTargetWindow(r)
if targetWindow == nil { 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 return
} }
@ -130,7 +131,7 @@ func (m *MessageProcessor) HandleRuntimeCallWithIDs(rw http.ResponseWriter, r *h
case cancelCallRequesst: case cancelCallRequesst:
m.processCallCancelMethod(method, rw, r, targetWindow, params) m.processCallCancelMethod(method, rw, r, targetWindow, params)
default: 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 { if data != nil {
jsonPayload, err = json.Marshal(data) jsonPayload, err = json.Marshal(data)
if err != nil { 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 return
} }
} }
_, err = rw.Write(jsonPayload) _, err = rw.Write(jsonPayload)
if err != nil { 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 return
} }
m.ok(rw) 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) { func (m *MessageProcessor) text(rw http.ResponseWriter, data string) {
_, err := rw.Write([]byte(data)) _, err := rw.Write([]byte(data))
if err != nil { 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 return
} }
rw.Header().Set("Content-Type", "text/plain") rw.Header().Set("Content-Type", "text/plain")

View File

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

View File

@ -3,6 +3,7 @@ package application
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
) )
@ -15,33 +16,46 @@ const (
) )
func (m *MessageProcessor) callErrorCallback(window Window, message string, callID *string, err error) { func (m *MessageProcessor) callErrorCallback(window Window, message string, callID *string, err error) {
errorMsg := fmt.Sprintf(message, err) m.Error(message, "id", *callID, "error", err)
m.Error(errorMsg) if cerr := (*CallError)(nil); errors.As(err, &cerr) {
window.CallError(*callID, errorMsg) 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) window.CallResponse(*callID, result)
} }
func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) { func (m *MessageProcessor) processCallCancelMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args() args, err := params.Args()
if err != nil { 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")
return return
} }
m.l.Lock() callID := args.String("call-id")
cancel := m.runningCalls[*callID] if callID == nil || *callID == "" {
m.l.Unlock() 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 { if cancel != nil {
cancel() cancel()
m.Info("Binding call canceled:", "id", *callID)
} }
m.ok(rw) 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) { func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args() args, err := params.Args()
if err != nil { 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 return
} }
callID := args.String("call-id") callID := args.String("call-id")
if callID == nil || *callID == "" { if callID == nil || *callID == "" {
m.Error("call-id is required") m.httpError(rw, "Invalid binding call:", errors.New("missing argument 'call-id'"))
return return
} }
@ -63,86 +78,116 @@ func (m *MessageProcessor) processCallMethod(method int, rw http.ResponseWriter,
var options CallOptions var options CallOptions
err := params.ToStruct(&options) err := params.ToStruct(&options)
if err != nil { if err != nil {
m.callErrorCallback(window, "Error parsing call options: %s", callID, err) m.httpError(rw, "Invalid binding call:", fmt.Errorf("error parsing call options: %w", 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))
return return
} }
ctx, cancel := context.WithCancel(context.WithoutCancel(r.Context())) 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 ambiguousID := false
m.l.Lock() func() {
if m.runningCalls[*callID] != nil { m.l.Lock()
ambiguousID = true defer m.l.Unlock()
} else {
m.runningCalls[*callID] = cancel if m.runningCalls[*callID] != nil {
} ambiguousID = true
m.l.Unlock() } else {
m.runningCalls[*callID] = cancel
}
}()
if ambiguousID { if ambiguousID {
cancel() m.httpError(rw, "Invalid binding call:", fmt.Errorf("ambiguous call id: %s", *callID))
m.callErrorCallback(window, "Error calling method: %s, a method call with the same id is already running", callID, err)
return return
} }
// Set the context values for the window m.ok(rw) // From now on, failures are reported through the error callback.
if window != nil {
ctx = context.WithValue(ctx, WindowKey, window) // Log call
var methodRef any = options.MethodName
if options.MethodName == "" {
methodRef = options.MethodID
} }
m.Info("Binding call started:", "id", *callID, "method", methodRef)
go func() { go func() {
defer handlePanic() defer handlePanic()
defer func() { defer func() {
cancel()
m.l.Lock() m.l.Lock()
defer m.l.Unlock()
delete(m.runningCalls, *callID) 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) result, err := boundMethod.Call(ctx, options.Args)
if err != nil { if cerr := (*CallError)(nil); errors.As(err, &cerr) {
msg := fmt.Sprintf("Error calling method '%v'", boundMethod.Name) switch cerr.Kind {
m.callErrorCallback(window, msg+": %s", callID, err) case ReferenceError, TypeError:
m.callErrorCallback(window, "Binding call failed:", callID, cerr)
case RuntimeError:
m.callErrorCallback(window, "Bound method returned an error:", callID, cerr)
}
return return
} }
var jsonResult = []byte("{}")
if result != nil { if result != nil {
// convert result to json // convert result to json
jsonResult, err = json.Marshal(result) jsonResult, err = json.Marshal(result)
if err != nil { 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 return
} }
} }
m.callCallback(window, callID, string(jsonResult), true)
var jsonArgs struct { m.callCallback(window, callID, string(jsonResult))
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.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 package application
import ( import (
"errors"
"fmt"
"net/http" "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) { func (m *MessageProcessor) processClipboardMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, params QueryParams) {
args, err := params.Args() args, err := params.Args()
if err != nil { 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 return
} }
var text string
switch method { switch method {
case ClipboardSetText: case ClipboardSetText:
text := args.String("text") textp := args.String("text")
if text == nil { if textp == nil {
m.Error("SetText: text is required") m.httpError(rw, "Invalid clipboard call:", errors.New("missing argument 'text'"))
return return
} }
globalApplication.Clipboard().SetText(*text) text = *textp
globalApplication.Clipboard().SetText(text)
m.ok(rw) m.ok(rw)
m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", *text)
case ClipboardText: case ClipboardText:
text, _ := globalApplication.Clipboard().Text() text, _ = globalApplication.Clipboard().Text()
m.text(rw, text) m.text(rw, text)
m.Info("Runtime Call:", "method", "Clipboard."+clipboardMethods[method], "text", text)
default: default:
m.httpError(rw, "Unknown clipboard method: %d", method) m.httpError(rw, "Invalid clipboard call:", fmt.Errorf("unknown method: %d", method))
return return
} }
m.Info("Runtime call:", "method", "Clipboard."+clipboardMethods[method], "text", text)
} }

View File

@ -1,6 +1,7 @@
package application package application
import ( import (
"fmt"
"net/http" "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) { func (m *MessageProcessor) processContextMenuMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
switch method { switch method {
case ContextMenuOpen: case ContextMenuOpen:
var data ContextMenuData var data ContextMenuData
err := params.ToStruct(&data) err := params.ToStruct(&data)
if err != nil { 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 return
} }
window.OpenContextMenu(&data) window.OpenContextMenu(&data)
m.ok(rw) m.ok(rw)
m.Info("Runtime call:", "method", "ContextMenu."+contextmenuMethodNames[method], "id", data.Id, "x", data.X, "y", data.Y, "data", data.Data)
default: 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 ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"runtime" "runtime"
@ -26,9 +27,8 @@ var dialogMethodNames = map[int]string{
} }
func (m *MessageProcessor) dialogErrorCallback(window Window, message string, dialogID *string, err error) { func (m *MessageProcessor) dialogErrorCallback(window Window, message string, dialogID *string, err error) {
errorMsg := fmt.Sprintf(message, err) m.Error(message, "error", err)
m.Error(errorMsg) window.DialogError(*dialogID, err.Error())
window.DialogError(*dialogID, errorMsg)
} }
func (m *MessageProcessor) dialogCallback(window Window, dialogID *string, result string, isJSON bool) { 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) { func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
args, err := params.Args() args, err := params.Args()
if err != nil { 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 return
} }
dialogID := args.String("dialog-id") dialogID := args.String("dialog-id")
if dialogID == nil { if dialogID == nil {
m.Error("dialog-id is required") m.httpError(rw, "Invalid window call:", errors.New("missing argument 'dialog-id'"))
return return
} }
@ -55,7 +55,7 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
var options MessageDialogOptions var options MessageDialogOptions
err := params.ToStruct(&options) err := params.ToStruct(&options)
if err != nil { 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 return
} }
if len(options.Buttons) == 0 { if len(options.Buttons) == 0 {
@ -91,13 +91,13 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
dialog.AddButtons(options.Buttons) dialog.AddButtons(options.Buttons)
dialog.Show() dialog.Show()
m.ok(rw) m.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options) m.Info("Runtime call:", "method", methodName, "options", options)
case DialogOpenFile: case DialogOpenFile:
var options OpenFileDialogOptions var options OpenFileDialogOptions
err := params.ToStruct(&options) err := params.ToStruct(&options)
if err != nil { 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 return
} }
var detached = args.Bool("Detached") var detached = args.Bool("Detached")
@ -111,35 +111,35 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
if options.AllowsMultipleSelection { if options.AllowsMultipleSelection {
files, err := dialog.PromptForMultipleSelection() files, err := dialog.PromptForMultipleSelection()
if err != nil { 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 return
} else { } else {
result, err := json.Marshal(files) result, err := json.Marshal(files)
if err != nil { 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 return
} }
m.dialogCallback(window, dialogID, string(result), true) m.dialogCallback(window, dialogID, string(result), true)
m.Info("Runtime Call:", "method", methodName, "result", result) m.Info("Runtime call:", "method", methodName, "result", result)
} }
} else { } else {
file, err := dialog.PromptForSingleSelection() file, err := dialog.PromptForSingleSelection()
if err != nil { 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 return
} }
m.dialogCallback(window, dialogID, file, false) 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.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options) m.Info("Runtime call:", "method", methodName, "options", options)
case DialogSaveFile: case DialogSaveFile:
var options SaveFileDialogOptions var options SaveFileDialogOptions
err := params.ToStruct(&options) err := params.ToStruct(&options)
if err != nil { 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 return
} }
var detached = args.Bool("Detached") var detached = args.Bool("Detached")
@ -152,17 +152,17 @@ func (m *MessageProcessor) processDialogMethod(method int, rw http.ResponseWrite
defer handlePanic() defer handlePanic()
file, err := dialog.PromptForSingleSelection() file, err := dialog.PromptForSingleSelection()
if err != nil { 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 return
} }
m.dialogCallback(window, dialogID, file, false) 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.ok(rw)
m.Info("Runtime Call:", "method", methodName, "options", options) m.Info("Runtime call:", "method", methodName, "options", options)
default: 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 package application
import ( import (
"fmt"
"net/http" "net/http"
"github.com/pkg/errors"
) )
const ( 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) { func (m *MessageProcessor) processEventsMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
var event CustomEvent
switch method { switch method {
case EventsEmit: case EventsEmit:
var event CustomEvent
err := params.ToStruct(&event) err := params.ToStruct(&event)
if err != nil { 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 return
} }
if event.Name == "" { if event.Name == "" {
m.httpError(rw, "Event name must be specified") m.httpError(rw, "Invalid events call:", errors.New("missing event name"))
return return
} }
event.Sender = window.Name() event.Sender = window.Name()
globalApplication.customEventProcessor.Emit(&event) globalApplication.customEventProcessor.Emit(&event)
m.ok(rw) m.ok(rw)
m.Info("Runtime call:", "method", "Events."+eventsMethodNames[method], "name", event.Name, "sender", event.Sender, "data", event.Data, "cancelled", event.IsCancelled())
default: default:
m.httpError(rw, "Unknown event method: %d", method) m.httpError(rw, "Invalid events call:", fmt.Errorf("unknown method: %d", method))
return 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 result = v
case float64: case float64:
result = T(v) result = T(v)
default:
return nil
} }
return &result return &result
} }
@ -154,6 +156,7 @@ func (a *Args) UInt8(s string) *uint8 {
} }
return nil return nil
} }
func (a *Args) UInt(s string) *uint { func (a *Args) UInt(s string) *uint {
if a == nil { if a == nil {
return nil return nil
@ -169,8 +172,9 @@ func (a *Args) Float64(s string) *float64 {
return nil return nil
} }
if val := a.data[s]; val != nil { if val := a.data[s]; val != nil {
result := val.(float64) if result, ok := val.(float64); ok {
return &result return &result
}
} }
return nil return nil
} }
@ -180,8 +184,9 @@ func (a *Args) Bool(s string) *bool {
return nil return nil
} }
if val := a.data[s]; val != nil { if val := a.data[s]; val != nil {
result := val.(bool) if result, ok := val.(bool); ok {
return &result return &result
}
} }
return nil return nil
} }

View File

@ -1,6 +1,7 @@
package application package application
import ( import (
"fmt"
"net/http" "net/http"
) )
@ -17,33 +18,33 @@ var screensMethodNames = map[int]string{
} }
func (m *MessageProcessor) processScreensMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, _ QueryParams) { func (m *MessageProcessor) processScreensMethod(method int, rw http.ResponseWriter, _ *http.Request, _ Window, _ QueryParams) {
switch method { switch method {
case ScreensGetAll: case ScreensGetAll:
screens, err := globalApplication.GetScreens() screens, err := globalApplication.GetScreens()
if err != nil { if err != nil {
m.Error("GetAll: %s", err.Error()) m.httpError(rw, "GetScreens failed:", err)
return return
} }
m.json(rw, screens) m.json(rw, screens)
case ScreensGetPrimary: case ScreensGetPrimary:
screen, err := globalApplication.GetPrimaryScreen() screen, err := globalApplication.GetPrimaryScreen()
if err != nil { if err != nil {
m.Error("GetPrimary: %s", err.Error()) m.httpError(rw, "GetPrimary failed:", err)
return return
} }
m.json(rw, screen) m.json(rw, screen)
case ScreensGetCurrent: case ScreensGetCurrent:
screen, err := globalApplication.CurrentWindow().GetScreen() screen, err := globalApplication.CurrentWindow().GetScreen()
if err != nil { if err != nil {
m.Error("GetCurrent: %s", err.Error()) m.httpError(rw, "Window.GetScreen failed:", err)
return return
} }
m.json(rw, screen) m.json(rw, screen)
default: 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 package application
import ( import (
"fmt"
"net/http" "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) { func (m *MessageProcessor) processSystemMethod(method int, rw http.ResponseWriter, r *http.Request, window Window, params QueryParams) {
switch method { switch method {
case SystemIsDarkMode: case SystemIsDarkMode:
m.json(rw, globalApplication.IsDarkMode()) m.json(rw, globalApplication.IsDarkMode())
case Environment: case Environment:
m.json(rw, globalApplication.Environment()) m.json(rw, globalApplication.Environment())
default: 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 package application
import ( import (
"errors"
"fmt"
"net/http" "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) { func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWriter, _ *http.Request, window Window, params QueryParams) {
args, err := params.Args() args, err := params.Args()
if err != nil { 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 return
} }
@ -145,7 +146,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowGetScreen: case WindowGetScreen:
screen, err := window.GetScreen() screen, err := window.GetScreen()
if err != nil { if err != nil {
m.httpError(rw, err.Error()) m.httpError(rw, "Window.GetScreen failed:", err)
return return
} }
m.json(rw, screen) m.json(rw, screen)
@ -197,18 +198,20 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetPosition: case WindowSetPosition:
x := args.Int("x") x := args.Int("x")
if x == nil { 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") y := args.Int("y")
if y == nil { 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) window.SetPosition(*x, *y)
m.ok(rw) m.ok(rw)
case WindowSetAlwaysOnTop: case WindowSetAlwaysOnTop:
alwaysOnTop := args.Bool("alwaysOnTop") alwaysOnTop := args.Bool("alwaysOnTop")
if alwaysOnTop == nil { 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 return
} }
window.SetAlwaysOnTop(*alwaysOnTop) window.SetAlwaysOnTop(*alwaysOnTop)
@ -216,22 +219,22 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetBackgroundColour: case WindowSetBackgroundColour:
r := args.UInt8("r") r := args.UInt8("r")
if r == nil { 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 return
} }
g := args.UInt8("g") g := args.UInt8("g")
if g == nil { 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 return
} }
b := args.UInt8("b") b := args.UInt8("b")
if b == nil { 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 return
} }
a := args.UInt8("a") a := args.UInt8("a")
if a == nil { 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 return
} }
window.SetBackgroundColour(RGBA{ window.SetBackgroundColour(RGBA{
@ -244,7 +247,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetFrameless: case WindowSetFrameless:
frameless := args.Bool("frameless") frameless := args.Bool("frameless")
if frameless == nil { 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 return
} }
window.SetFrameless(*frameless) window.SetFrameless(*frameless)
@ -252,40 +255,46 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetMaxSize: case WindowSetMaxSize:
width := args.Int("width") width := args.Int("width")
if width == nil { 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") height := args.Int("height")
if height == nil { 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) window.SetMaxSize(*width, *height)
m.ok(rw) m.ok(rw)
case WindowSetMinSize: case WindowSetMinSize:
width := args.Int("width") width := args.Int("width")
if width == nil { 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") height := args.Int("height")
if height == nil { 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) window.SetMinSize(*width, *height)
m.ok(rw) m.ok(rw)
case WindowSetRelativePosition: case WindowSetRelativePosition:
x := args.Int("x") x := args.Int("x")
if x == nil { 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") y := args.Int("y")
if y == nil { 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) window.SetRelativePosition(*x, *y)
m.ok(rw) m.ok(rw)
case WindowSetResizable: case WindowSetResizable:
resizable := args.Bool("resizable") resizable := args.Bool("resizable")
if resizable == nil { 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 return
} }
window.SetResizable(*resizable) window.SetResizable(*resizable)
@ -293,18 +302,20 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetSize: case WindowSetSize:
width := args.Int("width") width := args.Int("width")
if width == nil { 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") height := args.Int("height")
if height == nil { 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) window.SetSize(*width, *height)
m.ok(rw) m.ok(rw)
case WindowSetTitle: case WindowSetTitle:
title := args.String("title") title := args.String("title")
if title == nil { if title == nil {
m.Error("Invalid SetTitle Message: 'title' value required") m.httpError(rw, "Invalid window call:", errors.New("missing argument 'title'"))
return return
} }
window.SetTitle(*title) window.SetTitle(*title)
@ -312,7 +323,7 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
case WindowSetZoom: case WindowSetZoom:
zoom := args.Float64("zoom") zoom := args.Float64("zoom")
if zoom == nil { 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 return
} }
window.SetZoom(*zoom) window.SetZoom(*zoom)
@ -360,8 +371,9 @@ func (m *MessageProcessor) processWindowMethod(method int, rw http.ResponseWrite
window.ZoomReset() window.ZoomReset()
m.ok(rw) m.ok(rw)
default: 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 // Get the error
var err error err, ok := e.(error)
if errPanic, ok := e.(error); ok { if !ok {
err = errPanic
} else {
err = fmt.Errorf("%v", e) err = fmt.Errorf("%v", e)
} }
@ -102,6 +100,5 @@ func processPanic(panicDetails *PanicDetails) {
} }
func defaultPanicHandler(panicDetails *PanicDetails) { func defaultPanicHandler(panicDetails *PanicDetails) {
errorMessage := fmt.Sprintf("panic error: %s\n%s", panicDetails.Error.Error(), panicDetails.StackTrace) globalApplication.fatal("panic error: %w\n%s", panicDetails.Error, panicDetails.StackTrace)
globalApplication.fatal(errorMessage)
} }

View File

@ -1,7 +1,6 @@
package application package application
import ( import (
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32" "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)) ok := w32.AppendMenu(parentMenu, flags, uintptr(itemID), w32.MustStringToUTF16Ptr(menuText))
if !ok { if !ok {
globalApplication.fatal(fmt.Sprintf("Error adding menu item: %s", menuText)) globalApplication.fatal("error adding menu item '%s'", menuText)
} }
if item.bitmap != nil { if item.bitmap != nil {
err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil) err := w32.SetMenuIcons(parentMenu, itemID, item.bitmap, nil)
if err != 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 package application
import ( import (
"fmt" "errors"
"strconv" "strconv"
"github.com/wailsapp/wails/v3/pkg/w32" "github.com/wailsapp/wails/v3/pkg/w32"
@ -80,7 +80,7 @@ func getScreenForWindowHwnd(hwnd w32.HWND) (*Screen, error) {
return screen, nil 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 { func hMonitorToScreenID(hMonitor uintptr) string {

View File

@ -1,7 +1,7 @@
package application package application
import ( import (
"fmt" "errors"
"math" "math"
"sort" "sort"
) )
@ -363,7 +363,7 @@ func (s *Screen) physicalToDipRect(physicalRect Rect) Rect {
// for future coordinate transformation between the physical and logical (DIP) space // for future coordinate transformation between the physical and logical (DIP) space
func (m *ScreenManager) LayoutScreens(screens []*Screen) error { func (m *ScreenManager) LayoutScreens(screens []*Screen) error {
if screens == nil || len(screens) == 0 { 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 m.screens = screens
@ -397,9 +397,9 @@ func (m *ScreenManager) calculateScreensDipCoordinates() error {
} }
} }
if m.primaryScreen == nil { 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 { } 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 // 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 // it will be mounted on the internal asset server
// at the prefix specified by Route. // at the prefix specified by Route.
Route string 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, // 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, // The context will be valid as long as the application is running,
// and will be canceled right before shutdown. // and will be canceled right before shutdown.
// //
// If the return value is non-nil, it is logged along with the service name, // Services are guaranteed to receive the startup notification
// the startup process aborts and the application quits. // 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 // When that happens, service instances that have been already initialised
// receive a shutdown notification. // receive a shutdown notification.
type ServiceStartup interface { type ServiceStartup interface {
@ -83,17 +102,33 @@ type ServiceStartup interface {
// ServiceShutdown is an *optional* method that may be implemented by service instances. // 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. // 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 { type ServiceShutdown interface {
ServiceShutdown() error ServiceShutdown() error
} }
func getServiceName(service any) string { func getServiceName(service Service) string {
// First check it conforms to ServiceName interface if service.options.Name != "" {
if serviceName, ok := service.(ServiceName); ok { return service.options.Name
return serviceName.ServiceName()
} }
// 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 package application
import ( import (
"fmt" "errors"
"github.com/godbus/dbus/v5"
"os" "os"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"github.com/godbus/dbus/v5"
) )
type dbusHandler func(string) type dbusHandler func(string)
@ -36,7 +37,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
func (l *linuxLock) acquire(uniqueID string) error { func (l *linuxLock) acquire(uniqueID string) error {
if uniqueID == "" { 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, "-", "_"), ".", "_") id := "wails_app_" + strings.ReplaceAll(strings.ReplaceAll(uniqueID, "-", "_"), ".", "_")
@ -56,11 +57,11 @@ func (l *linuxLock) acquire(uniqueID string) error {
secondInstanceBuffer <- message secondInstanceBuffer <- message
}) })
err := conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName) err = conn.Export(f, dbus.ObjectPath(l.dbusPath), l.dbusName)
if err != nil {
globalApplication.error(err.Error())
}
}) })
if err != nil {
return err
}
reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue) reply, err := conn.RequestName(l.dbusName, dbus.NameFlagDoNotQueue)
if err != nil { if err != nil {

View File

@ -4,11 +4,11 @@ package application
import ( import (
"errors" "errors"
"fmt"
"github.com/wailsapp/wails/v3/pkg/w32"
"golang.org/x/sys/windows"
"syscall" "syscall"
"unsafe" "unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
"golang.org/x/sys/windows"
) )
var ( var (
@ -33,7 +33,7 @@ func newPlatformLock(manager *singleInstanceManager) (platformLock, error) {
func (l *windowsLock) acquire(uniqueID string) error { func (l *windowsLock) acquire(uniqueID string) error {
if uniqueID == "" { 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 l.uniqueID = uniqueID

View File

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

View File

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

View File

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

View File

@ -3,12 +3,13 @@
package application package application
import ( import (
"fmt" "errors"
"github.com/wailsapp/wails/v3/pkg/icons"
"syscall" "syscall"
"time" "time"
"unsafe" "unsafe"
"github.com/wailsapp/wails/v3/pkg/icons"
"github.com/samber/lo" "github.com/samber/lo"
"github.com/wailsapp/wails/v3/pkg/events" "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) monitor := w32.MonitorFromWindow(s.hwnd, w32.MONITOR_DEFAULTTONEAREST)
if monitor == 0 { if monitor == 0 {
return nil, fmt.Errorf("failed to get monitor") return nil, errors.New("failed to get monitor")
} }
return &Rect{ return &Rect{
@ -186,7 +187,7 @@ func (s *windowsSystemTray) run() {
for retries := range 6 { for retries := range 6 {
if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) { if !w32.ShellNotifyIcon(w32.NIM_ADD, &nid) {
if retries == 5 { 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) time.Sleep(500 * time.Millisecond)

View File

@ -1,13 +1,13 @@
package application package application
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"runtime" "runtime"
"slices" "slices"
"strings" "strings"
"sync" "sync"
"text/template"
"github.com/leaanthony/u" "github.com/leaanthony/u"
@ -276,7 +276,7 @@ func processKeyBindingOptions(keyBindings map[string]func(window *WebviewWindow)
// Parse the key to an accelerator // Parse the key to an accelerator
acc, err := parseAccelerator(key) acc, err := parseAccelerator(key)
if err != nil { if err != nil {
globalApplication.error("Invalid keybinding: %s", err.Error()) globalApplication.error("invalid keybinding: %w", err)
continue continue
} }
result[acc.String()] = callback result[acc.String()] = callback
@ -291,40 +291,27 @@ func (w *WebviewWindow) addCancellationFunction(canceller func()) {
w.cancellers = append(w.cancellers, canceller) w.cancellers = append(w.cancellers, canceller)
} }
// formatJS ensures the 'data' provided marshals to valid json or panics func (w *WebviewWindow) CallError(callID string, result string, isJSON bool) {
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) {
if w.impl != nil { 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) { func (w *WebviewWindow) CallResponse(callID string, result string) {
if w.impl != nil { 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) { func (w *WebviewWindow) DialogError(dialogID string, result string) {
if w.impl != nil { 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) { func (w *WebviewWindow) DialogResponse(dialogID string, result string, isJSON bool) {
if w.impl != nil { if w.impl != nil {
if isJSON { w.impl.execJS(fmt.Sprintf("_wails.dialogResultCallback('%s', '%s', %t);", dialogID, template.JSEscapeString(result), 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))
}
} }
} }
@ -690,7 +677,7 @@ func (w *WebviewWindow) HandleMessage(message string) {
InvokeSync(func() { InvokeSync(func() {
err := w.startDrag() err := w.startDrag()
if err != nil { 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() { if !w.IsFullscreen() {
sl := strings.Split(message, ":") sl := strings.Split(message, ":")
if len(sl) != 3 { if len(sl) != 3 {
w.Error("Unknown message returned from dispatcher", "message", message) w.Error("unknown message returned from dispatcher: %s", message)
return return
} }
err := w.startResize(sl[2]) err := w.startResize(sl[2])
if err != nil { if err != nil {
w.Error(err.Error()) w.Error("%w", err)
} }
} }
case message == "wails:runtime:ready": case message == "wails:runtime:ready":
@ -714,7 +701,7 @@ func (w *WebviewWindow) HandleMessage(message string) {
w.ExecJS(js) w.ExecJS(js)
} }
default: 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) { func (w *WebviewWindow) Error(message string, args ...any) {
var messageArgs []interface{} args = append([]any{w.Name()}, args...)
messageArgs = append(messageArgs, args...) globalApplication.error("in window '%s': "+message, args...)
messageArgs = append(messageArgs, "sender", w.Name())
globalApplication.error(message, messageArgs...)
} }
func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) { func (w *WebviewWindow) HandleDragAndDropMessage(filenames []string) {
@ -1182,7 +1167,7 @@ func (w *WebviewWindow) OpenContextMenu(data *ContextMenuData) {
// try application level context menu // try application level context menu
menu, ok := globalApplication.getContextMenu(data.Id) menu, ok := globalApplication.getContextMenu(data.Id)
if !ok { 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 return
} }
menu.setContextData(data) menu.setContextData(data)

View File

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

View File

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

View File

@ -183,7 +183,7 @@ func (w *windowsWebviewWindow) print() error {
func (w *windowsWebviewWindow) startResize(border string) error { func (w *windowsWebviewWindow) startResize(border string) error {
if !w32.ReleaseCapture() { 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. // 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) 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 { func (w *windowsWebviewWindow) startDrag() error {
if !w32.ReleaseCapture() { 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. // 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) w32.PostMessage(w.hwnd, w32.WM_NCLBUTTONDOWN, w32.HTCAPTION, 0)
@ -334,7 +334,7 @@ func (w *windowsWebviewWindow) run() {
nil) nil)
if w.hwnd == 0 { 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. // 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) data, err := pngToImage(imageData)
if err != nil { 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) bitmap, err := w32.CreateHBITMAPFromImage(data)
@ -1513,15 +1513,15 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ") useragent = strings.Join([]string{useragent, assetserver.WailsUserAgentValue}, " ")
err = reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent) err = reqHeaders.SetHeader(assetserver.HeaderUserAgent, useragent)
if err != nil { 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)) err = reqHeaders.SetHeader(webViewRequestHeaderWindowId, strconv.FormatUint(uint64(w.parent.id), 10))
if err != nil { if err != nil {
globalApplication.fatal("Error setting WindowId header: " + err.Error()) globalApplication.fatal("error setting WindowId header: %w", err)
} }
err = reqHeaders.Release() err = reqHeaders.Release()
if err != nil { 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() uri, _ := req.GetUri()
reqUri, err := url.ParseRequestURI(uri) reqUri, err := url.ParseRequestURI(uri)
if err != nil { 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 return
} }
@ -1553,7 +1553,7 @@ func (w *windowsWebviewWindow) processRequest(req *edge.ICoreWebView2WebResource
InvokeSync(fn) InvokeSync(fn)
}) })
if err != nil { if err != nil {
globalApplication.error("%s: NewRequest failed: %s", uri, err) globalApplication.error("%s: NewRequest failed: %w", uri, err)
return return
} }
@ -1572,7 +1572,7 @@ func (w *windowsWebviewWindow) setupChromium() {
webview2version, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(globalApplication.options.Windows.WebviewBrowserPath) webview2version, err := webviewloader.GetAvailableCoreWebView2BrowserVersionString(globalApplication.options.Windows.WebviewBrowserPath)
if err != nil { if err != nil {
globalApplication.error("Error getting WebView2 version: " + err.Error()) globalApplication.error("error getting WebView2 version: %w", err)
return return
} }
globalApplication.capabilities = capabilities.NewCapabilities(webview2version) globalApplication.capabilities = capabilities.NewCapabilities(webview2version)
@ -1614,14 +1614,14 @@ func (w *windowsWebviewWindow) setupChromium() {
if chromium.HasCapability(edge.SwipeNavigation) { if chromium.HasCapability(edge.SwipeNavigation) {
err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures) err := chromium.PutIsSwipeNavigationEnabled(opts.EnableSwipeGestures)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
if chromium.HasCapability(edge.AllowExternalDrop) { if chromium.HasCapability(edge.AllowExternalDrop) {
err := chromium.AllowExternalDrag(false) err := chromium.AllowExternalDrag(false)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
if w.parent.options.EnableDragAndDrop { if w.parent.options.EnableDragAndDrop {
@ -1657,7 +1657,7 @@ func (w *windowsWebviewWindow) setupChromium() {
//if windowName == "Chrome_RenderWidgetHostHWND" { //if windowName == "Chrome_RenderWidgetHostHWND" {
err := w32.RegisterDragDrop(hwnd, w.dropTarget) err := w32.RegisterDragDrop(hwnd, w.dropTarget)
if err != nil && !errors.Is(err, syscall.Errno(w32.DRAGDROP_E_ALREADYREGISTERED)) { 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 return 1
@ -1672,7 +1672,7 @@ func (w *windowsWebviewWindow) setupChromium() {
// warning // warning
globalApplication.warning("unsupported capability: GeneralAutofillEnabled") globalApplication.warning("unsupported capability: GeneralAutofillEnabled")
} else { } else {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
} }
@ -1683,7 +1683,7 @@ func (w *windowsWebviewWindow) setupChromium() {
if errors.Is(edge.UnsupportedCapabilityError, err) { if errors.Is(edge.UnsupportedCapabilityError, err) {
globalApplication.warning("unsupported capability: PasswordAutosaveEnabled") globalApplication.warning("unsupported capability: PasswordAutosaveEnabled")
} else { } else {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
} }
} }
@ -1692,7 +1692,7 @@ func (w *windowsWebviewWindow) setupChromium() {
//if chromium.HasCapability(edge.AllowExternalDrop) { //if chromium.HasCapability(edge.AllowExternalDrop) {
// err := chromium.AllowExternalDrag(w.parent.options.EnableDragAndDrop) // err := chromium.AllowExternalDrag(w.parent.options.EnableDragAndDrop)
// if err != nil { // if err != nil {
// globalApplication.fatal(err.Error()) // globalApplication.handleFatalError(err)
// } // }
// if w.parent.options.EnableDragAndDrop { // if w.parent.options.EnableDragAndDrop {
// chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects // chromium.MessageWithAdditionalObjectsCallback = w.processMessageWithAdditionalObjects
@ -1702,14 +1702,14 @@ func (w *windowsWebviewWindow) setupChromium() {
chromium.Resize() chromium.Resize()
settings, err := chromium.GetSettings() settings, err := chromium.GetSettings()
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
if settings == nil { if settings == nil {
globalApplication.fatal("Error getting settings") globalApplication.fatal("error getting settings")
} }
err = settings.PutAreDefaultContextMenusEnabled(debugMode || !w.parent.options.DefaultContextMenuDisabled) err = settings.PutAreDefaultContextMenusEnabled(debugMode || !w.parent.options.DefaultContextMenuDisabled)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
w.enableDevTools(settings) w.enableDevTools(settings)
@ -1719,20 +1719,20 @@ func (w *windowsWebviewWindow) setupChromium() {
} }
err = settings.PutIsZoomControlEnabled(w.parent.options.ZoomControlEnabled) err = settings.PutIsZoomControlEnabled(w.parent.options.ZoomControlEnabled)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
err = settings.PutIsStatusBarEnabled(false) err = settings.PutIsStatusBarEnabled(false)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
err = settings.PutAreBrowserAcceleratorKeysEnabled(false) err = settings.PutAreBrowserAcceleratorKeysEnabled(false)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
err = settings.PutIsSwipeNavigationEnabled(false) err = settings.PutIsSwipeNavigationEnabled(false)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
if debugMode && w.parent.options.OpenInspectorOnStartup { if debugMode && w.parent.options.OpenInspectorOnStartup {
@ -1761,7 +1761,7 @@ func (w *windowsWebviewWindow) setupChromium() {
} else { } else {
startURL, err := assetserver.GetStartURL(w.parent.options.URL) startURL, err := assetserver.GetStartURL(w.parent.options.URL)
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
w.webviewNavigationCompleted = false w.webviewNavigationCompleted = false
chromium.Navigate(startURL) chromium.Navigate(startURL)
@ -1772,7 +1772,7 @@ func (w *windowsWebviewWindow) setupChromium() {
func (w *windowsWebviewWindow) fullscreenChanged(sender *edge.ICoreWebView2, _ *edge.ICoreWebView2ContainsFullScreenElementChangedEventArgs) { func (w *windowsWebviewWindow) fullscreenChanged(sender *edge.ICoreWebView2, _ *edge.ICoreWebView2ContainsFullScreenElementChangedEventArgs) {
isFullscreen, err := sender.GetContainsFullScreenElement() isFullscreen, err := sender.GetContainsFullScreenElement()
if err != nil { if err != nil {
globalApplication.fatal("Fatal error in callback fullscreenChanged: " + err.Error()) globalApplication.fatal("fatal error in callback fullscreenChanged: %w", err)
} }
if isFullscreen { if isFullscreen {
w.fullscreen() 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 // Hack to make it visible: https://github.com/MicrosoftEdge/WebView2Feedback/issues/1077#issuecomment-825375026
err := w.chromium.Hide() err := w.chromium.Hide()
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
err = w.chromium.Show() err = w.chromium.Show()
if err != nil { if err != nil {
globalApplication.fatal(err.Error()) globalApplication.handleFatalError(err)
} }
if wasFocused { if wasFocused {
w.focus() w.focus()
@ -1839,7 +1839,7 @@ func (w *windowsWebviewWindow) processKeyBinding(vkey uint) bool {
// Get the keyboard state and convert to an accelerator // Get the keyboard state and convert to an accelerator
var keyState [256]byte var keyState [256]byte
if !w32.GetKeyboardState(keyState[:]) { if !w32.GetKeyboardState(keyState[:]) {
globalApplication.error("Error getting keyboard state") globalApplication.error("error getting keyboard state")
return false return false
} }
@ -1890,20 +1890,20 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
if strings.HasPrefix(message, "FilesDropped") { if strings.HasPrefix(message, "FilesDropped") {
objs, err := args.GetAdditionalObjects() objs, err := args.GetAdditionalObjects()
if err != nil { if err != nil {
globalApplication.error(err.Error()) globalApplication.handleError(err)
return return
} }
defer func() { defer func() {
err = objs.Release() err = objs.Release()
if err != nil { if err != nil {
globalApplication.error("Error releasing objects: " + err.Error()) globalApplication.error("error releasing objects: %w", err)
} }
}() }()
count, err := objs.GetCount() count, err := objs.GetCount()
if err != nil { if err != nil {
globalApplication.error("cannot get count: %s", err.Error()) globalApplication.error("cannot get count: %w", err)
return return
} }
@ -1911,7 +1911,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
for i := uint32(0); i < count; i++ { for i := uint32(0); i < count; i++ {
_file, err := objs.GetValueAtIndex(i) _file, err := objs.GetValueAtIndex(i)
if err != nil { 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 return
} }
@ -1922,7 +1922,7 @@ func (w *windowsWebviewWindow) processMessageWithAdditionalObjects(message strin
filepath, err := file.GetPath() filepath, err := file.GetPath()
if err != nil { 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 return
} }
@ -1983,7 +1983,7 @@ func NewIconFromResource(instance w32.HINSTANCE, resId uint16) (w32.HICON, error
var err error var err error
var result w32.HICON var result w32.HICON
if result = w32.LoadIconWithResourceID(instance, resId); result == 0 { 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 return result, err
} }

View File

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

View File

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