mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 13:22:55 +08:00

* 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>
399 lines
10 KiB
Go
399 lines
10 KiB
Go
package application
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/wailsapp/wails/v3/internal/hash"
|
|
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type CallOptions struct {
|
|
MethodID uint32 `json:"methodID"`
|
|
MethodName string `json:"methodName"`
|
|
Args []json.RawMessage `json:"args"`
|
|
}
|
|
|
|
type ErrorKind string
|
|
|
|
const (
|
|
ReferenceError ErrorKind = "ReferenceError"
|
|
TypeError ErrorKind = "TypeError"
|
|
RuntimeError ErrorKind = "RuntimeError"
|
|
)
|
|
|
|
type CallError struct {
|
|
Kind ErrorKind `json:"kind"`
|
|
Message string `json:"message"`
|
|
Cause any `json:"cause,omitempty"`
|
|
}
|
|
|
|
func (e *CallError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// Parameter defines a Go method parameter
|
|
type Parameter struct {
|
|
Name string `json:"name,omitempty"`
|
|
TypeName string `json:"type"`
|
|
ReflectType reflect.Type
|
|
}
|
|
|
|
func newParameter(Name string, Type reflect.Type) *Parameter {
|
|
return &Parameter{
|
|
Name: Name,
|
|
TypeName: Type.String(),
|
|
ReflectType: Type,
|
|
}
|
|
}
|
|
|
|
// IsType returns true if the given
|
|
func (p *Parameter) IsType(typename string) bool {
|
|
return p.TypeName == typename
|
|
}
|
|
|
|
// IsError returns true if the parameter type is an error
|
|
func (p *Parameter) IsError() bool {
|
|
return p.IsType("error")
|
|
}
|
|
|
|
// BoundMethod defines all the data related to a Go method that is
|
|
// bound to the Wails application
|
|
type BoundMethod struct {
|
|
ID uint32 `json:"id"`
|
|
Name string `json:"name"`
|
|
Inputs []*Parameter `json:"inputs,omitempty"`
|
|
Outputs []*Parameter `json:"outputs,omitempty"`
|
|
Comments string `json:"comments,omitempty"`
|
|
Method reflect.Value `json:"-"`
|
|
FQN string
|
|
|
|
marshalError func(error) []byte
|
|
needsContext bool
|
|
}
|
|
|
|
type Bindings struct {
|
|
marshalError func(error) []byte
|
|
boundMethods map[string]*BoundMethod
|
|
boundByID map[uint32]*BoundMethod
|
|
methodAliases map[uint32]uint32
|
|
}
|
|
|
|
func NewBindings(marshalError func(error) []byte, aliases map[uint32]uint32) *Bindings {
|
|
return &Bindings{
|
|
marshalError: wrapErrorMarshaler(marshalError, defaultMarshalError),
|
|
boundMethods: make(map[string]*BoundMethod),
|
|
boundByID: make(map[uint32]*BoundMethod),
|
|
methodAliases: aliases,
|
|
}
|
|
}
|
|
|
|
// Add adds the given service to the bindings.
|
|
func (b *Bindings) Add(service Service) error {
|
|
methods, err := getMethods(service.Instance())
|
|
if err != nil {
|
|
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 {
|
|
// Store composite error marshaler
|
|
method.marshalError = marshalError
|
|
|
|
// Register method
|
|
b.boundMethods[method.FQN] = method
|
|
b.boundByID[method.ID] = method
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get returns the bound method with the given name
|
|
func (b *Bindings) Get(options *CallOptions) *BoundMethod {
|
|
return b.boundMethods[options.MethodName]
|
|
}
|
|
|
|
// GetByID returns the bound method with the given ID
|
|
func (b *Bindings) GetByID(id uint32) *BoundMethod {
|
|
// Check method aliases
|
|
if b.methodAliases != nil {
|
|
if alias, ok := b.methodAliases[id]; ok {
|
|
id = alias
|
|
}
|
|
}
|
|
|
|
return b.boundByID[id]
|
|
}
|
|
|
|
// internalServiceMethod is a set of methods
|
|
// that are handled specially by the binding engine
|
|
// and must not be exposed to the frontend.
|
|
//
|
|
// For simplicity we exclude these by name
|
|
// without checking their signatures,
|
|
// and so does the binding generator.
|
|
var internalServiceMethods = map[string]bool{
|
|
"ServiceName": true,
|
|
"ServiceStartup": true,
|
|
"ServiceShutdown": true,
|
|
"ServeHTTP": true,
|
|
}
|
|
|
|
var ctxType = reflect.TypeFor[context.Context]()
|
|
|
|
func getMethods(value any) ([]*BoundMethod, error) {
|
|
// Create result placeholder
|
|
var result []*BoundMethod
|
|
|
|
// Check type
|
|
if !isNamed(value) {
|
|
if isFunction(value) {
|
|
name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name()
|
|
return nil, fmt.Errorf("%s is a function, not a pointer to named type. Wails v2 has deprecated the binding of functions. Please define your functions as methods on a struct and bind a pointer to that struct", name)
|
|
}
|
|
|
|
return nil, fmt.Errorf("%s is not a pointer to named type", reflect.ValueOf(value).Type().String())
|
|
} else if !isPtr(value) {
|
|
return nil, fmt.Errorf("%s is a named type, not a pointer to named type", reflect.ValueOf(value).Type().String())
|
|
}
|
|
|
|
// Process Named Type
|
|
namedValue := reflect.ValueOf(value)
|
|
ptrType := namedValue.Type()
|
|
namedType := ptrType.Elem()
|
|
typeName := namedType.Name()
|
|
packagePath := namedType.PkgPath()
|
|
|
|
if strings.Contains(namedType.String(), "[") {
|
|
return nil, fmt.Errorf("%s.%s is a generic type. Generic bound types are not supported", packagePath, namedType.String())
|
|
}
|
|
|
|
// Process Methods
|
|
for i := range ptrType.NumMethod() {
|
|
methodName := ptrType.Method(i).Name
|
|
method := namedValue.Method(i)
|
|
|
|
if internalServiceMethods[methodName] {
|
|
continue
|
|
}
|
|
|
|
fqn := fmt.Sprintf("%s.%s.%s", packagePath, typeName, methodName)
|
|
|
|
// Create new method
|
|
boundMethod := &BoundMethod{
|
|
ID: hash.Fnv(fqn),
|
|
FQN: fqn,
|
|
Name: methodName,
|
|
Inputs: nil,
|
|
Outputs: nil,
|
|
Comments: "",
|
|
Method: method,
|
|
}
|
|
|
|
// Iterate inputs
|
|
methodType := method.Type()
|
|
inputParamCount := methodType.NumIn()
|
|
var inputs []*Parameter
|
|
for inputIndex := 0; inputIndex < inputParamCount; inputIndex++ {
|
|
input := methodType.In(inputIndex)
|
|
if inputIndex == 0 && input.AssignableTo(ctxType) {
|
|
boundMethod.needsContext = true
|
|
}
|
|
thisParam := newParameter("", input)
|
|
inputs = append(inputs, thisParam)
|
|
}
|
|
|
|
boundMethod.Inputs = inputs
|
|
|
|
outputParamCount := methodType.NumOut()
|
|
var outputs []*Parameter
|
|
for outputIndex := 0; outputIndex < outputParamCount; outputIndex++ {
|
|
output := methodType.Out(outputIndex)
|
|
thisParam := newParameter("", output)
|
|
outputs = append(outputs, thisParam)
|
|
}
|
|
boundMethod.Outputs = outputs
|
|
|
|
// Save method in result
|
|
result = append(result, boundMethod)
|
|
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (b *BoundMethod) String() string {
|
|
return b.FQN
|
|
}
|
|
|
|
var errorType = reflect.TypeFor[error]()
|
|
|
|
// Call will attempt to call this bound method with the given args.
|
|
// If the call succeeds, result will be either a non-error return value (if there is only one)
|
|
// or a slice of non-error return values (if there are more than one).
|
|
//
|
|
// If the arguments are mistyped or the call returns one or more non-nil error values,
|
|
// result is nil and err is an instance of *[CallError].
|
|
func (b *BoundMethod) Call(ctx context.Context, args []json.RawMessage) (result any, err error) {
|
|
// Use a defer statement to capture panics
|
|
defer handlePanic(handlePanicOptions{skipEnd: 5})
|
|
argCount := len(args)
|
|
if b.needsContext {
|
|
argCount++
|
|
}
|
|
|
|
if argCount != len(b.Inputs) {
|
|
err = &CallError{
|
|
Kind: TypeError,
|
|
Message: fmt.Sprintf("%s expects %d arguments, got %d", b.FQN, len(b.Inputs), argCount),
|
|
}
|
|
return
|
|
}
|
|
|
|
// Convert inputs to values of appropriate type
|
|
callArgs := make([]reflect.Value, argCount)
|
|
base := 0
|
|
|
|
if b.needsContext {
|
|
callArgs[0] = reflect.ValueOf(ctx)
|
|
base++
|
|
}
|
|
|
|
// Iterate over given arguments
|
|
for index, arg := range args {
|
|
value := reflect.New(b.Inputs[base+index].ReflectType)
|
|
err = json.Unmarshal(arg, value.Interface())
|
|
if err != nil {
|
|
err = &CallError{
|
|
Kind: TypeError,
|
|
Message: fmt.Sprintf("could not parse argument #%d: %s", index, err),
|
|
Cause: json.RawMessage(b.marshalError(err)),
|
|
}
|
|
return
|
|
}
|
|
callArgs[base+index] = value.Elem()
|
|
}
|
|
|
|
// Do the call
|
|
var callResults []reflect.Value
|
|
if b.Method.Type().IsVariadic() {
|
|
callResults = b.Method.CallSlice(callArgs)
|
|
} else {
|
|
callResults = b.Method.Call(callArgs)
|
|
}
|
|
|
|
var nonErrorOutputs = make([]any, 0, len(callResults))
|
|
var errorOutputs []error
|
|
|
|
for _, field := range callResults {
|
|
if field.Type() == errorType {
|
|
if field.IsNil() {
|
|
continue
|
|
}
|
|
if errorOutputs == nil {
|
|
errorOutputs = make([]error, 0, len(callResults)-len(nonErrorOutputs))
|
|
nonErrorOutputs = nil
|
|
}
|
|
errorOutputs = append(errorOutputs, field.Interface().(error))
|
|
} else if nonErrorOutputs != nil {
|
|
nonErrorOutputs = append(nonErrorOutputs, field.Interface())
|
|
}
|
|
}
|
|
|
|
if len(errorOutputs) > 0 {
|
|
info := make([]json.RawMessage, len(errorOutputs))
|
|
for i, err := range errorOutputs {
|
|
info[i] = b.marshalError(err)
|
|
}
|
|
|
|
cerr := &CallError{
|
|
Kind: RuntimeError,
|
|
Message: errors.Join(errorOutputs...).Error(),
|
|
Cause: info,
|
|
}
|
|
if len(info) == 1 {
|
|
cerr.Cause = info[0]
|
|
}
|
|
|
|
err = cerr
|
|
} else if len(nonErrorOutputs) == 1 {
|
|
result = nonErrorOutputs[0]
|
|
} else if len(nonErrorOutputs) > 1 {
|
|
result = nonErrorOutputs
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// wrapErrorMarshaler returns an error marshaling functions
|
|
// that calls the primary marshaler first,
|
|
// then falls back to the secondary one.
|
|
func wrapErrorMarshaler(primary func(error) []byte, secondary func(error) []byte) func(error) []byte {
|
|
if primary == nil {
|
|
return secondary
|
|
}
|
|
|
|
return func(err error) []byte {
|
|
result := primary(err)
|
|
if result == nil {
|
|
result = secondary(err)
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
// defaultMarshalError implements the default error marshaling mechanism.
|
|
func defaultMarshalError(err error) []byte {
|
|
result, jsonErr := json.Marshal(&err)
|
|
if jsonErr != nil {
|
|
return nil
|
|
}
|
|
return result
|
|
}
|
|
|
|
// isPtr returns true if the value given is a pointer.
|
|
func isPtr(value interface{}) bool {
|
|
return reflect.ValueOf(value).Kind() == reflect.Ptr
|
|
}
|
|
|
|
// isFunction returns true if the given value is a function
|
|
func isFunction(value interface{}) bool {
|
|
return reflect.ValueOf(value).Kind() == reflect.Func
|
|
}
|
|
|
|
// isNamed returns true if the given value is of named type
|
|
// or pointer to named type.
|
|
func isNamed(value interface{}) bool {
|
|
rv := reflect.ValueOf(value)
|
|
if rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
|
|
return rv.Type().Name() != ""
|
|
}
|