Merge commit '25a157e6614739fbd4df1da3d11d46afe72ae9a8' as 'v2'

This commit is contained in:
Travis McLane
2020-09-01 19:34:51 -05:00
208 changed files with 23636 additions and 0 deletions

View File

@@ -0,0 +1,113 @@
package subsystem
import (
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/runtime/goruntime"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Binding is the Binding subsystem. It manages all service bus messages
// starting with "binding".
type Binding struct {
quitChannel <-chan *servicebus.Message
bindingChannel <-chan *servicebus.Message
running bool
// Binding db
bindings *binding.Bindings
// logger
logger logger.CustomLogger
// runtime
runtime *goruntime.Runtime
}
// NewBinding creates a new binding subsystem. Uses the given bindings db for reference.
func NewBinding(bus *servicebus.ServiceBus, logger *logger.Logger, bindings *binding.Bindings, runtime *goruntime.Runtime) (*Binding, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
bindingChannel, err := bus.Subscribe("binding")
if err != nil {
return nil, err
}
result := &Binding{
quitChannel: quitChannel,
bindingChannel: bindingChannel,
logger: logger.CustomLogger("Binding Subsystem"),
bindings: bindings,
runtime: runtime,
}
// Call WailsInit methods once the frontend is loaded
// TODO: Double check that this is actually being emitted
// when we want it to be
runtime.Events.On("wails:loaded", func(...interface{}) {
result.logger.Trace("Calling WailsInit() methods")
result.CallWailsInit()
})
return result, nil
}
// Start the subsystem
func (b *Binding) Start() error {
b.running = true
b.logger.Trace("Starting")
// Spin off a go routine
go func() {
for b.running {
select {
case <-b.quitChannel:
b.running = false
case bindingMessage := <-b.bindingChannel:
b.logger.Trace("Got binding message: %+v", bindingMessage)
}
}
// Call shutdown
b.shutdown()
}()
return nil
}
// CallWailsInit will callback to the registered WailsInit
// methods with the runtime object
func (b *Binding) CallWailsInit() error {
for _, wailsinit := range b.bindings.DB().WailsInitMethods() {
_, err := wailsinit.Call([]interface{}{b.runtime})
if err != nil {
return err
}
}
return nil
}
// CallWailsShutdown will callback to the registered WailsShutdown
// methods with the runtime object
func (b *Binding) CallWailsShutdown() error {
for _, wailsshutdown := range b.bindings.DB().WailsShutdownMethods() {
_, err := wailsshutdown.Call([]interface{}{})
if err != nil {
return err
}
}
return nil
}
func (b *Binding) shutdown() {
b.CallWailsShutdown()
b.logger.Trace("Shutdown")
}

View File

@@ -0,0 +1,148 @@
package subsystem
import (
"encoding/json"
"fmt"
"github.com/leaanthony/wailsv2/v2/internal/binding"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Call is the Call subsystem. It manages all service bus messages
// starting with "call".
type Call struct {
quitChannel <-chan *servicebus.Message
callChannel <-chan *servicebus.Message
running bool
// bindings DB
DB *binding.DB
// ServiceBus
bus *servicebus.ServiceBus
// logger
logger logger.CustomLogger
}
// NewCall creates a new log subsystem
func NewCall(bus *servicebus.ServiceBus, logger *logger.Logger, DB *binding.DB) (*Call, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
callChannel, err := bus.Subscribe("call:invoke")
if err != nil {
return nil, err
}
result := &Call{
quitChannel: quitChannel,
callChannel: callChannel,
logger: logger.CustomLogger("Call Subsystem"),
DB: DB,
bus: bus,
}
return result, nil
}
// Start the subsystem
func (c *Call) Start() error {
c.running = true
// Spin off a go routine
go func() {
for c.running {
select {
case <-c.quitChannel:
c.running = false
case callMessage := <-c.callChannel:
c.processCall(callMessage)
}
}
// Call shutdown
c.shutdown()
}()
return nil
}
func (c *Call) processCall(callMessage *servicebus.Message) {
c.logger.Trace("Got message: %+v", callMessage)
// Extract payload
payload := callMessage.Data().(*message.CallMessage)
// Lookup method
registeredMethod := c.DB.GetMethod(payload.Name)
// Check we have it
if registeredMethod == nil {
c.sendError(fmt.Errorf("Method not registered"), payload, callMessage.Target())
return
}
c.logger.Trace("Got registered method: %+v", registeredMethod)
result, err := registeredMethod.Call(payload.Args)
if err != nil {
c.sendError(err, payload, callMessage.Target())
return
}
c.logger.Trace("registeredMethod.Call: %+v, %+v", result, err)
// process result
c.sendResult(result, payload, callMessage.Target())
}
func (c *Call) sendResult(result interface{}, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending success result with CallbackID '%s' : %+v\n", payload.CallbackID, result)
message := &CallbackMessage{
Result: result,
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(message)
c.logger.Trace("json message data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
func (c *Call) sendError(err error, payload *message.CallMessage, clientID string) {
c.logger.Trace("Sending error result with CallbackID '%s' : %+v\n", payload.CallbackID, err.Error())
message := &CallbackMessage{
Err: err.Error(),
CallbackID: payload.CallbackID,
}
messageData, err := json.Marshal(message)
c.logger.Trace("json message data: %+v\n", string(messageData))
if err != nil {
// what now?
c.logger.Fatal(err.Error())
}
c.bus.PublishForTarget("call:result", string(messageData), clientID)
}
func (c *Call) shutdown() {
c.logger.Trace("Shutdown")
}
// CallbackMessage defines a message that contains the result of a call
type CallbackMessage struct {
Result interface{} `json:"result"`
Err string `json:"error"`
CallbackID string `json:"callbackid"`
}

View File

@@ -0,0 +1,193 @@
package subsystem
import (
"strings"
"sync"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// eventListener holds a callback function which is invoked when
// the event listened for is emitted. It has a counter which indicates
// how the total number of events it is interested in. A value of zero
// means it does not expire (default).
type eventListener struct {
callback func(...interface{}) // Function to call with emitted event data
counter int64 // The number of times this callback may be called. -1 = infinite
delete bool // Flag to indicate that this listener should be deleted
}
// Event is the Eventing subsystem. It manages all service bus messages
// starting with "event".
type Event struct {
quitChannel <-chan *servicebus.Message
eventChannel <-chan *servicebus.Message
running bool
// Event listeners
listeners map[string][]*eventListener
notifyLock sync.RWMutex
// logger
logger logger.CustomLogger
}
// NewEvent creates a new log subsystem
func NewEvent(bus *servicebus.ServiceBus, logger *logger.Logger) (*Event, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to event messages
eventChannel, err := bus.Subscribe("event")
if err != nil {
return nil, err
}
result := &Event{
quitChannel: quitChannel,
eventChannel: eventChannel,
logger: logger.CustomLogger("Event Subsystem"),
listeners: make(map[string][]*eventListener),
}
return result, nil
}
// RegisterListener provides a means of subscribing to events of type "eventName"
func (e *Event) RegisterListener(eventName string, callback func(...interface{})) {
// Create new eventListener
thisListener := &eventListener{
callback: callback,
counter: 0,
delete: false,
}
e.notifyLock.Lock()
// Append the new listener to the listeners slice
e.listeners[eventName] = append(e.listeners[eventName], thisListener)
e.notifyLock.Unlock()
}
// Start the subsystem
func (e *Event) Start() error {
e.logger.Trace("Starting")
e.running = true
// Spin off a go routine
go func() {
for e.running {
select {
case <-e.quitChannel:
e.running = false
break
case eventMessage := <-e.eventChannel:
splitTopic := strings.Split(eventMessage.Topic(), ":")
eventType := splitTopic[1]
switch eventType {
case "emit":
if len(splitTopic) != 4 {
e.logger.Error("Received emit message with invalid topic format. Expected 4 sections in topic, got %s", splitTopic)
continue
}
eventSource := splitTopic[3]
e.logger.Trace("Got Event Message: %s %+v", eventMessage.Topic(), eventMessage.Data())
event := eventMessage.Data().(*message.EventMessage)
eventName := event.Name
switch eventSource {
case "j":
// Notify Go Subscribers
e.logger.Trace("Notify Go subscribers to event '%s'", eventName)
go e.notifyListeners(eventName, event)
case "g":
// Notify Go listeners
e.logger.Trace("Got Go Event: %s", eventName)
go e.notifyListeners(eventName, event)
default:
e.logger.Error("unknown emit event message: %+v", eventMessage)
}
case "on":
// We wish to subscribe to an event channel
var message *message.OnEventMessage = eventMessage.Data().(*message.OnEventMessage)
eventName := message.Name
callback := message.Callback
e.RegisterListener(eventName, callback)
e.logger.Trace("Registered listener for event '%s' with callback %p", eventName, callback)
default:
e.logger.Error("unknown event message: %+v", eventMessage)
}
}
}
// Call shutdown
e.shutdown()
}()
return nil
}
// Notifies listeners for the given event name
func (e *Event) notifyListeners(eventName string, message *message.EventMessage) {
// Get list of event listeners
listeners := e.listeners[eventName]
if listeners == nil {
println("no listeners for", eventName)
return
}
// Lock the listeners
e.notifyLock.Lock()
// We have a dirty flag to indicate that there are items to delete
itemsToDelete := false
// Callback in goroutine
for _, listener := range listeners {
if listener.counter > 0 {
listener.counter--
}
go listener.callback(message.Data...)
if listener.counter == 0 {
listener.delete = true
itemsToDelete = true
}
}
// Do we have items to delete?
if itemsToDelete == true {
// Create a new Listeners slice
var newListeners = []*eventListener{}
// Iterate over current listeners
for _, listener := range listeners {
// If we aren't deleting the listener, add it to the new list
if !listener.delete {
newListeners = append(newListeners, listener)
}
}
// Save new listeners
e.listeners[eventName] = newListeners
}
// Unlock
e.notifyLock.Unlock()
}
func (e *Event) shutdown() {
e.logger.Trace("Shutdown")
}

View File

@@ -0,0 +1,50 @@
package subsystem
import (
"os"
"sync"
"testing"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/messagedispatcher/message"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
"github.com/matryer/is"
)
func TestSingleTopic(t *testing.T) {
is := is.New(t)
var expected string = "I am a message!"
var actual string
var wg sync.WaitGroup
// Create new bus
myLogger := logger.New(os.Stdout)
myLogger.SetLogLevel(logger.TRACE)
bus := servicebus.New(myLogger)
eventSubsystem, _ := NewEvent(bus, myLogger)
eventSubsystem.Start()
eventSubsystem.RegisterListener("test", func(data ...interface{}) {
is.Equal(len(data), 1)
actual = data[0].(string)
wg.Done()
})
wg.Add(1)
eventMessage := &message.EventMessage{
Name: "test",
Data: []interface{}{"I am a message!"},
}
bus.Start()
bus.Publish("event:test:from:j", eventMessage)
wg.Wait()
bus.Stop()
is.Equal(actual, expected)
}

View File

@@ -0,0 +1,79 @@
package subsystem
import (
"strings"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Log is the Logging subsystem. It handles messages with topics starting
// with "log:"
type Log struct {
logChannel <-chan *servicebus.Message
quitChannel <-chan *servicebus.Message
running bool
// Logger!
logger *logger.Logger
}
// NewLog creates a new log subsystem
func NewLog(bus *servicebus.ServiceBus, logger *logger.Logger) (*Log, error) {
// Subscribe to log messages
logChannel, err := bus.Subscribe("log")
if err != nil {
return nil, err
}
// Subscribe to quit messages
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
result := &Log{
logChannel: logChannel,
quitChannel: quitChannel,
logger: logger,
}
return result, nil
}
// Start the subsystem
func (l *Log) Start() error {
l.running = true
// Spin off a go routine
go func() {
for l.running {
select {
case <-l.quitChannel:
l.running = false
break
case logMessage := <-l.logChannel:
logType := strings.TrimPrefix(logMessage.Topic(), "log:")
switch logType {
case "debug":
l.logger.Debug(logMessage.Data().(string))
case "info":
l.logger.Info(logMessage.Data().(string))
case "warning":
l.logger.Warning(logMessage.Data().(string))
case "error":
l.logger.Error(logMessage.Data().(string))
case "fatal":
l.logger.Fatal(logMessage.Data().(string))
default:
l.logger.Error("unknown log message: %+v", logMessage)
}
}
}
l.logger.Trace("Logger Shutdown")
}()
return nil
}

View File

@@ -0,0 +1,116 @@
package subsystem
import (
"fmt"
"strings"
"github.com/leaanthony/wailsv2/v2/internal/logger"
"github.com/leaanthony/wailsv2/v2/internal/runtime/goruntime"
"github.com/leaanthony/wailsv2/v2/internal/servicebus"
)
// Runtime is the Runtime subsystem. It handles messages with topics starting
// with "runtime:"
type Runtime struct {
quitChannel <-chan *servicebus.Message
runtimeChannel <-chan *servicebus.Message
running bool
logger logger.CustomLogger
// Runtime library
runtime *goruntime.Runtime
}
// NewRuntime creates a new runtime subsystem
func NewRuntime(bus *servicebus.ServiceBus, logger *logger.Logger) (*Runtime, error) {
// Register quit channel
quitChannel, err := bus.Subscribe("quit")
if err != nil {
return nil, err
}
// Subscribe to log messages
runtimeChannel, err := bus.Subscribe("runtime")
if err != nil {
return nil, err
}
result := &Runtime{
quitChannel: quitChannel,
runtimeChannel: runtimeChannel,
logger: logger.CustomLogger("Runtime Subsystem"),
runtime: goruntime.New(bus),
}
return result, nil
}
// Start the subsystem
func (r *Runtime) Start() error {
r.running = true
// Spin off a go routine
go func() {
for r.running {
select {
case <-r.quitChannel:
r.running = false
break
case runtimeMessage := <-r.runtimeChannel:
r.logger.Trace(fmt.Sprintf("Received message: %+v", runtimeMessage))
// Topics have the format: "runtime:category:call"
messageSlice := strings.Split(runtimeMessage.Topic(), ":")
if len(messageSlice) != 3 {
r.logger.Error("Invalid runtime message: %#v\n", runtimeMessage)
continue
}
category := messageSlice[1]
method := messageSlice[2]
var err error
switch category {
case "browser":
err = r.processBrowserMessage(method, runtimeMessage.Data())
default:
fmt.Errorf("unknown log message: %+v", runtimeMessage)
}
// If we had an error, log it
if err != nil {
r.logger.Error(err.Error())
}
}
}
// Call shutdown
r.shutdown()
}()
return nil
}
// GoRuntime returns the Go Runtime object
func (r *Runtime) GoRuntime() *goruntime.Runtime {
return r.runtime
}
func (r *Runtime) shutdown() {
r.logger.Trace("Shutdown")
}
func (r *Runtime) processBrowserMessage(method string, data interface{}) error {
switch method {
case "openurl":
url, ok := data.(string)
if !ok {
return fmt.Errorf("expected 1 string parameter for runtime:browser:openurl")
}
go r.runtime.Browser.Open(url)
default:
return fmt.Errorf("unknown method runtime:browser:%s", method)
}
return nil
}