Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add plugin system #10

Open
wants to merge 26 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6253afe
Add devcontainer support
tutman96 Dec 31, 2024
7bc6516
Add dev:device script and support for setting JETKVM_PROXY_URL for de…
tutman96 Jan 1, 2025
377c3e8
Implement plugin upload support and placeholder settings item
tutman96 Jan 1, 2025
0a77200
Add extracting and validating the plugin
tutman96 Jan 1, 2025
00fdbaf
Write plugin database to tmp file first
tutman96 Jan 4, 2025
3853b58
Implement pluginList RPC and associated UI
tutman96 Jan 4, 2025
88f3e97
Add enable/disable button
tutman96 Jan 4, 2025
5de7bc7
Add process_manager and subprocess spawning support
tutman96 Jan 4, 2025
2ffb463
Handle "errored" condition instead of "stopped"
tutman96 Jan 4, 2025
5a05719
When tar extraction fails, delete extraction folder
tutman96 Jan 4, 2025
79305da
Fix net Listener interface and implement max process backoff time
tutman96 Jan 5, 2025
5652e8f
Fix bad pointer reference
tutman96 Jan 5, 2025
562f6c4
Add ability to uninstall a plugin
tutman96 Jan 5, 2025
e764000
Golang standards :)
tutman96 Jan 5, 2025
27b3395
Newlines for all things
tutman96 Jan 5, 2025
ce86105
Merge branch 'main' into plugin-system
tutman96 Jan 5, 2025
0b3cd59
Refactor jsonrpc server into its own package
tutman96 Jan 5, 2025
e61decf
wip: Plugin RPC with status reporting to the UI
tutman96 Jan 5, 2025
2428c15
Handle error conditions better and detect support methods automatically
tutman96 Jan 6, 2025
2e24916
Change wording from TODO to coming soon
tutman96 Jan 6, 2025
16064aa
Better handle install and re-install lifecycle. Also display all the …
tutman96 Jan 6, 2025
d1abc4b
Handle messages async to datachannel receive
tutman96 Jan 6, 2025
6fd978b
Rename JSONRPCServer to JSONRPCRouter
tutman96 Jan 19, 2025
ec20835
Fix jsonrpc references
tutman96 Jan 30, 2025
1d6b7ad
chore: bump version to 0.3.5
ym Feb 11, 2025
b9c871c
Merge branch 'dev' into plugin-system
tutman96 Feb 11, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
VERSION_DEV := 0.3.5-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.3.4
VERSION_DEV := 0.3.6-dev$(shell date +%Y%m%d%H%M)
VERSION := 0.3.5

hash_resource:
@shasum -a 256 resource/jetkvm_native | cut -d ' ' -f 1 > resource/jetkvm_native.sha256
Expand Down
300 changes: 300 additions & 0 deletions internal/jsonrpc/router.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
package jsonrpc

import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"reflect"
"sync"
"sync/atomic"
"time"
)

type JSONRPCRouter struct {
writer io.Writer

handlers map[string]*RPCHandler
nextId atomic.Int64

responseChannelsMutex sync.Mutex
responseChannels map[int64]chan JSONRPCResponse
}

func NewJSONRPCRouter(writer io.Writer, handlers map[string]*RPCHandler) *JSONRPCRouter {
return &JSONRPCRouter{
writer: writer,
handlers: handlers,

responseChannels: make(map[int64]chan JSONRPCResponse),
}
}

func (s *JSONRPCRouter) Request(method string, params map[string]interface{}, result interface{}) *JSONRPCResponseError {
id := s.nextId.Add(1)
request := JSONRPCRequest{
JSONRPC: "2.0",
Method: method,
Params: params,
ID: id,
}
requestBytes, err := json.Marshal(request)
if err != nil {
return &JSONRPCResponseError{
Code: -32700,
Message: "Parse error",
Data: err,
}
}

// log.Printf("Sending RPC request: Method=%s, Params=%v, ID=%d", method, params, id)

responseChan := make(chan JSONRPCResponse, 1)
s.responseChannelsMutex.Lock()
s.responseChannels[id] = responseChan
s.responseChannelsMutex.Unlock()
defer func() {
s.responseChannelsMutex.Lock()
delete(s.responseChannels, id)
s.responseChannelsMutex.Unlock()
}()

_, err = s.writer.Write(requestBytes)
if err != nil {
return &JSONRPCResponseError{
Code: -32603,
Message: "Internal error",
Data: err,
}
}

timeout := time.After(5 * time.Second)
select {
case response := <-responseChan:
if response.Error != nil {
return response.Error
}

rawResult, err := json.Marshal(response.Result)
if err != nil {
return &JSONRPCResponseError{
Code: -32603,
Message: "Internal error",
Data: err,
}
}

if err := json.Unmarshal(rawResult, result); err != nil {
return &JSONRPCResponseError{
Code: -32603,
Message: "Internal error",
Data: err,
}
}

return nil
case <-timeout:
return &JSONRPCResponseError{
Code: -32603,
Message: "Internal error",
Data: "timeout waiting for response",
}
}
}

type JSONRPCMessage struct {
Method *string `json:"method,omitempty"`
ID *int64 `json:"id,omitempty"`
}

func (s *JSONRPCRouter) HandleMessage(data []byte) error {
// Data will either be a JSONRPCRequest or JSONRPCResponse object
// We need to determine which one it is
var raw JSONRPCMessage
err := json.Unmarshal(data, &raw)
if err != nil {
errorResponse := JSONRPCResponse{
JSONRPC: "2.0",
Error: &JSONRPCResponseError{
Code: -32700,
Message: "Parse error",
},
ID: 0,
}
return s.writeResponse(errorResponse)
}

if raw.Method == nil && raw.ID != nil {
var resp JSONRPCResponse
if err := json.Unmarshal(data, &resp); err != nil {
fmt.Println("error unmarshalling response", err)
return err
}

s.responseChannelsMutex.Lock()
responseChan, ok := s.responseChannels[*raw.ID]
s.responseChannelsMutex.Unlock()
if ok {
responseChan <- resp
} else {
log.Println("No response channel found for ID", resp.ID)
}
return nil
}

var request JSONRPCRequest
err = json.Unmarshal(data, &request)
if err != nil {
errorResponse := JSONRPCResponse{
JSONRPC: "2.0",
Error: &JSONRPCResponseError{
Code: -32700,
Message: "Parse error",
},
ID: 0,
}
return s.writeResponse(errorResponse)
}

//log.Printf("Received RPC request: Method=%s, Params=%v, ID=%d", request.Method, request.Params, request.ID)
handler, ok := s.handlers[request.Method]
if !ok {
errorResponse := JSONRPCResponse{
JSONRPC: "2.0",
Error: &JSONRPCResponseError{
Code: -32601,
Message: "Method not found",
},
ID: request.ID,
}
return s.writeResponse(errorResponse)
}

result, err := callRPCHandler(handler, request.Params)
if err != nil {
errorResponse := JSONRPCResponse{
JSONRPC: "2.0",
Error: &JSONRPCResponseError{
Code: -32603,
Message: "Internal error",
Data: err.Error(),
},
ID: request.ID,
}
return s.writeResponse(errorResponse)
}

response := JSONRPCResponse{
JSONRPC: "2.0",
Result: result,
ID: request.ID,
}
return s.writeResponse(response)
}

func (s *JSONRPCRouter) writeResponse(response JSONRPCResponse) error {
responseBytes, err := json.Marshal(response)
if err != nil {
return err
}
_, err = s.writer.Write(responseBytes)
return err
}

func callRPCHandler(handler *RPCHandler, params map[string]interface{}) (interface{}, error) {
handlerValue := reflect.ValueOf(handler.Func)
handlerType := handlerValue.Type()

if handlerType.Kind() != reflect.Func {
return nil, errors.New("handler is not a function")
}

numParams := handlerType.NumIn()
args := make([]reflect.Value, numParams)
// Get the parameter names from the RPCHandler
paramNames := handler.Params

if len(paramNames) != numParams {
return nil, errors.New("mismatch between handler parameters and defined parameter names")
}

for i := 0; i < numParams; i++ {
paramType := handlerType.In(i)
paramName := paramNames[i]
paramValue, ok := params[paramName]
if !ok {
return nil, errors.New("missing parameter: " + paramName)
}

convertedValue := reflect.ValueOf(paramValue)
if !convertedValue.Type().ConvertibleTo(paramType) {
if paramType.Kind() == reflect.Slice && (convertedValue.Kind() == reflect.Slice || convertedValue.Kind() == reflect.Array) {
newSlice := reflect.MakeSlice(paramType, convertedValue.Len(), convertedValue.Len())
for j := 0; j < convertedValue.Len(); j++ {
elemValue := convertedValue.Index(j)
if elemValue.Kind() == reflect.Interface {
elemValue = elemValue.Elem()
}
if !elemValue.Type().ConvertibleTo(paramType.Elem()) {
// Handle float64 to uint8 conversion
if elemValue.Kind() == reflect.Float64 && paramType.Elem().Kind() == reflect.Uint8 {
intValue := int(elemValue.Float())
if intValue < 0 || intValue > 255 {
return nil, fmt.Errorf("value out of range for uint8: %v", intValue)
}
newSlice.Index(j).SetUint(uint64(intValue))
} else {
fromType := elemValue.Type()
toType := paramType.Elem()
return nil, fmt.Errorf("invalid element type in slice for parameter %s: from %v to %v", paramName, fromType, toType)
}
} else {
newSlice.Index(j).Set(elemValue.Convert(paramType.Elem()))
}
}
args[i] = newSlice
} else if paramType.Kind() == reflect.Struct && convertedValue.Kind() == reflect.Map {
jsonData, err := json.Marshal(convertedValue.Interface())
if err != nil {
return nil, fmt.Errorf("failed to marshal map to JSON: %v", err)
}

newStruct := reflect.New(paramType).Interface()
if err := json.Unmarshal(jsonData, newStruct); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON into struct: %v", err)
}
args[i] = reflect.ValueOf(newStruct).Elem()
} else {
return nil, fmt.Errorf("invalid parameter type for: %s", paramName)
}
} else {
args[i] = convertedValue.Convert(paramType)
}
}

results := handlerValue.Call(args)

if len(results) == 0 {
return nil, nil
}

if len(results) == 1 {
if results[0].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if !results[0].IsNil() {
return nil, results[0].Interface().(error)
}
return nil, nil
}
return results[0].Interface(), nil
}

if len(results) == 2 && results[1].Type().Implements(reflect.TypeOf((*error)(nil)).Elem()) {
if !results[1].IsNil() {
return nil, results[1].Interface().(error)
}
return results[0].Interface(), nil
}

return nil, errors.New("unexpected return values from handler")
}
32 changes: 32 additions & 0 deletions internal/jsonrpc/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package jsonrpc

type JSONRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params map[string]interface{} `json:"params,omitempty"`
ID interface{} `json:"id,omitempty"`
}

type JSONRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
Result interface{} `json:"result,omitempty"`
Error *JSONRPCResponseError `json:"error,omitempty"`
ID interface{} `json:"id"`
}

type JSONRPCResponseError struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}

type JSONRPCEvent struct {
JSONRPC string `json:"jsonrpc"`
Method string `json:"method"`
Params interface{} `json:"params,omitempty"`
}

type RPCHandler struct {
Func interface{}
Params []string
}
Loading